| @@ -18,6 +18,11 @@ spec: | |||||
| image: ${k8s-5auth-image} | image: ${k8s-5auth-image} | ||||
| ports: | ports: | ||||
| - containerPort: 9200 | - containerPort: 9200 | ||||
| env: | |||||
| - name: TZ | |||||
| value: Asia/Shanghai | |||||
| - name: JAVA_TOOL_OPTIONS | |||||
| value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" | |||||
| --- | --- | ||||
| apiVersion: v1 | apiVersion: v1 | ||||
| @@ -28,9 +33,15 @@ metadata: | |||||
| spec: | spec: | ||||
| type: NodePort | type: NodePort | ||||
| ports: | ports: | ||||
| - port: 9200 | |||||
| - name: http | |||||
| port: 9200 | |||||
| nodePort: 31206 | nodePort: 31206 | ||||
| protocol: TCP | protocol: TCP | ||||
| - name: debug | |||||
| nodePort: 31221 | |||||
| port: 5005 | |||||
| protocol: TCP | |||||
| targetPort: 5005 | |||||
| selector: | selector: | ||||
| app: ci4s-auth | app: ci4s-auth | ||||
| @@ -141,12 +141,23 @@ export default [ | |||||
| { | { | ||||
| name: '实验对比', | name: '实验对比', | ||||
| path: 'compare', | path: 'compare', | ||||
| component: './Experiment/Comparison/index', | |||||
| routes: [ | |||||
| { | |||||
| name: '实验对比', | |||||
| path: '', | |||||
| component: './Experiment/Comparison/index', | |||||
| }, | |||||
| { | |||||
| name: '可视化对比', | |||||
| path: 'compare-visual', | |||||
| component: './Experiment/Aim/index', | |||||
| }, | |||||
| ], | |||||
| }, | }, | ||||
| { | { | ||||
| name: '实验可视化对比', | |||||
| path: 'compare-visual', | |||||
| component: './Experiment/Aim/index', | |||||
| name: '可视化', | |||||
| path: 'visual', | |||||
| component: './Experiment/Tensorboard/index', | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| @@ -218,7 +229,18 @@ export default [ | |||||
| { | { | ||||
| name: '实验实例详情', | name: '实验实例详情', | ||||
| path: 'instance/:experimentId/:id', | path: 'instance/:experimentId/:id', | ||||
| component: './HyperParameter/Instance/index', | |||||
| routes: [ | |||||
| { | |||||
| name: '实验实例详情', | |||||
| path: '', | |||||
| component: './HyperParameter/Instance/index', | |||||
| }, | |||||
| { | |||||
| name: '可视化对比', | |||||
| path: 'compare-visual', | |||||
| component: './HyperParameter/Aim/index', | |||||
| }, | |||||
| ], | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| @@ -21,6 +21,7 @@ import { | |||||
| } from './services/session'; | } from './services/session'; | ||||
| import './styles/menu.less'; | import './styles/menu.less'; | ||||
| import { needAuth } from './utils'; | import { needAuth } from './utils'; | ||||
| // import { closeAllModals } from './utils/modal'; | |||||
| import { gotoLoginPage } from './utils/ui'; | import { gotoLoginPage } from './utils/ui'; | ||||
| export { requestConfig as request } from './requestConfig'; | export { requestConfig as request } from './requestConfig'; | ||||
| @@ -96,6 +97,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||||
| }, | }, | ||||
| onPageChange: () => { | onPageChange: () => { | ||||
| const { location } = history; | const { location } = history; | ||||
| // closeAllModals(); | |||||
| // 如果没有登录,重定向到 login | // 如果没有登录,重定向到 login | ||||
| if (!initialState?.currentUser && needAuth(location.pathname)) { | if (!initialState?.currentUser && needAuth(location.pathname)) { | ||||
| gotoLoginPage(); | gotoLoginPage(); | ||||
| @@ -1,12 +1,11 @@ | |||||
| import FullScreenFrame from '@/components/FullScreenFrame'; | import FullScreenFrame from '@/components/FullScreenFrame'; | ||||
| import KFSpin from '@/components/KFSpin'; | |||||
| import { getKnowledgeGraphUrl, getLabelStudioUrl } from '@/services/developmentEnvironment'; | import { getKnowledgeGraphUrl, getLabelStudioUrl } from '@/services/developmentEnvironment'; | ||||
| import Loading from '@/utils/loading'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import { FloatButton } from 'antd'; | import { FloatButton } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import { createPortal } from 'react-dom'; | |||||
| import './index.less'; | import './index.less'; | ||||
| export enum IframePageType { | export enum IframePageType { | ||||
| @@ -54,9 +53,13 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||||
| type IframePageProps = { | type IframePageProps = { | ||||
| /** 子系统 */ | /** 子系统 */ | ||||
| type: IframePageType; | |||||
| type?: IframePageType; | |||||
| /** url */ | |||||
| url?: string; | |||||
| /** 是否可以在页签上打开 */ | /** 是否可以在页签上打开 */ | ||||
| openInTab?: boolean; | openInTab?: boolean; | ||||
| /** 是否显示加载 */ | |||||
| showLoading?: boolean; | |||||
| /** 自定义样式类名 */ | /** 自定义样式类名 */ | ||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | /** 自定义样式 */ | ||||
| @@ -64,32 +67,60 @@ type IframePageProps = { | |||||
| }; | }; | ||||
| /** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ | /** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ | ||||
| function IframePage({ type, openInTab = false, className, style }: IframePageProps) { | |||||
| function IframePage({ | |||||
| type, | |||||
| url, | |||||
| showLoading = true, | |||||
| openInTab = false, | |||||
| className, | |||||
| style, | |||||
| }: IframePageProps) { | |||||
| const [iframeUrl, setIframeUrl] = useState(''); | const [iframeUrl, setIframeUrl] = useState(''); | ||||
| const [loading, setLoading] = useState(false); | |||||
| // const [loading, setLoading] = useState(false); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const requestIframeUrl = async () => { | |||||
| setLoading(true); | |||||
| const requestIframeUrl = async (type: IframePageType) => { | |||||
| if (showLoading) { | |||||
| Loading.show(); | |||||
| } | |||||
| const [res] = await to(getRequestAPI(type)()); | const [res] = await to(getRequestAPI(type)()); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| setIframeUrl(res.data); | setIframeUrl(res.data); | ||||
| } else { | } else { | ||||
| setLoading(false); | |||||
| if (showLoading) { | |||||
| Loading.hide(); | |||||
| } | |||||
| } | } | ||||
| }; | }; | ||||
| requestIframeUrl(); | |||||
| }, [type]); | |||||
| if (type) { | |||||
| requestIframeUrl(type); | |||||
| } else if (url) { | |||||
| if (showLoading) { | |||||
| Loading.show(); | |||||
| } | |||||
| setIframeUrl(url); | |||||
| } | |||||
| }, [type, url, showLoading]); | |||||
| const handleLoad = () => { | |||||
| if (showLoading) { | |||||
| Loading.hide(); | |||||
| } | |||||
| }; | |||||
| const hideLoading = () => { | |||||
| setLoading(false); | |||||
| const handleError = (error?: React.SyntheticEvent<HTMLIFrameElement, Event>) => { | |||||
| console.log('error', error); | |||||
| if (showLoading) { | |||||
| Loading.hide(); | |||||
| } | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <div className={classNames('kf-iframe-page', className)} style={style}> | <div className={classNames('kf-iframe-page', className)} style={style}> | ||||
| {loading && createPortal(<KFSpin size="large" />, document.body)} | |||||
| <FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} /> | |||||
| {/* {loading && createPortal(<KFSpin size="large" />, document.body)} */} | |||||
| {iframeUrl && <FullScreenFrame url={iframeUrl} onLoad={handleLoad} onError={handleError} />} | |||||
| {openInTab && <FloatButton onClick={() => window.open(iframeUrl, '_blank')} />} | {openInTab && <FloatButton onClick={() => window.open(iframeUrl, '_blank')} />} | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -1,7 +1,7 @@ | |||||
| import datasetImg from '@/assets/img/modal-select-dataset.png'; | import datasetImg from '@/assets/img/modal-select-dataset.png'; | ||||
| import mirrorImg from '@/assets/img/modal-select-mirror.png'; | import mirrorImg from '@/assets/img/modal-select-mirror.png'; | ||||
| import modelImg from '@/assets/img/modal-select-model.png'; | import modelImg from '@/assets/img/modal-select-model.png'; | ||||
| import { AvailableRange, CommonTabKeys } from '@/enums'; | |||||
| import { AvailableRange, CommonTabKeys, MirrorVersionStatus } from '@/enums'; | |||||
| import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config'; | import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config'; | ||||
| import { MirrorVersionData } from '@/pages/Mirror/Info'; | import { MirrorVersionData } from '@/pages/Mirror/Info'; | ||||
| import { MirrorData } from '@/pages/Mirror/List'; | import { MirrorData } from '@/pages/Mirror/List'; | ||||
| @@ -24,11 +24,11 @@ export enum ResourceSelectorType { | |||||
| } | } | ||||
| // 数据集、模型列表转为树形结构 | // 数据集、模型列表转为树形结构 | ||||
| const convertDatasetToTreeData = (list: ResourceData[]): TreeDataNode[] => { | |||||
| const convertDatasetToTreeData = (list: ResourceData[], isPublic: boolean): TreeDataNode[] => { | |||||
| return list.map((v) => ({ | return list.map((v) => ({ | ||||
| ...v, | ...v, | ||||
| key: `${v.id}`, | key: `${v.id}`, | ||||
| title: v.name, | |||||
| title: isPublic ? `${v.name} (${v.owner})` : v.name, | |||||
| isLeaf: false, | isLeaf: false, | ||||
| checkable: false, | checkable: false, | ||||
| })); | })); | ||||
| @@ -106,7 +106,7 @@ export class DatasetSelector implements SelectorTypeInfo { | |||||
| const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 }); | const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 }); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const list = res.data.content || []; | const list = res.data.content || []; | ||||
| return convertDatasetToTreeData(list); | |||||
| return convertDatasetToTreeData(list, isPublic); | |||||
| } else { | } else { | ||||
| return Promise.reject('获取数据集列表失败'); | return Promise.reject('获取数据集列表失败'); | ||||
| } | } | ||||
| @@ -158,7 +158,7 @@ export class ModelSelector implements SelectorTypeInfo { | |||||
| const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 }); | const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 }); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const list = res.data.content || []; | const list = res.data.content || []; | ||||
| return convertDatasetToTreeData(list); | |||||
| return convertDatasetToTreeData(list, isPublic); | |||||
| } else { | } else { | ||||
| return Promise.reject('获取模型列表失败'); | return Promise.reject('获取模型列表失败'); | ||||
| } | } | ||||
| @@ -224,8 +224,7 @@ export class MirrorSelector implements SelectorTypeInfo { | |||||
| image_id: parentKey, | image_id: parentKey, | ||||
| page: 0, | page: 0, | ||||
| size: 2000, | size: 2000, | ||||
| status: 'available', | |||||
| state: 1, | |||||
| status: MirrorVersionStatus.Available, | |||||
| }); | }); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const list = res.data.content || []; | const list = res.data.content || []; | ||||
| @@ -1,10 +1,11 @@ | |||||
| import { clearSessionToken } from '@/access'; | import { clearSessionToken } from '@/access'; | ||||
| import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||||
| import { setRemoteMenu } from '@/services/session'; | import { setRemoteMenu } from '@/services/session'; | ||||
| import { logout } from '@/services/system/auth'; | import { logout } from '@/services/system/auth'; | ||||
| import { ClientInfo } from '@/types'; | import { ClientInfo } from '@/types'; | ||||
| import { sleep } from '@/utils/promise'; | |||||
| import { sleep, to } from '@/utils/promise'; | |||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import { gotoLoginPage, oauthLogout } from '@/utils/ui'; | |||||
| import { oauthLogout } from '@/utils/ui'; | |||||
| import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | ||||
| import { setAlpha } from '@ant-design/pro-components'; | import { setAlpha } from '@ant-design/pro-components'; | ||||
| import { useEmotionCss } from '@ant-design/use-emotion-css'; | import { useEmotionCss } from '@ant-design/use-emotion-css'; | ||||
| @@ -63,17 +64,23 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||||
| * 退出登录,并且将当前的 url 保存 | * 退出登录,并且将当前的 url 保存 | ||||
| */ | */ | ||||
| const loginOut = async () => { | const loginOut = async () => { | ||||
| oauthLogout('http://172.20.32.197:31209/oauth/logout'); | |||||
| const [res] = await to(getLabelStudioUrl()); | |||||
| if (res && res.data) { | |||||
| oauthLogout(`${res.data}/oauth/logout`); | |||||
| } | |||||
| // 至少 1 秒后跳转,希望子系统能完成注销 | // 至少 1 秒后跳转,希望子系统能完成注销 | ||||
| await Promise.all([logout(), sleep(1000)]); | await Promise.all([logout(), sleep(1000)]); | ||||
| clearSessionToken(); | clearSessionToken(); | ||||
| setRemoteMenu(null); | setRemoteMenu(null); | ||||
| gotoLoginPage(); | |||||
| // 退出 oauth2 | |||||
| const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | ||||
| if (clientInfo) { | if (clientInfo) { | ||||
| const { logoutUri } = clientInfo; | const { logoutUri } = clientInfo; | ||||
| location.replace(logoutUri); | location.replace(logoutUri); | ||||
| } | } | ||||
| // setTimeout(() => { | |||||
| // gotoLoginPage(); | |||||
| // }, 100); | |||||
| }; | }; | ||||
| const actionClassName = useEmotionCss(({ token }) => { | const actionClassName = useEmotionCss(({ token }) => { | ||||
| return { | return { | ||||
| @@ -10,13 +10,21 @@ type RunDurationProps = { | |||||
| }; | }; | ||||
| function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) { | function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) { | ||||
| const [now] = useServerTime(); | const [now] = useServerTime(); | ||||
| const [currentTime, setCurrentTime] = useState<Date>(now()); | |||||
| const [currentTime, setCurrentTime] = useState<Date>(finishTime ? new Date(finishTime) : now()); | |||||
| // console.log( | |||||
| // 'currentTime', | |||||
| // new Date(createTime ?? 0), | |||||
| // currentTime, | |||||
| // (currentTime.getTime() - new Date(createTime ?? 0).getTime()) / 1000, | |||||
| // ); | |||||
| // 定时刷新耗时 | // 定时刷新耗时 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (finishTime) { | if (finishTime) { | ||||
| setCurrentTime(new Date(finishTime)); | setCurrentTime(new Date(finishTime)); | ||||
| } else { | } else { | ||||
| setCurrentTime(now()); | |||||
| const timer = setInterval(() => { | const timer = setInterval(() => { | ||||
| setCurrentTime(now()); | setCurrentTime(now()); | ||||
| }, 1000); | }, 1000); | ||||
| @@ -25,6 +33,7 @@ function RunDuration({ createTime, finishTime, className, style }: RunDurationPr | |||||
| }; | }; | ||||
| } | } | ||||
| }, [finishTime, now]); | }, [finishTime, now]); | ||||
| return ( | return ( | ||||
| <span className={className} style={style}> | <span className={className} style={style}> | ||||
| {elapsedTime(createTime, currentTime)} | {elapsedTime(createTime, currentTime)} | ||||
| @@ -33,7 +33,7 @@ export enum TensorBoardStatus { | |||||
| Unknown = 'Unknown', // 未知 | Unknown = 'Unknown', // 未知 | ||||
| Pending = 'Pending', // 启动中 | Pending = 'Pending', // 启动中 | ||||
| Running = 'Running', // 运行中 | Running = 'Running', // 运行中 | ||||
| Terminated = 'Terminated', // 未启动或者已终止 | |||||
| Terminated = 'Terminated', // 未启动 | |||||
| Failed = 'Failed', // 失败 | Failed = 'Failed', // 失败 | ||||
| } | } | ||||
| @@ -95,8 +95,8 @@ export enum AutoMLType { | |||||
| export const autoMLTypeOptions = [ | export const autoMLTypeOptions = [ | ||||
| { label: '表格', value: AutoMLType.Table }, | { label: '表格', value: AutoMLType.Table }, | ||||
| { label: '文本分类', value: AutoMLType.Text }, | |||||
| { label: '视频分类', value: AutoMLType.Video }, | |||||
| { label: '文本', value: AutoMLType.Text }, | |||||
| { label: '视频', value: AutoMLType.Video }, | |||||
| ]; | ]; | ||||
| // 自动化任务类型 | // 自动化任务类型 | ||||
| @@ -1,11 +1,24 @@ | |||||
| import { parseJsonText } from '@/utils'; | |||||
| import { useEffect } from 'react'; | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| import { parseJsonText } from '@/utils'; | |||||
| import { useEffect } from 'react'; | |||||
| export type MessageHandler = (experimentInsId: number, status: string, finishedAt: string, nodes: Record<string, NodeStatus>) => void | |||||
| export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: string, namespace: string, onMessage: MessageHandler) => { | |||||
| const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running | |||||
| export type MessageHandler = ( | |||||
| experimentId: number, | |||||
| experimentInsId: number, | |||||
| status: string, | |||||
| finishTime: string, | |||||
| nodes: Record<string, NodeStatus>, | |||||
| ) => void; | |||||
| export const useSSE = ( | |||||
| experimentId: number, | |||||
| experimentInsId: number, | |||||
| status: ExperimentStatus, | |||||
| name: string, | |||||
| namespace: string, | |||||
| onMessage: MessageHandler, | |||||
| ) => { | |||||
| const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (isRunning) { | if (isRunning) { | ||||
| const { origin } = location; | const { origin } = location; | ||||
| @@ -22,8 +35,8 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: | |||||
| const dataJson = parseJsonText(data); | const dataJson = parseJsonText(data); | ||||
| const statusData = dataJson?.result?.object?.status; | const statusData = dataJson?.result?.object?.status; | ||||
| if (statusData) { | if (statusData) { | ||||
| const { finishedAt, phase, nodes } = statusData; | |||||
| onMessage(experimentInsId, phase, finishedAt, nodes); | |||||
| const { finishedAt, phase, nodes } = statusData; | |||||
| onMessage(experimentId, experimentInsId, phase, finishedAt, nodes); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -33,8 +46,7 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: | |||||
| return () => { | return () => { | ||||
| evtSource.close(); | evtSource.close(); | ||||
| } | |||||
| }; | |||||
| } | } | ||||
| }, [experimentInsId, isRunning, name, namespace, onMessage]); | |||||
| }, [experimentId, experimentInsId, isRunning, name, namespace, onMessage]); | |||||
| }; | }; | ||||
| @@ -51,13 +51,12 @@ function ActiveLearnInstance() { | |||||
| const [res] = await to(getActiveLearnInsReq(instanceId)); | const [res] = await to(getActiveLearnInsReq(instanceId)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const info = res.data as ActiveLearnInstanceData; | const info = res.data as ActiveLearnInstanceData; | ||||
| 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 } = info; | |||||
| // 解析配置参数 | // 解析配置参数 | ||||
| const paramJson = parseJsonText(param); | const paramJson = parseJsonText(param); | ||||
| if (paramJson) { | if (paramJson) { | ||||
| setExperimentInfo({ | setExperimentInfo({ | ||||
| ...paramJson.data, | ...paramJson.data, | ||||
| create_time, | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -69,7 +68,7 @@ function ActiveLearnInstance() { | |||||
| return; | return; | ||||
| } | } | ||||
| // 进行节点状态 | |||||
| // 设置总 workflow 状态 | |||||
| const nodeStatusJson = parseJsonText(node_status); | const nodeStatusJson = parseJsonText(node_status); | ||||
| if (nodeStatusJson) { | if (nodeStatusJson) { | ||||
| setNodes(nodeStatusJson); | setNodes(nodeStatusJson); | ||||
| @@ -106,18 +105,17 @@ 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); | setNodes(nodes); | ||||
| // 设置总 workflow 状态 | |||||
| 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; | ||||
| // 设置工作流状态 | |||||
| if (workflowStatus) { | if (workflowStatus) { | ||||
| setWorkflowStatus(workflowStatus); | setWorkflowStatus(workflowStatus); | ||||
| // 实验结束,关闭 SSE | |||||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||||
| if ( | if ( | ||||
| workflowStatus.phase !== ExperimentStatus.Pending && | workflowStatus.phase !== ExperimentStatus.Pending && | ||||
| workflowStatus.phase !== ExperimentStatus.Running | workflowStatus.phase !== ExperimentStatus.Running | ||||
| @@ -152,8 +150,8 @@ function ActiveLearnInstance() { | |||||
| <ActiveLearnBasic | <ActiveLearnBasic | ||||
| className={styles['active-learn-instance__basic']} | className={styles['active-learn-instance__basic']} | ||||
| info={experimentInfo} | info={experimentInfo} | ||||
| runStatus={workflowStatus} | |||||
| instanceStatus={instanceInfo?.status} | |||||
| workflowStatus={workflowStatus} | |||||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -181,7 +179,7 @@ function ActiveLearnInstance() { | |||||
| }, | }, | ||||
| { | { | ||||
| key: TabKeys.History, | key: TabKeys.History, | ||||
| label: '训练列表', | |||||
| label: '运行列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: ( | children: ( | ||||
| <ExperimentHistory | <ExperimentHistory | ||||
| @@ -28,14 +28,14 @@ type BasicInfoProps = { | |||||
| info?: ActiveLearnData; | info?: ActiveLearnData; | ||||
| className?: string; | className?: string; | ||||
| isInstance?: boolean; | isInstance?: boolean; | ||||
| runStatus?: NodeStatus; | |||||
| workflowStatus?: NodeStatus; | |||||
| instanceStatus?: ExperimentStatus; | instanceStatus?: ExperimentStatus; | ||||
| }; | }; | ||||
| function BasicInfo({ | function BasicInfo({ | ||||
| info, | info, | ||||
| className, | className, | ||||
| runStatus, | |||||
| workflowStatus, | |||||
| instanceStatus, | instanceStatus, | ||||
| isInstance = false, | isInstance = false, | ||||
| }: BasicInfoProps) { | }: BasicInfoProps) { | ||||
| @@ -154,7 +154,7 @@ function BasicInfo({ | |||||
| value: info.dataset_py, | value: info.dataset_py, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '数据集类名', | |||||
| label: '数据集处理类名', | |||||
| value: info.dataset_class_name, | value: info.dataset_class_name, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -212,12 +212,8 @@ function BasicInfo({ | |||||
| return ( | return ( | ||||
| <div className={classNames(styles['active-learn-basic'], className)}> | <div className={classNames(styles['active-learn-basic'], className)}> | ||||
| {isInstance && runStatus && ( | |||||
| <ExperimentRunBasic | |||||
| create_time={info?.create_time} | |||||
| runStatus={runStatus} | |||||
| instanceStatus={instanceStatus} | |||||
| /> | |||||
| {isInstance && workflowStatus && ( | |||||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||||
| )} | )} | ||||
| {!isInstance && ( | {!isInstance && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| @@ -17,6 +17,11 @@ import { | |||||
| function ExecuteConfig() { | function ExecuteConfig() { | ||||
| const form = Form.useFormInstance(); | const form = Form.useFormInstance(); | ||||
| const task_type = Form.useWatch('task_type', form); | |||||
| const queryStrategiesOptions = | |||||
| task_type === AutoMLTaskType.Classification | |||||
| ? queryStrategies.slice(0, 2) | |||||
| : queryStrategies.slice(2); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <SubAreaTitle | <SubAreaTitle | ||||
| @@ -101,16 +106,16 @@ function ExecuteConfig() { | |||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="数据集类名" | |||||
| label="数据集处理类名" | |||||
| name="dataset_class_name" | name="dataset_class_name" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | required: true, | ||||
| message: '请输入数据集类名', | |||||
| message: '请输入数据集处理类名', | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <Input placeholder="请输入数据集类名" maxLength={64} showCount allowClear /> | |||||
| <Input placeholder="请输入数据集处理类名" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| @@ -488,7 +493,12 @@ function ExecuteConfig() { | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <Select placeholder="请选择查询策略" options={queryStrategies} showSearch allowClear /> | |||||
| <Select | |||||
| placeholder="请选择查询策略" | |||||
| options={queryStrategiesOptions} | |||||
| showSearch | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| @@ -87,4 +87,8 @@ export const queryStrategies = [ | |||||
| label: 'upper_confidence_bound', | label: 'upper_confidence_bound', | ||||
| value: 'upper_confidence_bound', | value: 'upper_confidence_bound', | ||||
| }, | }, | ||||
| { | |||||
| label: 'probability_of_improvement', | |||||
| value: 'probability_of_improvement', | |||||
| }, | |||||
| ]; | ]; | ||||
| @@ -1,5 +1,6 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types'; | import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types'; | ||||
| import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty'; | |||||
| import LogList from '@/pages/Experiment/components/LogList'; | import LogList from '@/pages/Experiment/components/LogList'; | ||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| import { Tabs } from 'antd'; | import { Tabs } from 'antd'; | ||||
| @@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| // icon: <KFIcon type="icon-rizhi1" />, | // icon: <KFIcon type="icon-rizhi1" />, | ||||
| children: ( | children: ( | ||||
| <div className={styles['experiment-log__tabs__log']}> | <div className={styles['experiment-log__tabs__log']}> | ||||
| {trainCloneNodeStatus && ( | |||||
| {trainCloneNodeStatus ? ( | |||||
| <LogList | <LogList | ||||
| instanceName={instanceInfo.argo_ins_name} | instanceName={instanceInfo.argo_ins_name} | ||||
| instanceNamespace={instanceInfo.argo_ins_ns} | instanceNamespace={instanceInfo.argo_ins_ns} | ||||
| @@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| instanceNodeStartTime={trainCloneNodeStatus.startedAt} | instanceNodeStartTime={trainCloneNodeStatus.startedAt} | ||||
| instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | ||||
| ></LogList> | ></LogList> | ||||
| ) : ( | |||||
| <EmptyLog /> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| ), | ), | ||||
| @@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| // icon: <KFIcon type="icon-rizhi1" />, | // icon: <KFIcon type="icon-rizhi1" />, | ||||
| children: ( | children: ( | ||||
| <div className={styles['experiment-log__tabs__log']}> | <div className={styles['experiment-log__tabs__log']}> | ||||
| {hpoNodeStatus && ( | |||||
| {hpoNodeStatus ? ( | |||||
| <LogList | <LogList | ||||
| instanceName={instanceInfo.argo_ins_name} | instanceName={instanceInfo.argo_ins_name} | ||||
| instanceNamespace={instanceInfo.argo_ins_ns} | instanceNamespace={instanceInfo.argo_ins_ns} | ||||
| @@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| instanceNodeStartTime={hpoNodeStatus.startedAt} | instanceNodeStartTime={hpoNodeStatus.startedAt} | ||||
| instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | ||||
| ></LogList> | ></LogList> | ||||
| ) : ( | |||||
| <EmptyLog /> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| ), | ), | ||||
| @@ -51,7 +51,7 @@ function AutoMLInstance() { | |||||
| const [res] = await to(getExperimentInsReq(instanceId)); | const [res] = await to(getExperimentInsReq(instanceId)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const info = res.data as AutoMLInstanceData; | const info = res.data as AutoMLInstanceData; | ||||
| const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time, type } = info; | |||||
| const { param, node_status, argo_ins_name, argo_ins_ns, status, type } = info; | |||||
| setType(type); | setType(type); | ||||
| // 解析配置参数 | // 解析配置参数 | ||||
| @@ -59,7 +59,6 @@ function AutoMLInstance() { | |||||
| if (paramJson) { | if (paramJson) { | ||||
| setAutoMLInfo({ | setAutoMLInfo({ | ||||
| ...paramJson.data, | ...paramJson.data, | ||||
| create_time, | |||||
| type, | type, | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -95,7 +94,10 @@ function AutoMLInstance() { | |||||
| }; | }; | ||||
| const setupSSE = (name: string, namespace: string) => { | const setupSSE = (name: string, namespace: string) => { | ||||
| const { origin } = location; | |||||
| let { origin } = location; | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| origin = 'http://172.20.32.235:31213'; | |||||
| } | |||||
| 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( | ||||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | ||||
| @@ -110,17 +112,17 @@ 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); | setNodes(nodes); | ||||
| // 设置总 workflow 状态 | |||||
| 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; | ||||
| if (workflowStatus) { | if (workflowStatus) { | ||||
| setWorkflowStatus(workflowStatus); | setWorkflowStatus(workflowStatus); | ||||
| // 实验结束,关闭 SSE | |||||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||||
| if ( | if ( | ||||
| workflowStatus.phase !== ExperimentStatus.Pending && | workflowStatus.phase !== ExperimentStatus.Pending && | ||||
| workflowStatus.phase !== ExperimentStatus.Running | workflowStatus.phase !== ExperimentStatus.Running | ||||
| @@ -155,8 +157,8 @@ function AutoMLInstance() { | |||||
| <AutoMLBasic | <AutoMLBasic | ||||
| className={styles['auto-ml-instance__basic']} | className={styles['auto-ml-instance__basic']} | ||||
| info={autoMLInfo} | info={autoMLInfo} | ||||
| runStatus={workflowStatus} | |||||
| instanceStatus={instanceInfo?.status} | |||||
| workflowStatus={workflowStatus} | |||||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -202,7 +204,7 @@ function AutoMLInstance() { | |||||
| } | } | ||||
| : { | : { | ||||
| key: TabKeys.History, | key: TabKeys.History, | ||||
| label: '试验列表', | |||||
| label: '运行列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | icon: <KFIcon type="icon-Trialliebiao" />, | ||||
| children: ( | children: ( | ||||
| <ExperimentHistory | <ExperimentHistory | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 自主机器学习列表 | |||||
| * @Description: 自动机器学习列表 | |||||
| */ | */ | ||||
| import ExperimentList, { ExperimentListType } from '../components/ExperimentList'; | import ExperimentList, { ExperimentListType } from '../components/ExperimentList'; | ||||
| @@ -7,10 +7,21 @@ import { | |||||
| autoMLTaskTypeOptions, | autoMLTaskTypeOptions, | ||||
| } from '@/enums'; | } from '@/enums'; | ||||
| import { useComputingResource } from '@/hooks/useComputingResource'; | import { useComputingResource } from '@/hooks/useComputingResource'; | ||||
| import { | |||||
| classificationAlgorithms, | |||||
| featureAlgorithms, | |||||
| regressorAlgorithms, | |||||
| } from '@/pages/AutoML/components/CreateForm/utils'; | |||||
| import { AutoMLData } from '@/pages/AutoML/types'; | import { AutoMLData } from '@/pages/AutoML/types'; | ||||
| import { type NodeStatus } from '@/types'; | import { type NodeStatus } from '@/types'; | ||||
| import { parseJsonText } from '@/utils'; | import { parseJsonText } from '@/utils'; | ||||
| import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format'; | |||||
| import { | |||||
| formatBoolean, | |||||
| formatDataset, | |||||
| formatDate, | |||||
| formatEnum, | |||||
| type EnumOptions, | |||||
| } from '@/utils/format'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import ExperimentRunBasic from '../ExperimentRunBasic'; | import ExperimentRunBasic from '../ExperimentRunBasic'; | ||||
| @@ -21,6 +32,7 @@ const formatOptimizeMode = (value: boolean) => { | |||||
| return value ? '越大越好' : '越小越好'; | return value ? '越大越好' : '越小越好'; | ||||
| }; | }; | ||||
| // 格式化权重 | |||||
| const formatMetricsWeight = (value: string) => { | const formatMetricsWeight = (value: string) => { | ||||
| if (!value) { | if (!value) { | ||||
| return '--'; | return '--'; | ||||
| @@ -34,18 +46,33 @@ const formatMetricsWeight = (value: string) => { | |||||
| .join('\n'); | .join('\n'); | ||||
| }; | }; | ||||
| // 格式化算法 | |||||
| const formatAlgorithm = (algorithms: EnumOptions[]) => { | |||||
| return (value: string) => { | |||||
| if (!value) { | |||||
| return '--'; | |||||
| } | |||||
| const list = value | |||||
| .split(',') | |||||
| .filter((v) => v !== '') | |||||
| .map((v) => v.trim()); | |||||
| return list.map((v) => formatEnum(algorithms)(v)).join(','); | |||||
| }; | |||||
| }; | |||||
| type AutoMLBasicProps = { | type AutoMLBasicProps = { | ||||
| info?: AutoMLData; | info?: AutoMLData; | ||||
| className?: string; | className?: string; | ||||
| isInstance?: boolean; | isInstance?: boolean; | ||||
| runStatus?: NodeStatus; | |||||
| workflowStatus?: NodeStatus; | |||||
| instanceStatus?: ExperimentStatus; | instanceStatus?: ExperimentStatus; | ||||
| instanceCreateTime?: string; | |||||
| }; | }; | ||||
| function AutoMLBasic({ | function AutoMLBasic({ | ||||
| info, | info, | ||||
| className, | className, | ||||
| runStatus, | |||||
| workflowStatus, | |||||
| instanceStatus, | instanceStatus, | ||||
| isInstance = false, | isInstance = false, | ||||
| }: AutoMLBasicProps) { | }: AutoMLBasicProps) { | ||||
| @@ -95,10 +122,12 @@ function AutoMLBasic({ | |||||
| { | { | ||||
| label: '特征预处理算法', | label: '特征预处理算法', | ||||
| value: info.include_feature_preprocessor, | value: info.include_feature_preprocessor, | ||||
| format: formatAlgorithm(featureAlgorithms), | |||||
| }, | }, | ||||
| { | { | ||||
| label: '排除的特征预处理算法', | label: '排除的特征预处理算法', | ||||
| value: info.exclude_feature_preprocessor, | value: info.exclude_feature_preprocessor, | ||||
| format: formatAlgorithm(featureAlgorithms), | |||||
| }, | }, | ||||
| { | { | ||||
| label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | ||||
| @@ -106,6 +135,11 @@ function AutoMLBasic({ | |||||
| info.task_type === AutoMLTaskType.Regression | info.task_type === AutoMLTaskType.Regression | ||||
| ? info.include_regressor | ? info.include_regressor | ||||
| : info.include_classifier, | : info.include_classifier, | ||||
| format: formatAlgorithm( | |||||
| info.task_type === AutoMLTaskType.Regression | |||||
| ? regressorAlgorithms | |||||
| : classificationAlgorithms, | |||||
| ), | |||||
| }, | }, | ||||
| { | { | ||||
| label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | ||||
| @@ -113,6 +147,11 @@ function AutoMLBasic({ | |||||
| info.task_type === AutoMLTaskType.Regression | info.task_type === AutoMLTaskType.Regression | ||||
| ? info.exclude_regressor | ? info.exclude_regressor | ||||
| : info.exclude_classifier, | : info.exclude_classifier, | ||||
| format: formatAlgorithm( | |||||
| info.task_type === AutoMLTaskType.Regression | |||||
| ? regressorAlgorithms | |||||
| : classificationAlgorithms, | |||||
| ), | |||||
| }, | }, | ||||
| { | { | ||||
| label: '集成方式', | label: '集成方式', | ||||
| @@ -292,12 +331,8 @@ function AutoMLBasic({ | |||||
| return ( | return ( | ||||
| <div className={classNames(styles['auto-ml-basic'], className)}> | <div className={classNames(styles['auto-ml-basic'], className)}> | ||||
| {isInstance && runStatus && ( | |||||
| <ExperimentRunBasic | |||||
| create_time={info?.create_time} | |||||
| runStatus={runStatus} | |||||
| instanceStatus={instanceStatus} | |||||
| /> | |||||
| {isInstance && workflowStatus && ( | |||||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||||
| )} | )} | ||||
| {!isInstance && ( | {!isInstance && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| @@ -8,69 +8,7 @@ import { | |||||
| autoMLTaskTypeOptions, | autoMLTaskTypeOptions, | ||||
| } from '@/enums'; | } from '@/enums'; | ||||
| import { Col, Form, InputNumber, Radio, Row, Select, Switch } from 'antd'; | import { Col, Form, InputNumber, Radio, Row, Select, Switch } from 'antd'; | ||||
| // 分类算法 | |||||
| const classificationAlgorithms = [ | |||||
| 'adaboost', | |||||
| 'bernoulli_nb', | |||||
| 'decision_tree', | |||||
| 'extra_trees', | |||||
| 'gaussian_nb', | |||||
| 'gradient_boosting', | |||||
| 'k_nearest_neighbors', | |||||
| 'lda', | |||||
| 'liblinear_svc', | |||||
| 'libsvm_svc', | |||||
| 'mlp', | |||||
| 'multinomial_nb', | |||||
| 'passive_aggressive', | |||||
| 'qda', | |||||
| 'random_forest', | |||||
| 'sgd', | |||||
| 'LightGBMClassification', | |||||
| 'XGBoostClassification', | |||||
| 'StackingClassification', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| // 回归算法 | |||||
| const regressorAlgorithms = [ | |||||
| 'adaboost', | |||||
| 'ard_regression', | |||||
| 'decision_tree', | |||||
| 'extra_trees', | |||||
| 'gaussian_process', | |||||
| 'gradient_boosting', | |||||
| 'k_nearest_neighbors', | |||||
| 'liblinear_svr', | |||||
| 'libsvm_svr', | |||||
| 'mlp', | |||||
| 'random_forest', | |||||
| 'sgd', | |||||
| 'LightGBMRegression', | |||||
| 'XGBoostRegression', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| // 特征预处理算法 | |||||
| const featureAlgorithms = [ | |||||
| 'densifier', | |||||
| 'extra_trees_preproc_for_classification', | |||||
| 'extra_trees_preproc_for_regression', | |||||
| 'fast_ica', | |||||
| 'feature_agglomeration', | |||||
| 'kernel_pca', | |||||
| 'kitchen_sinks', | |||||
| 'liblinear_svc_preprocessor', | |||||
| 'no_preprocessing', | |||||
| 'nystroem_sampler', | |||||
| 'pca', | |||||
| 'polynomial', | |||||
| 'random_trees_embedding', | |||||
| 'select_percentile_classification', | |||||
| 'select_percentile_regression', | |||||
| 'select_rates_classification', | |||||
| 'select_rates_regression', | |||||
| 'truncatedSVD', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| import { classificationAlgorithms, featureAlgorithms, regressorAlgorithms } from './utils'; | |||||
| // 分类指标 | // 分类指标 | ||||
| export const classificationMetrics = [ | export const classificationMetrics = [ | ||||
| @@ -280,9 +218,9 @@ function ExecuteConfig() { | |||||
| <Form.Item | <Form.Item | ||||
| label="集成模型数量" | label="集成模型数量" | ||||
| name="ensemble_size" | name="ensemble_size" | ||||
| tooltip="集成模型数量,如果设置为0,则没有集成。默认50" | |||||
| tooltip="集成模型数量,必须是大于等于1的整数,默认50" | |||||
| > | > | ||||
| <InputNumber placeholder="请输入集成模型数量" min={0} precision={0} /> | |||||
| <InputNumber placeholder="请输入集成模型数量" min={1} precision={0} /> | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| @@ -292,7 +230,7 @@ function ExecuteConfig() { | |||||
| <Form.Item | <Form.Item | ||||
| label="集成最佳模型数量" | label="集成最佳模型数量" | ||||
| name="ensemble_nbest" | name="ensemble_nbest" | ||||
| tooltip="仅集成最佳的N个模型" | |||||
| tooltip="仅集成最佳的N个模型,必须是大于等于1的整数" | |||||
| > | > | ||||
| <InputNumber placeholder="请输入集成最佳模型数量" min={1} precision={0} /> | <InputNumber placeholder="请输入集成最佳模型数量" min={1} precision={0} /> | ||||
| </Form.Item> | </Form.Item> | ||||
| @@ -419,6 +357,7 @@ function ExecuteConfig() { | |||||
| <Form.Item | <Form.Item | ||||
| label="交叉验证折数" | label="交叉验证折数" | ||||
| name="folds" | name="folds" | ||||
| tooltip="交叉验证折数必须是大于等于2的整数" | |||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | required: true, | ||||
| @@ -426,7 +365,7 @@ function ExecuteConfig() { | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <InputNumber placeholder="请输入交叉验证折数" min={1} precision={0} /> | |||||
| <InputNumber placeholder="请输入交叉验证折数" min={2} precision={0} /> | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| @@ -0,0 +1,85 @@ | |||||
| // 分类算法 | |||||
| export const classificationAlgorithms = [ | |||||
| { label: 'adaboost (自适应提升算法)', value: 'adaboost' }, | |||||
| { label: 'bernoulli_nb (伯努利朴素贝叶斯)', value: 'bernoulli_nb' }, | |||||
| { label: 'decision_tree (决策树)', value: 'decision_tree' }, | |||||
| { label: 'extra_trees (极端随机树)', value: 'extra_trees' }, | |||||
| { label: 'gaussian_nb (高斯朴素贝叶斯)', value: 'gaussian_nb' }, | |||||
| { label: 'gradient_boosting (梯度提升)', value: 'gradient_boosting' }, | |||||
| { label: 'k_nearest_neighbors (k近邻)', value: 'k_nearest_neighbors' }, | |||||
| { label: 'lda (线性判别分析)', value: 'lda' }, | |||||
| { label: 'liblinear_svc (liblinear支持向量分类)', value: 'liblinear_svc' }, | |||||
| { label: 'libsvm_svc (libsvm支持向量分类)', value: 'libsvm_svc' }, | |||||
| { label: 'mlp (多层感知器)', value: 'mlp' }, | |||||
| { label: 'multinomial_nb (多项式朴素贝叶斯)', value: 'multinomial_nb' }, | |||||
| { label: 'passive_aggressive (被动攻击算法)', value: 'passive_aggressive' }, | |||||
| { label: 'qda (二次判别式分析)', value: 'qda' }, | |||||
| { label: 'random_forest (随机森林)', value: 'random_forest' }, | |||||
| { label: 'sgd (随机梯度下降)', value: 'sgd' }, | |||||
| { label: 'tablenet (表格网络)', value: 'tablenet' }, | |||||
| { label: 'LightGBMClassification (轻量梯度提升机分类)', value: 'LightGBMClassification' }, | |||||
| { label: 'XGBoostClassification (极端梯度提升机分类)', value: 'XGBoostClassification' }, | |||||
| { label: 'StackingClassification (堆叠泛化)', value: 'StackingClassification' }, | |||||
| ]; | |||||
| // 回归算法 | |||||
| export const regressorAlgorithms = [ | |||||
| { label: 'adaboost (自适应提升算法)', value: 'adaboost' }, | |||||
| { label: 'ard_regression (自动相关性确定回归)', value: 'ard_regression' }, | |||||
| { label: 'decision_tree (决策树)', value: 'decision_tree' }, | |||||
| { label: 'extra_trees (极端随机树)', value: 'extra_trees' }, | |||||
| { label: 'gaussian_process (高斯过程回归)', value: 'gaussian_process' }, | |||||
| { label: 'gradient_boosting (梯度提升)', value: 'gradient_boosting' }, | |||||
| { label: 'k_nearest_neighbors (梯度提升)', value: 'k_nearest_neighbors' }, | |||||
| { label: 'liblinear_svr (liblinear支持向量回归)', value: 'liblinear_svr' }, | |||||
| { label: 'libsvm_svr (libsvm支持向量回归)', value: 'libsvm_svr' }, | |||||
| { label: 'mlp (多层感知器)', value: 'mlp' }, | |||||
| { label: 'random_forest (随机森林)', value: 'random_forest' }, | |||||
| { label: 'sgd (随机梯度下降)', value: 'sgd' }, | |||||
| { label: 'LightGBMRegression (轻量梯度提升机回归)', value: 'LightGBMRegression' }, | |||||
| { label: 'XGBoostRegression (极端梯度提升机回归)', value: 'XGBoostRegression' }, | |||||
| ]; | |||||
| // 特征预处理算法 | |||||
| export const featureAlgorithms = [ | |||||
| { label: 'densifier (特征变换-数据增稠)', value: 'densifier' }, | |||||
| { | |||||
| label: 'extra_trees_preproc_for_classification (特征选择-分类任务极端随机树)', | |||||
| value: 'extra_trees_preproc_for_classification', | |||||
| }, | |||||
| { | |||||
| label: 'extra_trees_preproc_for_regression (特征选择-回归任务极端随机树)', | |||||
| value: 'extra_trees_preproc_for_regression', | |||||
| }, | |||||
| { label: 'fast_ica (特征选择-快速独立成分分析)', value: 'fast_ica' }, | |||||
| { label: 'feature_agglomeration (特征变换-特征聚合)', value: 'feature_agglomeration' }, | |||||
| { label: 'kernel_pca (特征选择-核主成分分析)', value: 'kernel_pca' }, | |||||
| { label: 'kitchen_sinks (特征变换-随机特征映射)', value: 'kitchen_sinks' }, | |||||
| { | |||||
| label: 'liblinear_svc_preprocessor (特征选择-线性svc预处理器)', | |||||
| value: 'liblinear_svc_preprocessor', | |||||
| }, | |||||
| { label: 'miss_value_impute (缺失值填充)', value: 'miss_value_impute' }, | |||||
| { label: 'no_preprocessing (无预处理)', value: 'no_preprocessing' }, | |||||
| { label: 'nystroem_sampler (特征变换-尼斯特罗姆采样器)', value: 'nystroem_sampler' }, | |||||
| { label: 'pca (特征选择-主成分分析)', value: 'pca' }, | |||||
| { label: 'polynomial (特征变换-多项式特征扩展)', value: 'polynomial' }, | |||||
| { label: 'random_trees_embedding (特征变换-随机森林特征嵌入)', value: 'random_trees_embedding' }, | |||||
| { | |||||
| label: 'select_percentile_classification 特征选择-基于百分位的分类特征选择)', | |||||
| value: 'select_percentile_classification', | |||||
| }, | |||||
| { | |||||
| label: 'select_percentile_regression (特征选择-基于百分位的回归特征选择)', | |||||
| value: 'select_percentile_regression', | |||||
| }, | |||||
| { | |||||
| label: 'select_rates_classification (特征选择-基于比率的分类特征选择)', | |||||
| value: 'select_rates_classification', | |||||
| }, | |||||
| { | |||||
| label: 'select_rates_regression (特征选择-基于比率的回归特征选择)', | |||||
| value: 'select_rates_regression', | |||||
| }, | |||||
| { label: 'truncatedSVD (特征变换-截断奇异值分解)', value: 'truncatedSVD' }, | |||||
| ]; | |||||
| @@ -54,10 +54,6 @@ | |||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| width: 200px; | width: 200px; | ||||
| .statusIcon { | |||||
| visibility: visible; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -34,7 +34,14 @@ function ExperimentInstanceList({ | |||||
| }: ExperimentInstanceListProps) { | }: ExperimentInstanceListProps) { | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const allIntanceIds = useMemo(() => { | const allIntanceIds = useMemo(() => { | ||||
| return experimentInsList?.map((item) => item.id) || []; | |||||
| return ( | |||||
| experimentInsList | |||||
| ?.filter( | |||||
| (item) => | |||||
| item.status !== ExperimentStatus.Running && item.status !== ExperimentStatus.Pending, | |||||
| ) | |||||
| .map((item) => item.id) || [] | |||||
| ); | |||||
| }, [experimentInsList]); | }, [experimentInsList]); | ||||
| const [ | const [ | ||||
| selectedIns, | selectedIns, | ||||
| @@ -126,7 +133,12 @@ function ExperimentInstanceList({ | |||||
| <div> | <div> | ||||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | ||||
| <div className={styles.check}> | <div className={styles.check}> | ||||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||||
| <Checkbox | |||||
| checked={checked} | |||||
| indeterminate={indeterminate} | |||||
| disabled={allIntanceIds.length === 0} | |||||
| onChange={checkAll} | |||||
| ></Checkbox> | |||||
| </div> | </div> | ||||
| <div className={styles.index}>序号</div> | <div className={styles.index}>序号</div> | ||||
| <div className={styles.description}>运行时长</div> | <div className={styles.description}>运行时长</div> | ||||
| @@ -171,12 +183,8 @@ function ExperimentInstanceList({ | |||||
| {index + 1} | {index + 1} | ||||
| </a> | </a> | ||||
| <ExperimentInstanceComponent | <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} | |||||
| experimentId={item[config['idInsProperty'] as keyof ExperimentInstance] as number} | |||||
| instance={item} | |||||
| ></ExperimentInstanceComponent> | ></ExperimentInstanceComponent> | ||||
| <div className={styles.operation}> | <div className={styles.operation}> | ||||
| <Button | <Button | ||||
| @@ -2,67 +2,70 @@ import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { ExperimentInstance, NodeStatus } from '@/types'; | |||||
| import { ExperimentCompleted } from '@/utils/constant'; | import { ExperimentCompleted } from '@/utils/constant'; | ||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { getWorkflowStatus } from '@/utils/experiment'; | |||||
| import { Typography } from 'antd'; | import { Typography } from 'antd'; | ||||
| import React, { useCallback } from 'react'; | import React, { useCallback } from 'react'; | ||||
| import styles from './index.less'; | 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; | |||||
| type ExperimentInstanceComponentProps = { | |||||
| experimentId: number; | |||||
| instance: ExperimentInstance; | |||||
| }; | }; | ||||
| function ExperimentInstance({ | |||||
| create_time, | |||||
| finish_time, | |||||
| status, | |||||
| argo_ins_name, | |||||
| argo_ins_ns, | |||||
| experimentInsId, | |||||
| }: ExperimentInstanceProps) { | |||||
| function ExperimentInstanceComponent({ experimentId, instance }: ExperimentInstanceComponentProps) { | |||||
| const { id, argo_ins_name, argo_ins_ns, node_status } = instance; | |||||
| const workflowStatus = getWorkflowStatus(node_status) as NodeStatus | undefined; | |||||
| const status = instance.status as ExperimentStatus; | |||||
| const createTime = workflowStatus?.startedAt; | |||||
| const finishTime = workflowStatus?.finishedAt; | |||||
| const statusInfo = experimentStatusInfo[status]; | |||||
| const handleSSEMessage: MessageHandler = useCallback( | const handleSSEMessage: MessageHandler = useCallback( | ||||
| (experimentInsId: number, status: string, finish_time: string) => { | |||||
| (experimentId: number, experimentInsId: number, status: string, finishTime: string) => { | |||||
| window.postMessage({ | window.postMessage({ | ||||
| type: ExperimentCompleted, | type: ExperimentCompleted, | ||||
| payload: { | payload: { | ||||
| id: experimentInsId, | |||||
| experimentId, | |||||
| experimentInsId, | |||||
| status, | status, | ||||
| finish_time, | |||||
| finishTime, | |||||
| }, | }, | ||||
| }); | }); | ||||
| }, | }, | ||||
| [], | [], | ||||
| ); | ); | ||||
| useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||||
| useSSE(experimentId, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||||
| return ( | return ( | ||||
| <React.Fragment> | <React.Fragment> | ||||
| <div className={styles.description}> | <div className={styles.description}> | ||||
| <RunDuration createTime={create_time} finishTime={finish_time} /> | |||||
| <RunDuration createTime={createTime} finishTime={finishTime} /> | |||||
| </div> | </div> | ||||
| <div className={styles.startTime}> | <div className={styles.startTime}> | ||||
| <Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}> | |||||
| {formatDate(create_time)} | |||||
| <Typography.Text ellipsis={{ tooltip: formatDate(createTime) }}> | |||||
| {formatDate(createTime)} | |||||
| </Typography.Text> | </Typography.Text> | ||||
| </div> | </div> | ||||
| <div className={styles.statusBox}> | <div className={styles.statusBox}> | ||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[status]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}> | |||||
| {experimentStatusInfo[status]?.label} | |||||
| </span> | |||||
| {statusInfo ? ( | |||||
| <> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={statusInfo.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||||
| </> | |||||
| ) : ( | |||||
| '--' | |||||
| )} | |||||
| </div> | </div> | ||||
| </React.Fragment> | </React.Fragment> | ||||
| ); | ); | ||||
| } | } | ||||
| export default ExperimentInstance; | |||||
| export default ExperimentInstanceComponent; | |||||
| @@ -8,6 +8,7 @@ import { | |||||
| batchDeleteActiveLearnInsReq, | batchDeleteActiveLearnInsReq, | ||||
| deleteActiveLearnInsReq, | deleteActiveLearnInsReq, | ||||
| deleteActiveLearnReq, | deleteActiveLearnReq, | ||||
| editActiveLearnInsReq, | |||||
| getActiveLearnInsListReq, | getActiveLearnInsListReq, | ||||
| getActiveLearnListReq, | getActiveLearnListReq, | ||||
| runActiveLearnReq, | runActiveLearnReq, | ||||
| @@ -17,6 +18,7 @@ import { | |||||
| batchDeleteExperimentInsReq, | batchDeleteExperimentInsReq, | ||||
| deleteAutoMLReq, | deleteAutoMLReq, | ||||
| deleteExperimentInsReq, | deleteExperimentInsReq, | ||||
| editExperimentInsReq, | |||||
| getAutoMLListReq, | getAutoMLListReq, | ||||
| getExperimentInsListReq, | getExperimentInsListReq, | ||||
| runAutoMLReq, | runAutoMLReq, | ||||
| @@ -26,6 +28,7 @@ import { | |||||
| batchDeleteRayInsReq, | batchDeleteRayInsReq, | ||||
| deleteRayInsReq, | deleteRayInsReq, | ||||
| deleteRayReq, | deleteRayReq, | ||||
| editRayInsReq, | |||||
| getRayInsListReq, | getRayInsListReq, | ||||
| getRayListReq, | getRayListReq, | ||||
| runRayReq, | runRayReq, | ||||
| @@ -39,18 +42,20 @@ export enum ExperimentListType { | |||||
| } | } | ||||
| type ExperimentListInfo = { | type ExperimentListInfo = { | ||||
| getListReq: (params: any) => Promise<any>; // 获取列表 | |||||
| getInsListReq: (params: any) => Promise<any>; // 获取实例列表 | |||||
| getListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取列表 | |||||
| getInsListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取实例列表 | |||||
| deleteRecordReq: (params: any) => Promise<any>; // 删除 | deleteRecordReq: (params: any) => Promise<any>; // 删除 | ||||
| runRecordReq: (params: any) => Promise<any>; // 运行 | runRecordReq: (params: any) => Promise<any>; // 运行 | ||||
| deleteInsReq: (params: any) => Promise<any>; // 删除实例 | deleteInsReq: (params: any) => Promise<any>; // 删除实例 | ||||
| batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 | batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 | ||||
| stopInsReq: (params: any) => Promise<any>; // 终止实例 | stopInsReq: (params: any) => Promise<any>; // 终止实例 | ||||
| editInsReq: (params: any) => Promise<any>; // 编辑实例 | |||||
| title: string; // 标题 | title: string; // 标题 | ||||
| pathPrefix: string; // 路由路径前缀 | pathPrefix: string; // 路由路径前缀 | ||||
| idProperty: string; // ID属性 | idProperty: string; // ID属性 | ||||
| nameProperty: string; // 名称属性 | nameProperty: string; // 名称属性 | ||||
| descProperty: string; // 描述属性 | descProperty: string; // 描述属性 | ||||
| idInsProperty: string; // 实例返回的ID属性 | |||||
| }; | }; | ||||
| export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = { | export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = { | ||||
| @@ -62,11 +67,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||||
| deleteInsReq: deleteExperimentInsReq, | deleteInsReq: deleteExperimentInsReq, | ||||
| batchDeleteInsReq: batchDeleteExperimentInsReq, | batchDeleteInsReq: batchDeleteExperimentInsReq, | ||||
| stopInsReq: stopExperimentInsReq, | stopInsReq: stopExperimentInsReq, | ||||
| title: '自主机器学习', | |||||
| editInsReq: editExperimentInsReq, | |||||
| title: '自动机器学习', | |||||
| pathPrefix: 'automl', | pathPrefix: 'automl', | ||||
| nameProperty: 'name', | nameProperty: 'name', | ||||
| descProperty: 'description', | descProperty: 'description', | ||||
| idProperty: 'machineLearnId', | idProperty: 'machineLearnId', | ||||
| idInsProperty: 'machine_learn_id', | |||||
| }, | }, | ||||
| [ExperimentListType.HyperParameter]: { | [ExperimentListType.HyperParameter]: { | ||||
| getListReq: getRayListReq, | getListReq: getRayListReq, | ||||
| @@ -76,11 +83,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||||
| deleteInsReq: deleteRayInsReq, | deleteInsReq: deleteRayInsReq, | ||||
| batchDeleteInsReq: batchDeleteRayInsReq, | batchDeleteInsReq: batchDeleteRayInsReq, | ||||
| stopInsReq: stopRayInsReq, | stopInsReq: stopRayInsReq, | ||||
| editInsReq: editRayInsReq, | |||||
| title: '超参数自动寻优', | title: '超参数自动寻优', | ||||
| pathPrefix: 'hyperparameter', | pathPrefix: 'hyperparameter', | ||||
| nameProperty: 'name', | nameProperty: 'name', | ||||
| descProperty: 'description', | descProperty: 'description', | ||||
| idProperty: 'rayId', | idProperty: 'rayId', | ||||
| idInsProperty: 'ray_id', | |||||
| }, | }, | ||||
| [ExperimentListType.ActiveLearn]: { | [ExperimentListType.ActiveLearn]: { | ||||
| getListReq: getActiveLearnListReq, | getListReq: getActiveLearnListReq, | ||||
| @@ -90,10 +99,12 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||||
| deleteInsReq: deleteActiveLearnInsReq, | deleteInsReq: deleteActiveLearnInsReq, | ||||
| batchDeleteInsReq: batchDeleteActiveLearnInsReq, | batchDeleteInsReq: batchDeleteActiveLearnInsReq, | ||||
| stopInsReq: stopActiveLearnInsReq, | stopInsReq: stopActiveLearnInsReq, | ||||
| editInsReq: editActiveLearnInsReq, | |||||
| title: '自动学习', | title: '自动学习', | ||||
| pathPrefix: 'active-learn', | pathPrefix: 'active-learn', | ||||
| nameProperty: 'name', | nameProperty: 'name', | ||||
| descProperty: 'description', | descProperty: 'description', | ||||
| idProperty: 'activeLearnId', | idProperty: 'activeLearnId', | ||||
| idInsProperty: 'active_learn_id', | |||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -30,7 +30,7 @@ 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, useRef, useState } from 'react'; | |||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| import ExperimentInstanceList from '../ExperimentInstanceList'; | import ExperimentInstanceList from '../ExperimentInstanceList'; | ||||
| import { ExperimentListType, experimentListConfig } from './config'; | import { ExperimentListType, experimentListConfig } from './config'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -52,7 +52,6 @@ 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, | ||||
| @@ -60,38 +59,37 @@ 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 params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| [config.nameProperty]: searchText || undefined, | |||||
| }; | |||||
| const request = config.getListReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }, [pagination, searchText, config]); | |||||
| const [now] = useServerTime(); | |||||
| useEffect(() => { | |||||
| getAutoMLList(); | |||||
| }, [getAutoMLList]); | |||||
| // 获取实验列表 | |||||
| const getExperimentList = useCallback( | |||||
| async (skipLoading: boolean = false) => { | |||||
| const params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| [config.nameProperty]: searchText || undefined, | |||||
| }; | |||||
| const request = config.getListReq; | |||||
| const [res] = await to(request(params, skipLoading)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }, | |||||
| [pagination, searchText, config], | |||||
| ); | |||||
| // 获取实验实例列表 | // 获取实验实例列表 | ||||
| const getExperimentInsList = useCallback( | const getExperimentInsList = useCallback( | ||||
| async (recordId: number, page: number, size: number) => { | |||||
| async (recordId: number, page: number, size: number, skipLoading: boolean = false) => { | |||||
| const params = { | const params = { | ||||
| [config.idProperty]: recordId, | [config.idProperty]: recordId, | ||||
| page: page, | page: page, | ||||
| size: size, | size: size, | ||||
| }; | }; | ||||
| const request = config.getInsListReq; | const request = config.getInsListReq; | ||||
| const [res] = await to(request(params)); | |||||
| const [res] = await to(request(params, skipLoading)); | |||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const { content = [], totalElements = 0 } = res.data; | const { content = [], totalElements = 0 } = res.data; | ||||
| try { | try { | ||||
| @@ -111,59 +109,115 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| // 刷新实验列表状态, | // 刷新实验列表状态, | ||||
| // TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态 | // TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态 | ||||
| const refreshExperimentList = useCallback(() => { | |||||
| getAutoMLList(); | |||||
| }, [getAutoMLList]); | |||||
| const refreshExperimentList = useCallback( | |||||
| (skipLoading: boolean = false) => { | |||||
| getExperimentList(skipLoading); | |||||
| }, | |||||
| [getExperimentList], | |||||
| ); | |||||
| // 刷新实验实例列表 | // 刷新实验实例列表 | ||||
| const refreshExperimentIns = useCallback( | const refreshExperimentIns = useCallback( | ||||
| (experimentId: number) => { | |||||
| (experimentId: number, skipLoading: boolean = false) => { | |||||
| const length = experimentInsList.length; | const length = experimentInsList.length; | ||||
| getExperimentInsList(experimentId, 0, length); | |||||
| getExperimentInsList(experimentId, 0, length, skipLoading); | |||||
| }, | }, | ||||
| [getExperimentInsList, experimentInsList], | [getExperimentInsList, experimentInsList], | ||||
| ); | ); | ||||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||||
| // 更新实验实例状态 | |||||
| const editExperimentIns = useCallback( | |||||
| async ( | |||||
| experimentId: number, | |||||
| experimentInsId: number, | |||||
| status: ExperimentStatus, | |||||
| argo_ins_name: string, | |||||
| argo_ins_ns: string, | |||||
| ) => { | |||||
| const params = { | |||||
| [config.idInsProperty]: experimentId, | |||||
| id: experimentInsId, | |||||
| status: status, | |||||
| argo_ins_name, | |||||
| argo_ins_ns, | |||||
| }; | |||||
| const request = config.editInsReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| refreshExperimentIns(experimentId, true); | |||||
| refreshExperimentList(true); | |||||
| } | |||||
| }, | |||||
| [config, refreshExperimentIns, refreshExperimentList], | |||||
| ); | |||||
| // 获取实验列表 | |||||
| useEffect(() => { | |||||
| getExperimentList(); | |||||
| }, [getExperimentList]); | |||||
| // expandedRowKeys 变化 | |||||
| useEffect(() => { | |||||
| if (expandedRowKeys.length > 0) { | |||||
| getExperimentInsList(expandedRowKeys[0], 0, 5); | |||||
| refreshExperimentList(); | |||||
| } | |||||
| }, [expandedRowKeys, getExperimentInsList, refreshExperimentList]); | |||||
| // 实验实例状态变化 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const handleMessage = (e: MessageEvent) => { | const handleMessage = (e: MessageEvent) => { | ||||
| const { type, payload } = e.data; | const { type, payload } = e.data; | ||||
| if (type === ExperimentCompleted) { | if (type === ExperimentCompleted) { | ||||
| const { id, status, finish_time } = payload; | |||||
| // 修改实例的状态和结束时间 | |||||
| setExperimentInsList((prev) => | |||||
| prev.map((v) => | |||||
| v.id === id | |||||
| ? { | |||||
| ...v, | |||||
| status: status, | |||||
| finish_time: finish_time, | |||||
| } | |||||
| : v, | |||||
| ), | |||||
| const { experimentId, experimentInsId, status, finishTime } = payload; | |||||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||||
| console.log( | |||||
| '实验实例状态变化', | |||||
| currentIns?.status, | |||||
| status, | |||||
| experimentId, | |||||
| experimentInsId, | |||||
| finishTime, | |||||
| ); | ); | ||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| if ( | |||||
| !currentIns || | |||||
| currentIns.status === ExperimentStatus.Terminated || | |||||
| currentIns.status === status | |||||
| ) { | |||||
| return; | |||||
| } | } | ||||
| timerRef.current = setTimeout(() => { | |||||
| refreshExperimentList(); | |||||
| }, 10000); | |||||
| // refreshExperimentList(true); | |||||
| // refreshExperimentIns(experimentId); | |||||
| editExperimentIns( | |||||
| experimentId, | |||||
| experimentInsId, | |||||
| status, | |||||
| currentIns.argo_ins_name, | |||||
| currentIns.argo_ins_ns, | |||||
| ); | |||||
| // 修改实例的状态和结束时间 | |||||
| // setExperimentInsList((prev) => | |||||
| // prev.map((v) => | |||||
| // v.id === experimentInsId | |||||
| // ? { | |||||
| // ...v, | |||||
| // status: status, | |||||
| // finish_time: finishTime, | |||||
| // } | |||||
| // : v, | |||||
| // ), | |||||
| // ); | |||||
| } | } | ||||
| }; | }; | ||||
| window.addEventListener('message', handleMessage); | window.addEventListener('message', handleMessage); | ||||
| return () => { | return () => { | ||||
| window.removeEventListener('message', handleMessage); | window.removeEventListener('message', handleMessage); | ||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| } | |||||
| }; | }; | ||||
| }, [refreshExperimentList]); | |||||
| }, [experimentInsList, editExperimentIns]); | |||||
| // 搜索 | // 搜索 | ||||
| const onSearch: SearchProps['onSearch'] = (value) => { | const onSearch: SearchProps['onSearch'] = (value) => { | ||||
| @@ -207,6 +261,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| setCacheState({ | setCacheState({ | ||||
| pagination, | pagination, | ||||
| searchText, | searchText, | ||||
| expandedRowKeys, | |||||
| }); | }); | ||||
| if (record) { | if (record) { | ||||
| @@ -225,6 +280,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| setCacheState({ | setCacheState({ | ||||
| pagination, | pagination, | ||||
| searchText, | searchText, | ||||
| expandedRowKeys, | |||||
| }); | }); | ||||
| navigate(`info/${record.id}`); | navigate(`info/${record.id}`); | ||||
| @@ -237,8 +293,8 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| if (res) { | if (res) { | ||||
| message.success('运行成功'); | message.success('运行成功'); | ||||
| setExpandedRowKeys([record.id]); | setExpandedRowKeys([record.id]); | ||||
| refreshExperimentList(); | |||||
| getExperimentInsList(record.id, 0, 5); | |||||
| // getExperimentInsList(record.id, 0, 5); | |||||
| // refreshExperimentList(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -248,8 +304,8 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| setExperimentInsList([]); | setExperimentInsList([]); | ||||
| if (expanded) { | if (expanded) { | ||||
| setExpandedRowKeys([record.id]); | setExpandedRowKeys([record.id]); | ||||
| getExperimentInsList(record.id, 0, 5); | |||||
| refreshExperimentList(); | |||||
| // getExperimentInsList(record.id, 0, 5); | |||||
| // refreshExperimentList(); | |||||
| } else { | } else { | ||||
| setExpandedRowKeys([]); | setExpandedRowKeys([]); | ||||
| } | } | ||||
| @@ -257,6 +313,11 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| // 跳转到实验实例详情 | // 跳转到实验实例详情 | ||||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | ||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| expandedRowKeys, | |||||
| }); | |||||
| navigate(`instance/${autoML.id}/${record.id}`); | navigate(`instance/${autoML.id}/${record.id}`); | ||||
| }; | }; | ||||
| @@ -269,8 +330,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| // 实验实例终止 | // 实验实例终止 | ||||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | ||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| // 修改实例的状态和结束时间 | |||||
| setExperimentInsList((prevList) => { | setExperimentInsList((prevList) => { | ||||
| return prevList.map((item) => { | return prevList.map((item) => { | ||||
| if (item.id === experimentIns.id) { | if (item.id === experimentIns.id) { | ||||
| @@ -283,6 +343,11 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| return item; | return item; | ||||
| }); | }); | ||||
| }); | }); | ||||
| // 刷新实验列表和实例列表 | |||||
| refreshExperimentList(true); | |||||
| if (expandedRowKeys.length > 0) { | |||||
| refreshExperimentIns(expandedRowKeys[0]); | |||||
| } | |||||
| }; | }; | ||||
| // --------------------------- Table --------------------------- | // --------------------------- Table --------------------------- | ||||
| // 分页切换 | // 分页切换 | ||||
| @@ -330,7 +395,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||||
| }, | }, | ||||
| ...diffColumns, | ...diffColumns, | ||||
| { | { | ||||
| title: '创建时间', | |||||
| title: '更新时间', | |||||
| dataIndex: 'update_time', | dataIndex: 'update_time', | ||||
| key: 'update_time', | key: 'update_time', | ||||
| width: '20%', | width: '20%', | ||||
| @@ -0,0 +1,7 @@ | |||||
| import styles from './index.less'; | |||||
| function EmptyLog() { | |||||
| return <div className={styles['empty-log']}>暂无日志</div>; | |||||
| } | |||||
| export default EmptyLog; | |||||
| @@ -5,3 +5,14 @@ | |||||
| height: 100%; | height: 100%; | ||||
| } | } | ||||
| } | } | ||||
| .empty-log { | |||||
| height: 100%; | |||||
| padding: 15px; | |||||
| color: white; | |||||
| font-size: 14px; | |||||
| white-space: pre-line; | |||||
| text-align: center; | |||||
| word-break: break-all; | |||||
| background: #19253b; | |||||
| } | |||||
| @@ -2,6 +2,7 @@ import { ExperimentStatus } from '@/enums'; | |||||
| import { AutoMLInstanceData } from '@/pages/AutoML/types'; | import { AutoMLInstanceData } from '@/pages/AutoML/types'; | ||||
| import LogList from '@/pages/Experiment/components/LogList'; | import LogList from '@/pages/Experiment/components/LogList'; | ||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| import EmptyLog from './empty'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const NodePrefix = 'auto-ml'; | const NodePrefix = 'auto-ml'; | ||||
| @@ -19,7 +20,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['experiment-log']}> | <div className={styles['experiment-log']}> | ||||
| <div className={styles['experiment-log__log']}> | <div className={styles['experiment-log__log']}> | ||||
| {nodeStatus && ( | |||||
| {nodeStatus ? ( | |||||
| <LogList | <LogList | ||||
| instanceName={instanceInfo.argo_ins_name} | instanceName={instanceInfo.argo_ins_name} | ||||
| instanceNamespace={instanceInfo.argo_ins_ns} | instanceNamespace={instanceInfo.argo_ins_ns} | ||||
| @@ -28,6 +29,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| instanceNodeStartTime={nodeStatus.startedAt} | instanceNodeStartTime={nodeStatus.startedAt} | ||||
| instanceNodeStatus={nodeStatus.phase as ExperimentStatus} | instanceNodeStatus={nodeStatus.phase as ExperimentStatus} | ||||
| ></LogList> | ></LogList> | ||||
| ) : ( | |||||
| <EmptyLog /> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -3,59 +3,61 @@ 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 { type NodeStatus } from '@/types'; | import { type NodeStatus } from '@/types'; | ||||
| import { getExperimentInstanceStatus } from '@/utils/experiment'; | |||||
| import { formatDate } from '@/utils/format'; | import { formatDate } from '@/utils/format'; | ||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| type ExperimentRunBasicProps = { | type ExperimentRunBasicProps = { | ||||
| create_time?: string; | |||||
| runStatus?: NodeStatus; | |||||
| workflowStatus?: NodeStatus; | |||||
| instanceStatus?: ExperimentStatus; | instanceStatus?: ExperimentStatus; | ||||
| }; | }; | ||||
| function ExperimentRunBasic({ create_time, runStatus, instanceStatus }: ExperimentRunBasicProps) { | |||||
| function ExperimentRunBasic({ workflowStatus, instanceStatus }: ExperimentRunBasicProps) { | |||||
| const instanceDatas = useMemo(() => { | const instanceDatas = useMemo(() => { | ||||
| if (!runStatus) { | |||||
| return []; | |||||
| } | |||||
| const status = | |||||
| instanceStatus === ExperimentStatus.Terminated ? instanceStatus : runStatus.phase; | |||||
| const status = getExperimentInstanceStatus(instanceStatus as ExperimentStatus, workflowStatus); | |||||
| const statusInfo = experimentStatusInfo[status]; | const statusInfo = experimentStatusInfo[status]; | ||||
| return [ | return [ | ||||
| { | { | ||||
| label: '启动时间', | label: '启动时间', | ||||
| value: formatDate(create_time), | |||||
| value: formatDate(workflowStatus?.startedAt), | |||||
| }, | }, | ||||
| { | { | ||||
| label: '执行时长', | label: '执行时长', | ||||
| value: <RunDuration createTime={create_time} finishTime={runStatus.finishedAt} />, | |||||
| value: ( | |||||
| <RunDuration | |||||
| createTime={workflowStatus?.startedAt} | |||||
| finishTime={workflowStatus?.finishedAt} | |||||
| /> | |||||
| ), | |||||
| }, | }, | ||||
| { | { | ||||
| label: '状态', | label: '状态', | ||||
| value: ( | |||||
| value: statusInfo ? ( | |||||
| <Flex align="center"> | <Flex align="center"> | ||||
| <img | <img | ||||
| style={{ width: '17px', marginRight: '7px' }} | style={{ width: '17px', marginRight: '7px' }} | ||||
| src={statusInfo?.icon} | |||||
| src={statusInfo.icon} | |||||
| draggable={false} | draggable={false} | ||||
| alt="" | alt="" | ||||
| /> | /> | ||||
| <div | <div | ||||
| style={{ | style={{ | ||||
| color: statusInfo?.color, | |||||
| color: statusInfo.color, | |||||
| fontSize: '15px', | fontSize: '15px', | ||||
| lineHeight: 1.6, | lineHeight: 1.6, | ||||
| }} | }} | ||||
| > | > | ||||
| {statusInfo?.label} | |||||
| {statusInfo.label} | |||||
| </div> | </div> | ||||
| </Flex> | </Flex> | ||||
| ) : ( | |||||
| '--' | |||||
| ), | ), | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| }, [runStatus, create_time, instanceStatus]); | |||||
| }, [workflowStatus, instanceStatus]); | |||||
| return ( | return ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| @@ -0,0 +1,8 @@ | |||||
| .experiment-visual { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| &__empty { | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| @@ -5,10 +5,15 @@ | |||||
| */ | */ | ||||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | import IframePage, { IframePageType } from '@/components/IFramePage'; | ||||
| import { runTensorBoardReq } from '@/services/experiment/index.js'; | |||||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||||
| import KFSpin from '@/components/KFSpin'; | |||||
| import { TensorBoardStatus } from '@/enums'; | |||||
| import { getTensorBoardStatusReq, runTensorBoardReq } from '@/services/experiment/index.js'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import { LoadingOutlined } from '@ant-design/icons'; | |||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| import styles from './index.less'; | |||||
| type TensorBoardProps = { | type TensorBoardProps = { | ||||
| namespace?: string; | namespace?: string; | ||||
| @@ -16,28 +21,75 @@ type TensorBoardProps = { | |||||
| }; | }; | ||||
| function ExperimentVisualResult({ namespace, path }: TensorBoardProps) { | function ExperimentVisualResult({ namespace, path }: TensorBoardProps) { | ||||
| const [tensorboardUrl, setTensorboardUrl] = useState(''); | |||||
| useEffect(() => { | |||||
| // 运行 TensorBoard | |||||
| const runTensorBoard = async () => { | |||||
| const params = { | |||||
| namespace: namespace, | |||||
| path: path, | |||||
| }; | |||||
| const [res] = await to(runTensorBoardReq(params)); | |||||
| if (res && res.data) { | |||||
| const url = res.data; | |||||
| SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url); | |||||
| setTensorboardUrl(url); | |||||
| const [tensorboardUrl, setTensorboardUrl] = useState<string | undefined | null>(undefined); | |||||
| const [status, setStatus] = useState<TensorBoardStatus | undefined>(undefined); | |||||
| // 获取 TensorBoard 状态 | |||||
| const getTensorBoardStatus = useCallback(async () => { | |||||
| const params = { | |||||
| namespace: namespace, | |||||
| path: path, | |||||
| }; | |||||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||||
| if (res && res.data) { | |||||
| const status = res.data.status as TensorBoardStatus | undefined; | |||||
| setStatus(res.data.status); | |||||
| if (!status || status === TensorBoardStatus.Pending) { | |||||
| setTimeout(() => { | |||||
| getTensorBoardStatus(); | |||||
| }, 5000); | |||||
| } | } | ||||
| } | |||||
| }, [namespace, path]); | |||||
| // 运行 TensorBoard | |||||
| const runTensorBoard = useCallback(async () => { | |||||
| const params = { | |||||
| namespace: namespace, | |||||
| path: path, | |||||
| }; | }; | ||||
| const [res] = await to(runTensorBoardReq(params)); | |||||
| if (res && res.data) { | |||||
| const url = res.data; | |||||
| SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url); | |||||
| setTensorboardUrl(url); | |||||
| getTensorBoardStatus(); | |||||
| } else { | |||||
| setTensorboardUrl(null); | |||||
| } | |||||
| }, [namespace, path, getTensorBoardStatus]); | |||||
| useEffect(() => { | |||||
| if (namespace && path) { | if (namespace && path) { | ||||
| runTensorBoard(); | runTensorBoard(); | ||||
| } | } | ||||
| }, [namespace, path]); | |||||
| }, [namespace, path, runTensorBoard]); | |||||
| return <>{tensorboardUrl && <IframePage type={IframePageType.TensorBoard}></IframePage>}</>; | |||||
| if (tensorboardUrl === null || status === TensorBoardStatus.Failed) { | |||||
| return ( | |||||
| <div className={styles['experiment-visual']}> | |||||
| <KFEmpty | |||||
| className={styles['experiment-visual__empty']} | |||||
| type={EmptyType.NoData} | |||||
| title="运行可视化失败" | |||||
| buttonTitle="重新运行" | |||||
| onButtonClick={runTensorBoard} | |||||
| /> | |||||
| </div> | |||||
| ); | |||||
| } else if (status === TensorBoardStatus.Pending) { | |||||
| return ( | |||||
| <div className={styles['experiment-visual']}> | |||||
| <KFSpin indicator={<LoadingOutlined spin />} size="large" /> | |||||
| </div> | |||||
| ); | |||||
| } else if (status === TensorBoardStatus.Running) { | |||||
| return ( | |||||
| <div className={styles['experiment-visual']}> | |||||
| <IframePage type={IframePageType.TensorBoard}></IframePage> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| } | } | ||||
| export default ExperimentVisualResult; | export default ExperimentVisualResult; | ||||
| @@ -118,12 +118,14 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||||
| }, | }, | ||||
| { | { | ||||
| pattern: /^[a-zA-Z0-9._-]+$/, | pattern: /^[a-zA-Z0-9._-]+$/, | ||||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||||
| message: '数据集版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||||
| }, | }, | ||||
| { | { | ||||
| validator: (_rule, value) => { | validator: (_rule, value) => { | ||||
| if (value === 'master') { | if (value === 'master') { | ||||
| return Promise.reject(`版本不能为 master`); | |||||
| return Promise.reject(`数据集版本不能为 master`); | |||||
| } else if (value === 'origin') { | |||||
| return Promise.reject(`数据集版本不能为 origin`); | |||||
| } | } | ||||
| return Promise.resolve(); | return Promise.resolve(); | ||||
| }, | }, | ||||
| @@ -109,12 +109,14 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||||
| }, | }, | ||||
| { | { | ||||
| pattern: /^[a-zA-Z0-9._-]+$/, | pattern: /^[a-zA-Z0-9._-]+$/, | ||||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||||
| message: '模型版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||||
| }, | }, | ||||
| { | { | ||||
| validator: (_rule, value) => { | validator: (_rule, value) => { | ||||
| if (value === 'master') { | if (value === 'master') { | ||||
| return Promise.reject(`版本不能为 master`); | |||||
| return Promise.reject(`模型版本不能为 master`); | |||||
| } else if (value === 'origin') { | |||||
| return Promise.reject(`模型版本不能为 origin`); | |||||
| } | } | ||||
| return Promise.resolve(); | return Promise.resolve(); | ||||
| }, | }, | ||||
| @@ -126,7 +128,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||||
| <Form.Item label="模型框架" name="model_type"> | <Form.Item label="模型框架" name="model_type"> | ||||
| <Select | <Select | ||||
| allowClear | allowClear | ||||
| placeholder="请选择模型类型" | |||||
| placeholder="请选择模型框架" | |||||
| options={typeList} | options={typeList} | ||||
| fieldNames={{ label: 'name', value: 'name' }} | fieldNames={{ label: 'name', value: 'name' }} | ||||
| optionFilterProp="name" | optionFilterProp="name" | ||||
| @@ -136,7 +138,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||||
| <Form.Item label="模型能力" name="model_tag"> | <Form.Item label="模型能力" name="model_tag"> | ||||
| <Select | <Select | ||||
| allowClear | allowClear | ||||
| placeholder="请选择模型标签" | |||||
| placeholder="请选择模型能力" | |||||
| options={tagList} | options={tagList} | ||||
| fieldNames={{ label: 'name', value: 'name' }} | fieldNames={{ label: 'name', value: 'name' }} | ||||
| optionFilterProp="name" | optionFilterProp="name" | ||||
| @@ -132,12 +132,14 @@ function AddVersionModal({ | |||||
| }, | }, | ||||
| { | { | ||||
| pattern: /^[a-zA-Z0-9._-]+$/, | pattern: /^[a-zA-Z0-9._-]+$/, | ||||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||||
| message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`, | |||||
| }, | }, | ||||
| { | { | ||||
| validator: (_rule, value) => { | validator: (_rule, value) => { | ||||
| if (value === 'master') { | if (value === 'master') { | ||||
| return Promise.reject(`版本不能为 master`); | |||||
| return Promise.reject(`${name}版本不能为 master`); | |||||
| } else if (value === 'origin') { | |||||
| return Promise.reject(`${name}版本不能为 origin`); | |||||
| } | } | ||||
| return Promise.resolve(); | return Promise.resolve(); | ||||
| }, | }, | ||||
| @@ -0,0 +1,121 @@ | |||||
| import KFModal from '@/components/KFModal'; | |||||
| import { DataSource, ResourceData, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { Form, Input, message, type ModalProps } from 'antd'; | |||||
| interface EditVersionModalProps extends Omit<ModalProps, 'onOk'> { | |||||
| resourceType: ResourceType; | |||||
| resourceVersion: ResourceData; | |||||
| onOk: () => void; | |||||
| } | |||||
| function EditVersionModal({ resourceType, resourceVersion, onOk, ...rest }: EditVersionModalProps) { | |||||
| const config = resourceConfig[resourceType]; | |||||
| const { name: resoureName, version, version_desc } = resourceVersion; | |||||
| // 修改请求 | |||||
| const editDatasetVersion = async (params: any) => { | |||||
| const request = config.editVersion; | |||||
| const [res] = await to(request(params)); | |||||
| if (res) { | |||||
| message.success('编辑成功'); | |||||
| onOk?.(); | |||||
| } | |||||
| }; | |||||
| // 提交 | |||||
| const onFinish = (formData: any) => { | |||||
| const params = { | |||||
| ...resourceVersion, | |||||
| ...formData, | |||||
| [config.sourceParamKey]: DataSource.Create, | |||||
| }; | |||||
| editDatasetVersion(params); | |||||
| }; | |||||
| const name = config.name; | |||||
| return ( | |||||
| <KFModal | |||||
| {...rest} | |||||
| title="编辑版本" | |||||
| image={require('@/assets/img/create-experiment.png')} | |||||
| width={825} | |||||
| okButtonProps={{ | |||||
| htmlType: 'submit', | |||||
| form: 'form', | |||||
| }} | |||||
| > | |||||
| <Form | |||||
| name="form" | |||||
| layout="vertical" | |||||
| initialValues={{ | |||||
| name: resoureName, | |||||
| version: version, | |||||
| version_desc: version_desc, | |||||
| }} | |||||
| onFinish={onFinish} | |||||
| autoComplete="off" | |||||
| > | |||||
| <Form.Item | |||||
| label={`${name}名称`} | |||||
| name="name" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: `请输入${name}名称`, | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input disabled placeholder={`请输入${name}名称`} /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label={`${name}版本`} | |||||
| name="version" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: `请输入${name}版本`, | |||||
| }, | |||||
| { | |||||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||||
| message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`, | |||||
| }, | |||||
| { | |||||
| validator: (_rule, value) => { | |||||
| if (value === 'master') { | |||||
| return Promise.reject(`${name}版本不能为 master`); | |||||
| } else if (value === 'origin') { | |||||
| return Promise.reject(`${name}版本不能为 origin`); | |||||
| } | |||||
| return Promise.resolve(); | |||||
| }, | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="版本描述" | |||||
| name="version_desc" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入版本描述', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input.TextArea | |||||
| placeholder="请输入版本描述" | |||||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||||
| maxLength={200} | |||||
| showCount | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | |||||
| </Form> | |||||
| </KFModal> | |||||
| ); | |||||
| } | |||||
| export default EditVersionModal; | |||||
| @@ -42,6 +42,10 @@ | |||||
| border-radius: 4px; | border-radius: 4px; | ||||
| cursor: pointer; | cursor: pointer; | ||||
| &:hover { | |||||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||||
| } | |||||
| &--praised { | &--praised { | ||||
| color: @primary-color; | color: @primary-color; | ||||
| } | } | ||||
| @@ -23,6 +23,7 @@ import { App, Button, Flex, Select, Tabs } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useCallback, useEffect, useState } from 'react'; | import { useCallback, useEffect, useState } from 'react'; | ||||
| import AddVersionModal from '../AddVersionModal'; | import AddVersionModal from '../AddVersionModal'; | ||||
| import EditVersionModal from '../EditVersionModal'; | |||||
| import ResourceIntro from '../ResourceIntro'; | import ResourceIntro from '../ResourceIntro'; | ||||
| import ResourceVersion from '../ResourceVersion'; | import ResourceVersion from '../ResourceVersion'; | ||||
| import VersionCompareModal from '../VersionCompareModal'; | import VersionCompareModal from '../VersionCompareModal'; | ||||
| @@ -132,6 +133,18 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| }); | }); | ||||
| }; | }; | ||||
| // 版本编辑 | |||||
| const showEditVersionModal = () => { | |||||
| const { close } = openAntdModal(EditVersionModal, { | |||||
| resourceType: resourceType, | |||||
| resourceVersion: info, | |||||
| onOk: () => { | |||||
| getResourceDetail(); | |||||
| close(); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 选择版本 | // 选择版本 | ||||
| const showVersionSelector = () => { | const showVersionSelector = () => { | ||||
| const { close } = openAntdModal(VersionSelectorModal, { | const { close } = openAntdModal(VersionSelectorModal, { | ||||
| @@ -291,6 +304,14 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | ||||
| 创建新版本 | 创建新版本 | ||||
| </Button> | </Button> | ||||
| <Button | |||||
| type="default" | |||||
| style={{ marginLeft: '20px' }} | |||||
| icon={<KFIcon type="icon-bianji" />} | |||||
| onClick={showEditVersionModal} | |||||
| > | |||||
| 版本编辑 | |||||
| </Button> | |||||
| <Button | <Button | ||||
| type="default" | type="default" | ||||
| style={{ marginLeft: '20px' }} | style={{ marginLeft: '20px' }} | ||||
| @@ -14,9 +14,7 @@ type ResourceItemProps = { | |||||
| }; | }; | ||||
| function ResourceItem({ item, isPublic, onClick, onRemove }: ResourceItemProps) { | function ResourceItem({ item, isPublic, onClick, onRemove }: ResourceItemProps) { | ||||
| const timeAgo = `更新于${ | |||||
| item.update_time ? formatDate(item.update_time, 'YYYY-MM-DD') : item.time_ago ?? '' | |||||
| }`; | |||||
| const timeAgo = `最近更新:${formatDate(item.full_last_update_time, 'YYYY-MM-DD HH:mm')}`; | |||||
| const create_by = item.create_by ?? ''; | const create_by = item.create_by ?? ''; | ||||
| return ( | return ( | ||||
| <div className={styles['resource-item']} onClick={() => onClick(item)}> | <div className={styles['resource-item']} onClick={() => onClick(item)}> | ||||
| @@ -16,6 +16,7 @@ import styles from './index.less'; | |||||
| export type ResourceListRef = { | export type ResourceListRef = { | ||||
| reset: () => void; | reset: () => void; | ||||
| resetPage: () => void; | |||||
| }; | }; | ||||
| type ResourceListProps = { | type ResourceListProps = { | ||||
| @@ -97,6 +98,12 @@ function ResourceList( | |||||
| setDataList(undefined); | setDataList(undefined); | ||||
| setTotal(0); | setTotal(0); | ||||
| }, | }, | ||||
| resetPage: () => { | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| }, | |||||
| }; | }; | ||||
| }, | }, | ||||
| [], | [], | ||||
| @@ -56,11 +56,13 @@ function ResourcePage({ resourceType }: ResourcePageProps) { | |||||
| // 选择类型 | // 选择类型 | ||||
| const chooseType = (record: CategoryData) => { | const chooseType = (record: CategoryData) => { | ||||
| dataListRef.current?.resetPage(); | |||||
| setActiveType((prev) => (prev === record.name ? undefined : record.name)); | setActiveType((prev) => (prev === record.name ? undefined : record.name)); | ||||
| }; | }; | ||||
| // 选择 Tag | // 选择 Tag | ||||
| const chooseTag = (record: CategoryData) => { | const chooseTag = (record: CategoryData) => { | ||||
| dataListRef.current?.resetPage(); | |||||
| setActiveTag((prev) => (prev === record.name ? undefined : record.name)); | setActiveTag((prev) => (prev === record.name ? undefined : record.name)); | ||||
| }; | }; | ||||
| @@ -96,7 +98,7 @@ function ResourcePage({ resourceType }: ResourcePageProps) { | |||||
| dataType={activeType} | dataType={activeType} | ||||
| dataTag={activeTag} | dataTag={activeTag} | ||||
| initialSearchText={cacheState?.searchText} | initialSearchText={cacheState?.searchText} | ||||
| initialPagination={cacheState?.initialPagination} | |||||
| initialPagination={cacheState?.pagination} | |||||
| setCacheState={setCacheState} | setCacheState={setCacheState} | ||||
| ></ResourceList> | ></ResourceList> | ||||
| </Flex> | </Flex> | ||||
| @@ -9,6 +9,8 @@ import { | |||||
| deleteDatasetVersion, | deleteDatasetVersion, | ||||
| deleteModel, | deleteModel, | ||||
| deleteModelVersion, | deleteModelVersion, | ||||
| editDatasetVersion, | |||||
| editModelVersion, | |||||
| getDatasetInfo, | getDatasetInfo, | ||||
| getDatasetList, | getDatasetList, | ||||
| getDatasetVersionList, | getDatasetVersionList, | ||||
| @@ -36,6 +38,7 @@ type ResourceTypeInfo = { | |||||
| getVersions: (params: any) => Promise<any>; // 获取版本列表 | getVersions: (params: any) => Promise<any>; // 获取版本列表 | ||||
| deleteRecord: (params: any) => Promise<any>; // 删除 | deleteRecord: (params: any) => Promise<any>; // 删除 | ||||
| addVersion: (params: any) => Promise<any>; // 新增版本 | addVersion: (params: any) => Promise<any>; // 新增版本 | ||||
| editVersion: (params: any) => Promise<any>; // 编辑版本 | |||||
| deleteVersion: (params: any) => Promise<any>; // 删除版本 | deleteVersion: (params: any) => Promise<any>; // 删除版本 | ||||
| getInfo: (params: any) => Promise<any>; // 获取详情 | getInfo: (params: any) => Promise<any>; // 获取详情 | ||||
| compareVersion: (params: any) => Promise<any>; // 版本对比 | compareVersion: (params: any) => Promise<any>; // 版本对比 | ||||
| @@ -65,6 +68,7 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||||
| getVersions: getDatasetVersionList, | getVersions: getDatasetVersionList, | ||||
| deleteRecord: deleteDataset, | deleteRecord: deleteDataset, | ||||
| addVersion: addDatasetVersion, | addVersion: addDatasetVersion, | ||||
| editVersion: editDatasetVersion, | |||||
| deleteVersion: deleteDatasetVersion, | deleteVersion: deleteDatasetVersion, | ||||
| getInfo: getDatasetInfo, | getInfo: getDatasetInfo, | ||||
| compareVersion: compareDatasetVersion, | compareVersion: compareDatasetVersion, | ||||
| @@ -103,6 +107,7 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||||
| getVersions: getModelVersionList, | getVersions: getModelVersionList, | ||||
| deleteRecord: deleteModel, | deleteRecord: deleteModel, | ||||
| addVersion: addModelVersion, | addVersion: addModelVersion, | ||||
| editVersion: editModelVersion, | |||||
| deleteVersion: deleteModelVersion, | deleteVersion: deleteModelVersion, | ||||
| getInfo: getModelInfo, | getInfo: getModelInfo, | ||||
| compareVersion: compareModelVersion, | compareVersion: compareModelVersion, | ||||
| @@ -164,6 +169,7 @@ export interface ResourceData { | |||||
| train_task?: TrainTask; // 训练任务 | train_task?: TrainTask; // 训练任务 | ||||
| praises_count: number; // 点赞数 | praises_count: number; // 点赞数 | ||||
| praised: boolean; // 是否点赞 | praised: boolean; // 是否点赞 | ||||
| full_last_update_time: string; // 完整的更新时间 | |||||
| } | } | ||||
| // 数据集数据 | // 数据集数据 | ||||
| @@ -2,7 +2,7 @@ const Docs = () => { | |||||
| return ( | return ( | ||||
| <iframe | <iframe | ||||
| style={{ width: '100%', height: '100%', border: 0 }} | style={{ width: '100%', height: '100%', border: 0 }} | ||||
| src={'/assets/材料科研软件平台使用文档.pdf'} | |||||
| src={'/assets/材料科研软件平台使用文档-v1.0.pdf'} | |||||
| ></iframe> | ></iframe> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -77,7 +77,7 @@ function ExperimentComparison() { | |||||
| const url = res.data; | const url = res.data; | ||||
| // window.open(url, '_blank'); | // window.open(url, '_blank'); | ||||
| SessionStorage.setItem(SessionStorage.aimUrlKey, url); | SessionStorage.setItem(SessionStorage.aimUrlKey, url); | ||||
| navigate('../compare-visual'); | |||||
| navigate('compare-visual'); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -3,10 +3,10 @@ import { ExperimentStatus } from '@/enums'; | |||||
| import { useStateRef } from '@/hooks/useStateRef'; | import { useStateRef } from '@/hooks/useStateRef'; | ||||
| import { useVisible } from '@/hooks/useVisible'; | import { useVisible } from '@/hooks/useVisible'; | ||||
| import { getExperimentIns } from '@/services/experiment/index.js'; | import { getExperimentIns } from '@/services/experiment/index.js'; | ||||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { fittingString, parseJsonText } from '@/utils'; | import { fittingString, parseJsonText } from '@/utils'; | ||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { getExperimentInstanceStatus } from '@/utils/experiment'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import G6, { Util } from '@antv/g6'; | import G6, { Util } from '@antv/g6'; | ||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| @@ -18,10 +18,12 @@ import { experimentStatusInfo } from '../status'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| let graph = null; | let graph = null; | ||||
| const NodePrefix = 'workflow'; | |||||
| function ExperimentText() { | function ExperimentText() { | ||||
| const [experimentIns, setExperimentIns] = useState(undefined); | const [experimentIns, setExperimentIns] = useState(undefined); | ||||
| const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); | const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); | ||||
| const [workflowStatus, setWorkflowStatus] = useState(undefined); | |||||
| const graphRef = useRef(); | const graphRef = useRef(); | ||||
| const workflowRef = useRef(); | const workflowRef = useRef(); | ||||
| const locationParams = useParams(); // 新版本获取路由参数接口 | const locationParams = useParams(); // 新版本获取路由参数接口 | ||||
| @@ -32,10 +34,12 @@ function ExperimentText() { | |||||
| const evtSourceRef = useRef(); | const evtSourceRef = useRef(); | ||||
| const width = 110; | const width = 110; | ||||
| const height = 36; | const height = 36; | ||||
| const status = getExperimentInstanceStatus(experimentIns?.status, workflowStatus); | |||||
| const statusInfo = experimentStatusInfo[status]; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| initGraph(); | initGraph(); | ||||
| getWorkflow(); | |||||
| getExperimentInstance(); | |||||
| return () => { | return () => { | ||||
| if (evtSourceRef.current) { | if (evtSourceRef.current) { | ||||
| @@ -61,54 +65,83 @@ function ExperimentText() { | |||||
| }, []); | }, []); | ||||
| // 获取流水线模版 | // 获取流水线模版 | ||||
| const getWorkflow = async () => { | |||||
| const [res] = await to(getWorkflowById(locationParams.workflowId)); | |||||
| if (res && res.data && res.data.dag) { | |||||
| try { | |||||
| const dag = JSON.parse(res.data.dag); | |||||
| dag.nodes.forEach((item) => { | |||||
| item.in_parameters = JSON.parse(item.in_parameters); | |||||
| item.out_parameters = JSON.parse(item.out_parameters); | |||||
| item.control_strategy = JSON.parse(item.control_strategy); | |||||
| item.imgName = item.img.slice(0, item.img.length - 4); | |||||
| }); | |||||
| workflowRef.current = dag; | |||||
| getExperimentInstance(); | |||||
| } catch (error) { | |||||
| // JSON.parse 错误 | |||||
| console.error('JSON.parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // const getWorkflow = async () => { | |||||
| // const [res] = await to(getWorkflowById(locationParams.workflowId)); | |||||
| // if (res && res.data && res.data.dag) { | |||||
| // try { | |||||
| // const dag = JSON.parse(res.data.dag); | |||||
| // dag.nodes.forEach((item) => { | |||||
| // item.in_parameters = JSON.parse(item.in_parameters); | |||||
| // item.out_parameters = JSON.parse(item.out_parameters); | |||||
| // item.control_strategy = JSON.parse(item.control_strategy); | |||||
| // item.imgName = item.img.slice(0, item.img.length - 4); | |||||
| // }); | |||||
| // workflowRef.current = dag; | |||||
| // getExperimentInstance(); | |||||
| // } catch (error) { | |||||
| // // JSON.parse 错误 | |||||
| // console.error('JSON.parse error: ', error); | |||||
| // } | |||||
| // } | |||||
| // }; | |||||
| // 获取实验实例 | // 获取实验实例 | ||||
| const getExperimentInstance = async () => { | const getExperimentInstance = async () => { | ||||
| const [res] = await to(getExperimentIns(locationParams.id)); | const [res] = await to(getExperimentIns(locationParams.id)); | ||||
| if (res && res.data && workflowRef.current) { | |||||
| if (res && res.data) { | |||||
| setExperimentIns(res.data); | setExperimentIns(res.data); | ||||
| const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time } = res.data; | |||||
| const workflowData = workflowRef.current; | |||||
| const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time, dag } = res.data; | |||||
| if (!dag) { | |||||
| return; | |||||
| } | |||||
| const workflow = parseJsonText(dag); | |||||
| const experimentStatusObjs = parseJsonText(nodes_status); | const experimentStatusObjs = parseJsonText(nodes_status); | ||||
| workflowData.nodes.forEach((item) => { | |||||
| const experimentNode = experimentStatusObjs?.[item.id]; | |||||
| updateWorkflowNode(item, experimentNode); | |||||
| if (!workflow || !workflow.nodes) { | |||||
| return; | |||||
| } | |||||
| workflow.nodes.forEach((item) => { | |||||
| item.in_parameters = parseJsonText(item.in_parameters); | |||||
| item.out_parameters = parseJsonText(item.out_parameters); | |||||
| item.control_strategy = parseJsonText(item.control_strategy); | |||||
| item.imgName = item.img.slice(0, item.img.length - 4); | |||||
| }); | }); | ||||
| workflowRef.current = workflow; | |||||
| if (experimentStatusObjs) { | |||||
| // 更新各个节点 | |||||
| workflow.nodes.forEach((item) => { | |||||
| const experimentNode = experimentStatusObjs[item.id]; | |||||
| updateWorkflowNode(item, experimentNode); | |||||
| }); | |||||
| // 设置 workflow 总状态 | |||||
| Object.keys(experimentStatusObjs).some((key) => { | |||||
| if (key.startsWith(NodePrefix)) { | |||||
| const tempWorkflowStatus = experimentStatusObjs[key]; | |||||
| setWorkflowStatus(tempWorkflowStatus); | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| }); | |||||
| } | |||||
| // 绘制图 | // 绘制图 | ||||
| getGraphData(workflowData, true); | |||||
| getGraphData(workflow, true); | |||||
| if (status === ExperimentStatus.Pending) { | if (status === ExperimentStatus.Pending) { | ||||
| // 如果状态是 Pending, 打开第一个节点 | // 如果状态是 Pending, 打开第一个节点 | ||||
| const node = workflowData.nodes[0]; | |||||
| const node = workflow.nodes[0]; | |||||
| if (node) { | if (node) { | ||||
| setExperimentNodeData(node); | setExperimentNodeData(node); | ||||
| openPropsDrawer(); | openPropsDrawer(); | ||||
| } | } | ||||
| } else if (status === ExperimentStatus.Running) { | } else if (status === ExperimentStatus.Running) { | ||||
| // 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,则打开第一个节点 | |||||
| // 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点 | |||||
| const node = | const node = | ||||
| workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? | |||||
| workflowData.nodes[0]; | |||||
| workflow.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running || item.experimentStatus === ExperimentStatus.Pending) ?? | |||||
| workflow.nodes[0]; | |||||
| if (node) { | if (node) { | ||||
| setExperimentNodeData(node); | setExperimentNodeData(node); | ||||
| openPropsDrawer(); | openPropsDrawer(); | ||||
| @@ -135,23 +168,36 @@ function ExperimentText() { | |||||
| return; | return; | ||||
| } | } | ||||
| try { | try { | ||||
| const dataJson = JSON.parse(data); | |||||
| const dataJson = parseJsonText(data); | |||||
| const statusData = dataJson?.result?.object?.status; | const statusData = dataJson?.result?.object?.status; | ||||
| if (!statusData) { | if (!statusData) { | ||||
| return; | return; | ||||
| } | } | ||||
| const { startedAt, finishedAt, phase, nodes = {} } = statusData; | |||||
| setExperimentIns((prev) => ({ | |||||
| ...prev, | |||||
| finish_time: finishedAt, | |||||
| status: phase, | |||||
| })); | |||||
| const { finishedAt, phase, nodes = {} } = statusData; | |||||
| // 更新实验实例状态和结束时间 | |||||
| // setExperimentIns((prev) => ({ | |||||
| // ...prev, | |||||
| // finish_time: finishedAt, | |||||
| // status: phase, | |||||
| // })); | |||||
| // 设置总 workflow 状态 | |||||
| const tempWorkflowStatus = Object.values(nodes).find((node) => | |||||
| node.displayName.startsWith(NodePrefix), | |||||
| ); | |||||
| if (tempWorkflowStatus) { | |||||
| setWorkflowStatus(tempWorkflowStatus); | |||||
| } | |||||
| // 更新各个节点 | |||||
| const workflowData = workflowRef.current; | const workflowData = workflowRef.current; | ||||
| workflowData.nodes.forEach((item) => { | workflowData.nodes.forEach((item) => { | ||||
| const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id); | const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id); | ||||
| updateWorkflowNode(item, experimentNode); | updateWorkflowNode(item, experimentNode); | ||||
| }); | }); | ||||
| // 绘制图 | |||||
| getGraphData(workflowData, false); | getGraphData(workflowData, false); | ||||
| // 更新打开的抽屉数据 | // 更新打开的抽屉数据 | ||||
| @@ -177,6 +223,7 @@ function ExperimentText() { | |||||
| evtSourceRef.current = evtSource; | evtSourceRef.current = evtSource; | ||||
| }; | }; | ||||
| // 更新各个节点 | |||||
| function updateWorkflowNode(workflowNode, statusNode) { | function updateWorkflowNode(workflowNode, statusNode) { | ||||
| if (!statusNode) { | if (!statusNode) { | ||||
| return; | return; | ||||
| @@ -471,29 +518,30 @@ function ExperimentText() { | |||||
| <div className={styles['pipeline-container']}> | <div className={styles['pipeline-container']}> | ||||
| <div className={styles['pipeline-container__top']}> | <div className={styles['pipeline-container__top']}> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 启动时间:{formatDate(experimentIns?.create_time)} | |||||
| 启动时间:{formatDate(workflowStatus?.startedAt)} | |||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 执行时长: | 执行时长: | ||||
| <RunDuration | <RunDuration | ||||
| createTime={experimentIns?.create_time} | |||||
| finishTime={experimentIns?.finish_time} | |||||
| createTime={workflowStatus?.startedAt} | |||||
| finishTime={workflowStatus?.finishedAt} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| <div className={styles['pipeline-container__top__info']}> | <div className={styles['pipeline-container__top__info']}> | ||||
| 状态: | 状态: | ||||
| <div | |||||
| style={{ | |||||
| width: '8px', | |||||
| height: '8px', | |||||
| borderRadius: '50%', | |||||
| marginRight: '6px', | |||||
| backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, | |||||
| }} | |||||
| ></div> | |||||
| <span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}> | |||||
| {experimentStatusInfo[experimentIns?.status]?.label} | |||||
| </span> | |||||
| {statusInfo ? ( | |||||
| <> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={statusInfo.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||||
| </> | |||||
| ) : ( | |||||
| '--' | |||||
| )} | |||||
| </div> | </div> | ||||
| <Button | <Button | ||||
| className={styles['pipeline-container__top__param-button']} | className={styles['pipeline-container__top__param-button']} | ||||
| @@ -0,0 +1,12 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-03-31 16:38:59 | |||||
| * @Description: 实验可视化 Tensorboard | |||||
| */ | |||||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||||
| function TensorboardPage() { | |||||
| return <IframePage type={IframePageType.TensorBoard}></IframePage>; | |||||
| } | |||||
| export default TensorboardPage; | |||||
| @@ -30,7 +30,7 @@ interface Workflow { | |||||
| } | } | ||||
| // 根据参数设置输入组件 | // 根据参数设置输入组件 | ||||
| export const getParamComponent = (paramType: number, isSensitive?: number): JSX.Element => { | |||||
| export const getParamComponent = (paramType: number): JSX.Element => { | |||||
| // 防止后台返回不是 number 类型 | // 防止后台返回不是 number 类型 | ||||
| if (Number(paramType) === 3) { | if (Number(paramType) === 3) { | ||||
| return ( | return ( | ||||
| @@ -40,9 +40,9 @@ export const getParamComponent = (paramType: number, isSensitive?: number): JSX. | |||||
| </Radio.Group> | </Radio.Group> | ||||
| ); | ); | ||||
| } | } | ||||
| if (isSensitive && Number(isSensitive) === 1) { | |||||
| return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />; | |||||
| } | |||||
| // if (isSensitive && Number(isSensitive) === 1) { | |||||
| // return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />; | |||||
| // } | |||||
| return <Input placeholder="请输入值" allowClear />; | return <Input placeholder="请输入值" allowClear />; | ||||
| }; | }; | ||||
| @@ -95,8 +95,8 @@ function AddExperimentModal({ | |||||
| }; | }; | ||||
| const layout = { | const layout = { | ||||
| labelCol: { span: 4 }, | |||||
| wrapperCol: { span: 20 }, | |||||
| labelCol: { span: 5 }, | |||||
| wrapperCol: { span: 19 }, | |||||
| }; | }; | ||||
| const paramLayout = { | const paramLayout = { | ||||
| @@ -181,13 +181,13 @@ function AddExperimentModal({ | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item | <Form.Item | ||||
| label="选择流水线" | |||||
| label="选择流水线模板" | |||||
| name="workflow_id" | name="workflow_id" | ||||
| rules={[{ required: true, message: '请选择流水线' }]} | |||||
| rules={[{ required: true, message: '请选择流水线模板' }]} | |||||
| > | > | ||||
| <Select | <Select | ||||
| disabled={workflowDisabled} | disabled={workflowDisabled} | ||||
| placeholder="请选择流水线" | |||||
| placeholder="请选择流水线模板" | |||||
| onChange={handleWorkflowChange} | onChange={handleWorkflowChange} | ||||
| > | > | ||||
| {Array.isArray(workflowList) | {Array.isArray(workflowList) | ||||
| @@ -202,11 +202,7 @@ function AddExperimentModal({ | |||||
| </Select> | </Select> | ||||
| </Form.Item> | </Form.Item> | ||||
| {globalParam.length > 0 && ( | {globalParam.length > 0 && ( | ||||
| <Form.Item | |||||
| label="运行参数" | |||||
| tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示" | |||||
| {...tailLayout} | |||||
| > | |||||
| <Form.Item label="运行参数" tooltip="展示关联的流水线的参数" {...tailLayout}> | |||||
| <div className={styles.global_param_item}> | <div className={styles.global_param_item}> | ||||
| <Form.List name="global_param"> | <Form.List name="global_param"> | ||||
| {(fields) => | {(fields) => | ||||
| @@ -219,10 +215,7 @@ function AddExperimentModal({ | |||||
| name={[name, 'param_value']} | name={[name, 'param_value']} | ||||
| rules={getParamRules(globalParam[name]['param_type'], true)} | rules={getParamRules(globalParam[name]['param_type'], true)} | ||||
| > | > | ||||
| {getParamComponent( | |||||
| globalParam[name]['param_type'], | |||||
| globalParam[name]['is_sensitive'], | |||||
| )} | |||||
| {getParamComponent(globalParam[name]['param_type'])} | |||||
| </Form.Item> | </Form.Item> | ||||
| )) | )) | ||||
| } | } | ||||
| @@ -55,10 +55,6 @@ | |||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| width: 160px; | width: 160px; | ||||
| .statusIcon { | |||||
| visibility: visible; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -39,7 +39,14 @@ function ExperimentInstanceList({ | |||||
| }: ExperimentInstanceListProps) { | }: ExperimentInstanceListProps) { | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const allIntanceIds = useMemo(() => { | const allIntanceIds = useMemo(() => { | ||||
| return experimentInsList?.map((item) => item.id) || []; | |||||
| return ( | |||||
| experimentInsList | |||||
| ?.filter( | |||||
| (item) => | |||||
| item.status !== ExperimentStatus.Running && item.status !== ExperimentStatus.Pending, | |||||
| ) | |||||
| .map((item) => item.id) || [] | |||||
| ); | |||||
| }, [experimentInsList]); | }, [experimentInsList]); | ||||
| const [ | const [ | ||||
| selectedIns, | selectedIns, | ||||
| @@ -127,7 +134,12 @@ function ExperimentInstanceList({ | |||||
| <div> | <div> | ||||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | ||||
| <div className={styles.check}> | <div className={styles.check}> | ||||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||||
| <Checkbox | |||||
| checked={checked} | |||||
| indeterminate={indeterminate} | |||||
| disabled={allIntanceIds.length === 0} | |||||
| onChange={checkAll} | |||||
| ></Checkbox> | |||||
| </div> | </div> | ||||
| <div className={styles.index}>序号</div> | <div className={styles.index}>序号</div> | ||||
| <div className={styles.tensorBoard}>可视化</div> | <div className={styles.tensorBoard}>可视化</div> | ||||
| @@ -185,14 +197,7 @@ function ExperimentInstanceList({ | |||||
| )} | )} | ||||
| </div> | </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> | |||||
| <ExperimentInstanceComponent instance={item}></ExperimentInstanceComponent> | |||||
| <div className={styles.operation}> | <div className={styles.operation}> | ||||
| <Button | <Button | ||||
| @@ -2,69 +2,72 @@ import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { ExperimentInstance, NodeStatus } from '@/types'; | |||||
| import { ExperimentCompleted } from '@/utils/constant'; | import { ExperimentCompleted } from '@/utils/constant'; | ||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { getWorkflowStatus } from '@/utils/experiment'; | |||||
| import { Typography } from 'antd'; | import { Typography } from 'antd'; | ||||
| import React, { useCallback } from 'react'; | import React, { useCallback } from 'react'; | ||||
| import styles from './index.less'; | 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; | |||||
| type ExperimentInstanceComponentProps = { | |||||
| instance: ExperimentInstance; | |||||
| }; | }; | ||||
| function ExperimentInstance({ | |||||
| create_time, | |||||
| finish_time, | |||||
| status, | |||||
| argo_ins_name, | |||||
| argo_ins_ns, | |||||
| experimentInsId, | |||||
| }: ExperimentInstanceProps) { | |||||
| function ExperimentInstanceComponent({ instance }: ExperimentInstanceComponentProps) { | |||||
| const { id, experiment_id, argo_ins_name, argo_ins_ns, nodes_status, create_time, finish_time } = | |||||
| instance; | |||||
| const workflowStatus = getWorkflowStatus(nodes_status) as NodeStatus | undefined; | |||||
| const status = instance.status as ExperimentStatus; | |||||
| const createTime = workflowStatus?.startedAt ?? create_time; | |||||
| const finishTime = workflowStatus?.finishedAt ?? finish_time; | |||||
| const statusInfo = experimentStatusInfo[status]; | |||||
| const handleSSEMessage: MessageHandler = useCallback( | const handleSSEMessage: MessageHandler = useCallback( | ||||
| (experimentInsId: number, status: string, finish_time: string) => { | |||||
| (experimentId: number, experimentInsId: number, status: string, finishTime: string) => { | |||||
| window.postMessage({ | window.postMessage({ | ||||
| type: ExperimentCompleted, | type: ExperimentCompleted, | ||||
| payload: { | payload: { | ||||
| id: experimentInsId, | |||||
| experimentId, | |||||
| experimentInsId, | |||||
| status, | status, | ||||
| finish_time, | |||||
| finishTime, | |||||
| }, | }, | ||||
| }); | }); | ||||
| }, | }, | ||||
| [], | [], | ||||
| ); | ); | ||||
| useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||||
| useSSE(experiment_id, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||||
| return ( | return ( | ||||
| <React.Fragment> | <React.Fragment> | ||||
| <div className={styles.description}> | <div className={styles.description}> | ||||
| <div style={{ width: '50%' }}> | <div style={{ width: '50%' }}> | ||||
| <RunDuration createTime={create_time} finishTime={finish_time} /> | |||||
| <RunDuration createTime={createTime} finishTime={finishTime} /> | |||||
| </div> | </div> | ||||
| <div style={{ width: '50%' }} className={styles.startTime}> | <div style={{ width: '50%' }} className={styles.startTime}> | ||||
| <Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}> | |||||
| {formatDate(create_time)} | |||||
| <Typography.Text ellipsis={{ tooltip: formatDate(createTime) }}> | |||||
| {formatDate(createTime)} | |||||
| </Typography.Text> | </Typography.Text> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className={styles.statusBox}> | <div className={styles.statusBox}> | ||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[status]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}> | |||||
| {experimentStatusInfo[status]?.label} | |||||
| </span> | |||||
| {statusInfo ? ( | |||||
| <> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={statusInfo.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||||
| </> | |||||
| ) : ( | |||||
| '--' | |||||
| )} | |||||
| </div> | </div> | ||||
| </React.Fragment> | </React.Fragment> | ||||
| ); | ); | ||||
| } | } | ||||
| export default ExperimentInstance; | |||||
| export default ExperimentInstanceComponent; | |||||
| @@ -11,9 +11,15 @@ type ExperimentParameterProps = { | |||||
| function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | ||||
| // 控制策略 | // 控制策略 | ||||
| const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map( | |||||
| ([key, value]) => ({ key, value }), | |||||
| ); | |||||
| // const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map( | |||||
| // ([key, value]) => ({ key, value }), | |||||
| // ); | |||||
| const nodeId = nodeData.id; | |||||
| const hasTaskInfo = | |||||
| nodeId && | |||||
| !nodeId.startsWith('git-clone') && | |||||
| !nodeId.startsWith('dataset-export') && | |||||
| !nodeId.startsWith('model-export'); | |||||
| // 输入参数 | // 输入参数 | ||||
| const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({ | const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({ | ||||
| @@ -74,54 +80,58 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||||
| > | > | ||||
| <FormInfo /> | <FormInfo /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <div className={styles['experiment-parameter__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="任务信息" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| <Form.Item | |||||
| label="镜像" | |||||
| name="image" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入镜像', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| <Form.Item label="工作目录" name="working_directory"> | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| {hasTaskInfo && ( | |||||
| <> | |||||
| <div className={styles['experiment-parameter__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="任务信息" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| <Form.Item | |||||
| label="镜像" | |||||
| name="image" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入镜像', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| <Form.Item label="工作目录" name="working_directory"> | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| <Form.Item label="启动命令" name="command"> | |||||
| <FormInfo textArea /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="资源规格" | |||||
| name="resources_standard" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入资源规格', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" display /> | |||||
| </Form.Item> | |||||
| <Form.Item label="挂载路径" name="mount_path"> | |||||
| <FormInfo /> | |||||
| </Form.Item> | |||||
| <Form.Item label="环境变量" name="env_variables"> | |||||
| <FormInfo textArea /> | |||||
| </Form.Item> | |||||
| {controlStrategyList.map((item) => ( | |||||
| <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> | |||||
| <FormInfo valuePropName="showValue" /> | |||||
| </Form.Item> | |||||
| ))} | |||||
| <Form.Item label="启动命令" name="command"> | |||||
| <FormInfo textArea /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="资源规格" | |||||
| name="resources_standard" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入资源规格', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" display /> | |||||
| </Form.Item> | |||||
| {/* <Form.Item label="挂载路径" name="mount_path"> | |||||
| <FormInfo /> | |||||
| </Form.Item> */} | |||||
| <Form.Item label="环境变量" name="env_variables"> | |||||
| <FormInfo textArea /> | |||||
| </Form.Item> | |||||
| {/* {controlStrategyList.map((item) => ( | |||||
| <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> | |||||
| <FormInfo valuePropName="showValue" /> | |||||
| </Form.Item> | |||||
| ))} */} | |||||
| </> | |||||
| )} | |||||
| <div className={styles['experiment-parameter__title']}> | <div className={styles['experiment-parameter__title']}> | ||||
| <SubAreaTitle | <SubAreaTitle | ||||
| image={require('@/assets/img/duty-message.png')} | image={require('@/assets/img/duty-message.png')} | ||||
| @@ -121,6 +121,8 @@ function ExportModelModal({ | |||||
| const params = { | const params = { | ||||
| ...formData, | ...formData, | ||||
| identifier: resource?.identifier, | identifier: resource?.identifier, | ||||
| owner: resource?.owner, | |||||
| is_public: resource?.is_public, | |||||
| name: resource?.name, | name: resource?.name, | ||||
| [config.sourceParamKey]: DataSource.HandExport, | [config.sourceParamKey]: DataSource.HandExport, | ||||
| train_task: { | train_task: { | ||||
| @@ -174,6 +176,8 @@ function ExportModelModal({ | |||||
| onChange={handleResourceChange} | onChange={handleResourceChange} | ||||
| options={resources} | options={resources} | ||||
| fieldNames={{ label: 'name', value: 'id' }} | fieldNames={{ label: 'name', value: 'id' }} | ||||
| optionFilterProp="name" | |||||
| showSearch | |||||
| allowClear | allowClear | ||||
| ></Select> | ></Select> | ||||
| </Form.Item> | </Form.Item> | ||||
| @@ -191,9 +195,17 @@ function ExportModelModal({ | |||||
| } | } | ||||
| rules={[ | rules={[ | ||||
| { required: true, message: `请输入${config.name}版本` }, | { required: true, message: `请输入${config.name}版本` }, | ||||
| { | |||||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||||
| message: `${config.name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`, | |||||
| }, | |||||
| { | { | ||||
| validator: (_, value) => { | validator: (_, value) => { | ||||
| if (value && versions.map((item) => item.name).includes(value)) { | |||||
| if (value === 'master') { | |||||
| return Promise.reject(`${config.name}版本不能为 master`); | |||||
| } else if (value === 'origin') { | |||||
| return Promise.reject(`${config.name}版本不能为 origin`); | |||||
| } else if (value && versions.map((item) => item.name).includes(value)) { | |||||
| return Promise.reject(`${config.name}版本已存在`); | return Promise.reject(`${config.name}版本已存在`); | ||||
| } else { | } else { | ||||
| return Promise.resolve(); | return Promise.resolve(); | ||||
| @@ -54,7 +54,10 @@ function LogGroup({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // 建立 socket 连接 | // 建立 socket 连接 | ||||
| const setupSockect = () => { | const setupSockect = () => { | ||||
| const { host } = location; | |||||
| let { host } = location; | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| host = '172.20.32.235:31213'; | |||||
| } | |||||
| const socket = new WebSocket( | const socket = new WebSocket( | ||||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | ||||
| ); | ); | ||||
| @@ -47,10 +47,7 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||||
| name={[name, 'param_value']} | name={[name, 'param_value']} | ||||
| label={getParamLabel(globalParam[name])} | label={getParamLabel(globalParam[name])} | ||||
| > | > | ||||
| {getParamComponent( | |||||
| globalParam[name]['param_type'], | |||||
| globalParam[name]['is_sensitive'], | |||||
| )} | |||||
| {getParamComponent(globalParam[name]['param_type'])} | |||||
| </Form.Item> | </Form.Item> | ||||
| )) | )) | ||||
| } | } | ||||
| @@ -2,8 +2,10 @@ import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | ||||
| import { useCacheState } from '@/hooks/useCacheState'; | import { useCacheState } from '@/hooks/useCacheState'; | ||||
| import { useServerTime } from '@/hooks/useServerTime'; | |||||
| import { | import { | ||||
| deleteExperimentById, | deleteExperimentById, | ||||
| editExperimentInsReq, | |||||
| getExperiment, | getExperiment, | ||||
| getExperimentById, | getExperimentById, | ||||
| getQueryByExperimentId, | getQueryByExperimentId, | ||||
| @@ -17,6 +19,7 @@ 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 { ExperimentCompleted } from '@/utils/constant'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | |||||
| 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'; | ||||
| @@ -28,7 +31,6 @@ import AddExperimentModal from './components/AddExperimentModal'; | |||||
| import ExperimentInstanceList from './components/ExperimentInstanceList'; | 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(); | ||||
| @@ -39,7 +41,7 @@ function Experiment() { | |||||
| const [workflowList, setWorkflowList] = useState([]); | const [workflowList, setWorkflowList] = useState([]); | ||||
| const [experimentId, setExperimentId] = useState(null); | const [experimentId, setExperimentId] = useState(null); | ||||
| const [experimentInsList, setExperimentInsList] = useState([]); | const [experimentInsList, setExperimentInsList] = useState([]); | ||||
| const [expandedRowKeys, setExpandedRowKeys] = useState(null); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState([]); | |||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| const [isAdd, setIsAdd] = useState(true); | const [isAdd, setIsAdd] = useState(true); | ||||
| const [isModalOpen, setIsModalOpen] = useState(false); | const [isModalOpen, setIsModalOpen] = useState(false); | ||||
| @@ -59,29 +61,137 @@ function Experiment() { | |||||
| const timerRef = useRef(); | const timerRef = useRef(); | ||||
| // 获取实验列表 | // 获取实验列表 | ||||
| const getExperimentList = useCallback(async () => { | |||||
| const getExperimentList = useCallback( | |||||
| async (skipLoading = false) => { | |||||
| const params = { | |||||
| page: pagination.current - 1, | |||||
| size: pagination.pageSize, | |||||
| name: searchText || undefined, | |||||
| }; | |||||
| const [res] = await to(getExperiment(params, skipLoading)); | |||||
| 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( | |||||
| (skipLoading = false) => { | |||||
| getExperimentList(skipLoading); | |||||
| }, | |||||
| [getExperimentList], | |||||
| ); | |||||
| // 获取 TensorBoard 状态 | |||||
| const getTensorBoardStatus = useCallback(async (experimentIn) => { | |||||
| const params = { | const params = { | ||||
| page: pagination.current - 1, | |||||
| size: pagination.pageSize, | |||||
| name: searchText || undefined, | |||||
| namespace: experimentIn.nodes_result.tensorboard_log.namespace, | |||||
| path: experimentIn.nodes_result.tensorboard_log.path, | |||||
| pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, | |||||
| }; | }; | ||||
| const [res] = await to(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); | |||||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||||
| if (res && res.data) { | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIn.id) { | |||||
| return { | |||||
| ...item, | |||||
| tensorBoardStatus: res.data.status, | |||||
| tensorboardUrl: res.data.url, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| let timerId = timerIds.get(experimentIn.id); | |||||
| if (timerId) { | |||||
| clearTimeout(timerId); | |||||
| timerIds.delete(experimentIn.id); | |||||
| } | |||||
| timerId = setTimeout(() => { | |||||
| getTensorBoardStatus(experimentIn); | |||||
| }, 10 * 1000); | |||||
| timerIds.set(experimentIn.id, timerId); | |||||
| } | } | ||||
| }, [pagination, searchText]); | |||||
| }, []); | |||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = useCallback(() => { | |||||
| getExperimentList(); | |||||
| }, [getExperimentList]); | |||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = useCallback( | |||||
| async (experimentId, page, size = 5, skipLoading = false) => { | |||||
| const params = { | |||||
| experimentId: experimentId, | |||||
| page: page, | |||||
| size: size, | |||||
| }; | |||||
| const [res, error] = await to(getQueryByExperimentId(params, skipLoading)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| const list = content.map((v) => { | |||||
| const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; | |||||
| return { | |||||
| ...v, | |||||
| nodes_result, | |||||
| }; | |||||
| }); | |||||
| if (page === 0) { | |||||
| setExperimentInsList(list); | |||||
| clearExperimentInTimers(); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...list]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| // 获取 TensorBoard 状态 | |||||
| list.forEach((item) => { | |||||
| if (item.nodes_result?.tensorboard_log) { | |||||
| getTensorBoardStatus(item); | |||||
| } | |||||
| }); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }, | |||||
| [getTensorBoardStatus], | |||||
| ); | |||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = useCallback( | |||||
| (experimentId, skipLoading = false) => { | |||||
| const length = experimentInsList.length; | |||||
| getExperimentInsList(experimentId, 0, length, skipLoading); | |||||
| }, | |||||
| [experimentInsList, getExperimentInsList], | |||||
| ); | |||||
| // 更新实验状态 | |||||
| const editExperimentIns = useCallback( | |||||
| async (experimentId, experimentInsId, status, argo_ins_name, argo_ins_ns) => { | |||||
| const params = { | |||||
| experiment_id: experimentId, | |||||
| id: experimentInsId, | |||||
| status: status, | |||||
| argo_ins_name, | |||||
| argo_ins_ns, | |||||
| }; | |||||
| const [res, error] = await to(editExperimentInsReq(params)); | |||||
| if (res && res.data) { | |||||
| refreshExperimentIns(experimentId, true); | |||||
| refreshExperimentList(true); | |||||
| } | |||||
| }, | |||||
| [refreshExperimentIns, refreshExperimentList], | |||||
| ); | |||||
| // 获取流水线列表 | // 获取流水线列表 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -104,52 +214,66 @@ function Experiment() { | |||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| // 获取实验列表 | // 获取实验列表 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| getExperimentList(); | getExperimentList(); | ||||
| }, [getExperimentList]); | }, [getExperimentList]); | ||||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||||
| // 更新实验实例状态 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const handleMessage = (e) => { | const handleMessage = (e) => { | ||||
| const { type, payload } = e.data; | const { type, payload } = e.data; | ||||
| if (type === ExperimentCompleted) { | if (type === ExperimentCompleted) { | ||||
| const { id, status, finish_time } = payload; | |||||
| // 修改实例的状态和结束时间 | |||||
| setExperimentInsList((prev) => | |||||
| prev.map((v) => | |||||
| v.id === id | |||||
| ? { | |||||
| ...v, | |||||
| status: status, | |||||
| finish_time: finish_time, | |||||
| } | |||||
| : v, | |||||
| ), | |||||
| const { experimentId, experimentInsId, status, finishTime } = payload; | |||||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||||
| console.log( | |||||
| '实验实例状态变化', | |||||
| currentIns?.status, | |||||
| status, | |||||
| experimentId, | |||||
| experimentInsId, | |||||
| finishTime, | |||||
| ); | ); | ||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| if ( | |||||
| !currentIns || | |||||
| currentIns.status === ExperimentStatus.Terminated || | |||||
| currentIns.status === status | |||||
| ) { | |||||
| return; | |||||
| } | } | ||||
| timerRef.current = setTimeout(() => { | |||||
| refreshExperimentList(); | |||||
| }, 10000); | |||||
| editExperimentIns( | |||||
| experimentId, | |||||
| experimentInsId, | |||||
| status, | |||||
| currentIns.argo_ins_name, | |||||
| currentIns.argo_ins_ns, | |||||
| ); | |||||
| // refreshExperimentList(true); | |||||
| // refreshExperimentIns(experimentId); | |||||
| // 修改实例的状态和结束时间 | |||||
| // setExperimentInsList((prev) => | |||||
| // prev.map((v) => | |||||
| // v.id === experimentInsId | |||||
| // ? { | |||||
| // ...v, | |||||
| // status: status, | |||||
| // finish_time: finishTime, | |||||
| // } | |||||
| // : v, | |||||
| // ), | |||||
| // ); | |||||
| } | } | ||||
| }; | }; | ||||
| window.addEventListener('message', handleMessage); | window.addEventListener('message', handleMessage); | ||||
| return () => { | return () => { | ||||
| window.removeEventListener('message', handleMessage); | window.removeEventListener('message', handleMessage); | ||||
| if (timerRef.current) { | |||||
| clearTimeout(timerRef.current); | |||||
| timerRef.current = undefined; | |||||
| } | |||||
| }; | }; | ||||
| }, [refreshExperimentList]); | |||||
| }, [experimentInsList, editExperimentIns]); | |||||
| // 搜索 | // 搜索 | ||||
| const onSearch = (value) => { | const onSearch = (value) => { | ||||
| @@ -160,44 +284,6 @@ function Experiment() { | |||||
| })); | })); | ||||
| }; | }; | ||||
| // 获取实验实例列表 | |||||
| const getQueryByExperiment = async (experimentId, page, size = 5) => { | |||||
| const params = { | |||||
| experimentId: experimentId, | |||||
| page: page, | |||||
| size: size, | |||||
| }; | |||||
| const [res, error] = await to(getQueryByExperimentId(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setExpandedRowKeys(experimentId); | |||||
| try { | |||||
| const list = content.map((v) => { | |||||
| const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; | |||||
| return { | |||||
| ...v, | |||||
| nodes_result, | |||||
| }; | |||||
| }); | |||||
| if (page === 0) { | |||||
| setExperimentInsList(list); | |||||
| clearExperimentInTimers(); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...list]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| // 获取 TensorBoard 状态 | |||||
| list.forEach((item) => { | |||||
| if (item.nodes_result?.tensorboard_log) { | |||||
| getTensorBoardStatus(item); | |||||
| } | |||||
| }); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 运行 TensorBoard | // 运行 TensorBoard | ||||
| const runTensorBoard = async (experimentIn) => { | const runTensorBoard = async (experimentIn) => { | ||||
| const params = { | const params = { | ||||
| @@ -217,49 +303,16 @@ function Experiment() { | |||||
| } | } | ||||
| }; | }; | ||||
| // 获取 TensorBoard 状态 | |||||
| const getTensorBoardStatus = async (experimentIn) => { | |||||
| const params = { | |||||
| namespace: experimentIn.nodes_result.tensorboard_log.namespace, | |||||
| path: experimentIn.nodes_result.tensorboard_log.path, | |||||
| pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, | |||||
| }; | |||||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||||
| if (res && res.data) { | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIn.id) { | |||||
| return { | |||||
| ...item, | |||||
| tensorBoardStatus: res.data.status, | |||||
| tensorboardUrl: res.data.url, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| let timerId = timerIds.get(experimentIn.id); | |||||
| if (timerId) { | |||||
| clearTimeout(timerId); | |||||
| timerIds.delete(experimentIn.id); | |||||
| } | |||||
| timerId = setTimeout(() => { | |||||
| getTensorBoardStatus(experimentIn); | |||||
| }, 10 * 1000); | |||||
| timerIds.set(experimentIn.id, timerId); | |||||
| } | |||||
| }; | |||||
| // 展开实例 | // 展开实例 | ||||
| const expandChange = (e, record) => { | |||||
| const expandChange = (expanded, record) => { | |||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| setExperimentInsList([]); | setExperimentInsList([]); | ||||
| if (record.id === expandedRowKeys) { | |||||
| setExpandedRowKeys(null); | |||||
| } else { | |||||
| getQueryByExperiment(record.id, 0, 5); | |||||
| if (expanded) { | |||||
| setExpandedRowKeys([record.id]); | |||||
| getExperimentInsList(record.id, 0, 5); | |||||
| refreshExperimentList(); | refreshExperimentList(); | ||||
| } else { | |||||
| setExpandedRowKeys([]); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -337,8 +390,9 @@ function Experiment() { | |||||
| const [res] = await to(runExperiments(id)); | const [res] = await to(runExperiments(id)); | ||||
| if (res) { | if (res) { | ||||
| message.success('运行成功'); | message.success('运行成功'); | ||||
| setExpandedRowKeys([id]); | |||||
| refreshExperimentList(); | refreshExperimentList(); | ||||
| getQueryByExperiment(id, 0, 5); | |||||
| getExperimentInsList(id, 0, 5); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -372,14 +426,16 @@ function Experiment() { | |||||
| experimentIn.tensorBoardStatus === TensorBoardStatus.Running && | experimentIn.tensorBoardStatus === TensorBoardStatus.Running && | ||||
| experimentIn.tensorboardUrl | experimentIn.tensorboardUrl | ||||
| ) { | ) { | ||||
| window.open(experimentIn.tensorboardUrl, '_blank'); | |||||
| const url = experimentIn.tensorboardUrl; | |||||
| SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url); | |||||
| navigateToUrl(`/pipeline/experiment/visual`); | |||||
| // window.open(experimentIn.tensorboardUrl, '_blank'); | |||||
| } | } | ||||
| }; | }; | ||||
| // 实验实例终止 | // 实验实例终止 | ||||
| const handleInstanceTerminate = async (experimentIn) => { | const handleInstanceTerminate = async (experimentIn) => { | ||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| // 修改实例的状态和结束时间 | |||||
| setExperimentInsList((prevList) => { | setExperimentInsList((prevList) => { | ||||
| return prevList.map((item) => { | return prevList.map((item) => { | ||||
| if (item.id === experimentIn.id) { | if (item.id === experimentIn.id) { | ||||
| @@ -392,6 +448,9 @@ function Experiment() { | |||||
| return item; | return item; | ||||
| }); | }); | ||||
| }); | }); | ||||
| // 刷新实验列表和实例列表 | |||||
| refreshExperimentList(true); | |||||
| refreshExperimentIns(experimentIn.experiment_id); | |||||
| }; | }; | ||||
| // 实验对比菜单 | // 实验对比菜单 | ||||
| @@ -413,16 +472,10 @@ function Experiment() { | |||||
| }; | }; | ||||
| }; | }; | ||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId) => { | |||||
| const length = experimentInsList.length; | |||||
| getQueryByExperiment(experimentId, 0, length); | |||||
| }; | |||||
| // 加载更多实验实例 | // 加载更多实验实例 | ||||
| const loadMoreExperimentIns = () => { | const loadMoreExperimentIns = () => { | ||||
| const page = Math.round(experimentInsList.length / 5); | const page = Math.round(experimentInsList.length / 5); | ||||
| getQueryByExperiment(expandedRowKeys, page, 5); | |||||
| getExperimentInsList(expandedRowKeys[0], page, 5); | |||||
| }; | }; | ||||
| // 处理删除 | // 处理删除 | ||||
| @@ -607,7 +660,7 @@ function Experiment() { | |||||
| ></ExperimentInstanceList> | ></ExperimentInstanceList> | ||||
| ), | ), | ||||
| onExpand: expandChange, | onExpand: expandChange, | ||||
| expandedRowKeys: [expandedRowKeys], | |||||
| expandedRowKeys: expandedRowKeys, | |||||
| }} | }} | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| @@ -0,0 +1,12 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-03-31 16:38:59 | |||||
| * @Description: 实验对比 Aim | |||||
| */ | |||||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||||
| function AimPage() { | |||||
| return <IframePage type={IframePageType.Aim}></IframePage>; | |||||
| } | |||||
| export default AimPage; | |||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 自主机器学习详情 | |||||
| * @Description: 自动机器学习详情 | |||||
| */ | */ | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { getRayInfoReq } from '@/services/hyperParameter'; | import { getRayInfoReq } from '@/services/hyperParameter'; | ||||
| @@ -51,7 +51,7 @@ function HyperParameterInstance() { | |||||
| const [res] = await to(getRayInsReq(instanceId)); | const [res] = await to(getRayInsReq(instanceId)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| 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 } = info; | |||||
| // 解析配置参数 | // 解析配置参数 | ||||
| const paramJson = parseJsonText(param).data; | const paramJson = parseJsonText(param).data; | ||||
| if (paramJson) { | if (paramJson) { | ||||
| @@ -72,7 +72,6 @@ function HyperParameterInstance() { | |||||
| } | } | ||||
| setExperimentInfo({ | setExperimentInfo({ | ||||
| ...paramJson, | ...paramJson, | ||||
| create_time, | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -121,18 +120,17 @@ 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); | setNodes(nodes); | ||||
| // 设置总 workflow 状态 | |||||
| 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; | ||||
| // 设置工作流状态 | |||||
| if (workflowStatus) { | if (workflowStatus) { | ||||
| setWorkflowStatus(workflowStatus); | setWorkflowStatus(workflowStatus); | ||||
| // 实验结束,关闭 SSE | |||||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||||
| if ( | if ( | ||||
| workflowStatus.phase !== ExperimentStatus.Pending && | workflowStatus.phase !== ExperimentStatus.Pending && | ||||
| workflowStatus.phase !== ExperimentStatus.Running | workflowStatus.phase !== ExperimentStatus.Running | ||||
| @@ -167,8 +165,8 @@ function HyperParameterInstance() { | |||||
| <HyperParameterBasic | <HyperParameterBasic | ||||
| className={styles['hyper-parameter-instance__basic']} | className={styles['hyper-parameter-instance__basic']} | ||||
| info={experimentInfo} | info={experimentInfo} | ||||
| runStatus={workflowStatus} | |||||
| instanceStatus={instanceInfo?.status} | |||||
| workflowStatus={workflowStatus} | |||||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||||
| isInstance | isInstance | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -3,7 +3,9 @@ import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; | |||||
| import { HyperParameterTrial } from '@/pages/HyperParameter/types'; | import { HyperParameterTrial } from '@/pages/HyperParameter/types'; | ||||
| import { getExpMetricsReq } from '@/services/hyperParameter'; | import { getExpMetricsReq } from '@/services/hyperParameter'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | import tableCellRender, { TableCellValueType } from '@/utils/table'; | ||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { App, Button, Table, type TableProps } from 'antd'; | import { App, Button, Table, type TableProps } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| @@ -36,6 +38,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | ||||
| const paramsNames = Object.keys(config); | const paramsNames = Object.keys(config); | ||||
| const metricNames = Object.keys(metricAnalysis); | const metricNames = Object.keys(metricAnalysis); | ||||
| const navigate = useNavigate(); | |||||
| const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ | const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ | ||||
| { | { | ||||
| @@ -160,7 +163,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||||
| const [res] = await to(getExpMetricsReq(selectedRowKeys)); | const [res] = await to(getExpMetricsReq(selectedRowKeys)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const url = res.data; | const url = res.data; | ||||
| window.open(url, '_blank'); | |||||
| SessionStorage.setItem(SessionStorage.aimUrlKey, url); | |||||
| navigate('compare-visual'); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -1,4 +1,5 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty'; | |||||
| import LogList from '@/pages/Experiment/components/LogList'; | import LogList from '@/pages/Experiment/components/LogList'; | ||||
| import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; | import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; | ||||
| import { NodeStatus } from '@/types'; | import { NodeStatus } from '@/types'; | ||||
| @@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| // icon: <KFIcon type="icon-rizhi1" />, | // icon: <KFIcon type="icon-rizhi1" />, | ||||
| children: ( | children: ( | ||||
| <div className={styles['experiment-log__tabs__log']}> | <div className={styles['experiment-log__tabs__log']}> | ||||
| {trainCloneNodeStatus && ( | |||||
| {trainCloneNodeStatus ? ( | |||||
| <LogList | <LogList | ||||
| instanceName={instanceInfo.argo_ins_name} | instanceName={instanceInfo.argo_ins_name} | ||||
| instanceNamespace={instanceInfo.argo_ins_ns} | instanceNamespace={instanceInfo.argo_ins_ns} | ||||
| @@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| instanceNodeStartTime={trainCloneNodeStatus.startedAt} | instanceNodeStartTime={trainCloneNodeStatus.startedAt} | ||||
| instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | ||||
| ></LogList> | ></LogList> | ||||
| ) : ( | |||||
| <EmptyLog /> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| ), | ), | ||||
| @@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| // icon: <KFIcon type="icon-rizhi1" />, | // icon: <KFIcon type="icon-rizhi1" />, | ||||
| children: ( | children: ( | ||||
| <div className={styles['experiment-log__tabs__log']}> | <div className={styles['experiment-log__tabs__log']}> | ||||
| {hpoNodeStatus && ( | |||||
| {hpoNodeStatus ? ( | |||||
| <LogList | <LogList | ||||
| instanceName={instanceInfo.argo_ins_name} | instanceName={instanceInfo.argo_ins_name} | ||||
| instanceNamespace={instanceInfo.argo_ins_ns} | instanceNamespace={instanceInfo.argo_ins_ns} | ||||
| @@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||||
| instanceNodeStartTime={hpoNodeStatus.startedAt} | instanceNodeStartTime={hpoNodeStatus.startedAt} | ||||
| instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | ||||
| ></LogList> | ></LogList> | ||||
| ) : ( | |||||
| <EmptyLog /> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| ), | ), | ||||
| @@ -1,9 +1,7 @@ | |||||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | ||||
| import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums'; | import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums'; | ||||
| import { useComputingResource } from '@/hooks/useComputingResource'; | import { useComputingResource } from '@/hooks/useComputingResource'; | ||||
| import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic'; | import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { | import { | ||||
| schedulerAlgorithms, | schedulerAlgorithms, | ||||
| searchAlgorithms, | searchAlgorithms, | ||||
| @@ -18,7 +16,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 ParameterInfo from '../ParameterInfo'; | import ParameterInfo from '../ParameterInfo'; | ||||
| @@ -33,14 +30,14 @@ type HyperParameterBasicProps = { | |||||
| info?: HyperParameterData; | info?: HyperParameterData; | ||||
| className?: string; | className?: string; | ||||
| isInstance?: boolean; | isInstance?: boolean; | ||||
| runStatus?: NodeStatus; | |||||
| workflowStatus?: NodeStatus; | |||||
| instanceStatus?: ExperimentStatus; | instanceStatus?: ExperimentStatus; | ||||
| }; | }; | ||||
| function HyperParameterBasic({ | function HyperParameterBasic({ | ||||
| info, | info, | ||||
| className, | className, | ||||
| runStatus, | |||||
| workflowStatus, | |||||
| instanceStatus, | instanceStatus, | ||||
| isInstance = false, | isInstance = false, | ||||
| }: HyperParameterBasicProps) { | }: HyperParameterBasicProps) { | ||||
| @@ -83,7 +80,7 @@ function HyperParameterBasic({ | |||||
| } | } | ||||
| return [ | return [ | ||||
| { | { | ||||
| label: '代码', | |||||
| label: '代码配置', | |||||
| value: info.code_config, | value: info.code_config, | ||||
| format: formatCodeConfig, | format: formatCodeConfig, | ||||
| }, | }, | ||||
| @@ -145,56 +142,10 @@ function HyperParameterBasic({ | |||||
| ]; | ]; | ||||
| }, [info, getResourceDescription]); | }, [info, getResourceDescription]); | ||||
| const instanceDatas = useMemo(() => { | |||||
| if (!info || !runStatus) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| label: '启动时间', | |||||
| value: formatDate(info.create_time), | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '执行时长', | |||||
| value: <RunDuration createTime={info.create_time} finishTime={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['hyper-parameter-basic'], className)}> | <div className={classNames(styles['hyper-parameter-basic'], className)}> | ||||
| {isInstance && runStatus && ( | |||||
| <ExperimentRunBasic | |||||
| create_time={info?.create_time} | |||||
| runStatus={runStatus} | |||||
| instanceStatus={instanceStatus} | |||||
| /> | |||||
| {isInstance && workflowStatus && ( | |||||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||||
| )} | )} | ||||
| {!isInstance && ( | {!isInstance && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| @@ -9,6 +9,14 @@ | |||||
| background-color: white; | background-color: white; | ||||
| border-radius: 10px; | border-radius: 10px; | ||||
| &__name-row { | |||||
| :global { | |||||
| .ant-form-item-row { | |||||
| flex-wrap: nowrap; | |||||
| } | |||||
| } | |||||
| } | |||||
| &__type { | &__type { | ||||
| color: @text-color; | color: @text-color; | ||||
| font-size: @font-size-input-lg; | font-size: @font-size-input-lg; | ||||
| @@ -123,8 +123,6 @@ function MirrorCreate() { | |||||
| return true; | return true; | ||||
| }; | }; | ||||
| const descTitle = isAddVersion ? '版本描述' : '镜像描述'; | |||||
| return ( | return ( | ||||
| <div className={styles['mirror-create']}> | <div className={styles['mirror-create']}> | ||||
| <PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle> | <PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle> | ||||
| @@ -161,6 +159,7 @@ function MirrorCreate() { | |||||
| message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)', | message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)', | ||||
| }, | }, | ||||
| ]} | ]} | ||||
| className={styles['mirror-create__content__name-row']} | |||||
| > | > | ||||
| <Input | <Input | ||||
| placeholder="请输入镜像名称" | placeholder="请输入镜像名称" | ||||
| @@ -193,21 +192,45 @@ function MirrorCreate() { | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| {!isAddVersion && ( | |||||
| <Row gutter={10}> | |||||
| <Col span={20}> | |||||
| <Form.Item | |||||
| label="镜像描述" | |||||
| name="description" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入镜像描述', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input.TextArea | |||||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||||
| placeholder="请输入镜像描述" | |||||
| maxLength={128} | |||||
| showCount | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| )} | |||||
| <Row gutter={10}> | <Row gutter={10}> | ||||
| <Col span={20}> | <Col span={20}> | ||||
| <Form.Item | <Form.Item | ||||
| label={descTitle} | |||||
| name="description" | |||||
| label="版本描述" | |||||
| name="version_description" | |||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| required: true, | required: true, | ||||
| message: `请输入${descTitle}`, | |||||
| message: '请输入版本描述', | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <Input.TextArea | <Input.TextArea | ||||
| autoSize={{ minRows: 2, maxRows: 6 }} | autoSize={{ minRows: 2, maxRows: 6 }} | ||||
| placeholder={`请输入${descTitle}`} | |||||
| placeholder="请输入版本描述" | |||||
| maxLength={128} | maxLength={128} | ||||
| showCount | showCount | ||||
| allowClear | allowClear | ||||
| @@ -125,6 +125,7 @@ function MirrorInfo() { | |||||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | ||||
| }; | }; | ||||
| }); | }); | ||||
| getMirrorInfo(); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -164,20 +165,21 @@ function MirrorInfo() { | |||||
| title: '镜像版本', | title: '镜像版本', | ||||
| dataIndex: 'tag_name', | dataIndex: 'tag_name', | ||||
| key: 'tag_name', | key: 'tag_name', | ||||
| width: '25%', | |||||
| width: '30%', | |||||
| render: tableCellRender(), | render: tableCellRender(), | ||||
| }, | }, | ||||
| { | { | ||||
| title: '镜像地址', | title: '镜像地址', | ||||
| dataIndex: 'url', | dataIndex: 'url', | ||||
| key: 'url', | key: 'url', | ||||
| width: '25%', | |||||
| width: '40%', | |||||
| render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }), | render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }), | ||||
| }, | }, | ||||
| { | { | ||||
| title: '版本描述', | title: '版本描述', | ||||
| dataIndex: 'description', | dataIndex: 'description', | ||||
| key: 'description', | key: 'description', | ||||
| width: '30%', | |||||
| render: tableCellRender(true), | render: tableCellRender(true), | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -209,7 +211,7 @@ function MirrorInfo() { | |||||
| hidden: isPublic, | hidden: isPublic, | ||||
| render: (_: any, record: MirrorVersionData) => ( | render: (_: any, record: MirrorVersionData) => ( | ||||
| <div> | <div> | ||||
| {!isPublic && ( | |||||
| {!isPublic && record.status && record.status !== MirrorVersionStatus.Building && ( | |||||
| <ConfigProvider | <ConfigProvider | ||||
| theme={{ | theme={{ | ||||
| token: { | token: { | ||||
| @@ -65,14 +65,18 @@ export type MetricsChartProps = { | |||||
| function MetricsChart({ name, chartData }: MetricsChartProps) { | function MetricsChart({ name, chartData }: MetricsChartProps) { | ||||
| const chartRef = useRef<HTMLDivElement>(null); | const chartRef = useRef<HTMLDivElement>(null); | ||||
| const xAxisData = chartData[0]?.iters; | const xAxisData = chartData[0]?.iters; | ||||
| const seriesData = chartData.map((item) => { | |||||
| return { | |||||
| name: item.version, | |||||
| type: 'line' as const, | |||||
| smooth: true, | |||||
| data: item.values, | |||||
| }; | |||||
| }); | |||||
| const seriesData = useMemo( | |||||
| () => | |||||
| chartData.map((item) => { | |||||
| return { | |||||
| name: item.version, | |||||
| type: 'line' as const, | |||||
| smooth: true, | |||||
| data: item.values, | |||||
| }; | |||||
| }), | |||||
| [chartData], | |||||
| ); | |||||
| const options: echarts.EChartsOption = useMemo( | const options: echarts.EChartsOption = useMemo( | ||||
| () => ({ | () => ({ | ||||
| @@ -158,7 +162,7 @@ function MetricsChart({ name, chartData }: MetricsChartProps) { | |||||
| // 组件卸载 | // 组件卸载 | ||||
| return () => { | return () => { | ||||
| // myChart.dispose() 销毁实例 | |||||
| // 销毁实例 | |||||
| chart.dispose(); | chart.dispose(); | ||||
| }; | }; | ||||
| }, [options]); | }, [options]); | ||||
| @@ -238,7 +238,7 @@ function CreateServiceVersion() { | |||||
| }, | }, | ||||
| { | { | ||||
| pattern: /^[a-zA-Z0-9._-]+$/, | pattern: /^[a-zA-Z0-9._-]+$/, | ||||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||||
| message: '服务支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| @@ -3,16 +3,16 @@ | |||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 服务版本详情 | * @Description: 服务版本详情 | ||||
| */ | */ | ||||
| import FullScreenFrame from '@/components/FullScreenFrame'; | |||||
| import IframePage from '@/components/IFramePage'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { ServiceRunStatus } from '@/enums'; | |||||
| import { getServiceVersionInfoReq } from '@/services/modelDeployment'; | import { getServiceVersionInfoReq } from '@/services/modelDeployment'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useParams } from '@umijs/max'; | import { useParams } from '@umijs/max'; | ||||
| import { Tabs } from 'antd'; | import { Tabs } from 'antd'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import ServerLog from '../components/ServerLog'; | import ServerLog from '../components/ServerLog'; | ||||
| import UserGuide from '../components/UserGuide'; | |||||
| import VersionBasicInfo from '../components/VersionBasicInfo'; | import VersionBasicInfo from '../components/VersionBasicInfo'; | ||||
| import { ServiceVersionData } from '../types'; | import { ServiceVersionData } from '../types'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -50,32 +50,35 @@ function ServiceVersionInfo() { | |||||
| icon: <KFIcon type="icon-jibenxinxi" />, | icon: <KFIcon type="icon-jibenxinxi" />, | ||||
| children: <VersionBasicInfo info={versionInfo} />, | children: <VersionBasicInfo info={versionInfo} />, | ||||
| }, | }, | ||||
| { | |||||
| key: ModelDeploymentTabKey.Predict, | |||||
| label: '预测', | |||||
| icon: <KFIcon type="icon-yuce" />, | |||||
| children: ( | |||||
| <div style={{ height: '100%', width: '100%' }}> | |||||
| {versionInfo?.page_path && ( | |||||
| <FullScreenFrame url={versionInfo?.page_path}></FullScreenFrame> | |||||
| )} | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| key: ModelDeploymentTabKey.Guide, | |||||
| label: '调用指南', | |||||
| icon: <KFIcon type="icon-tiaoyongzhinan" />, | |||||
| children: <UserGuide info={versionInfo}></UserGuide>, | |||||
| }, | |||||
| { | |||||
| key: ModelDeploymentTabKey.Log, | |||||
| label: '服务日志', | |||||
| icon: <KFIcon type="icon-fuwurizhi" />, | |||||
| children: <ServerLog info={versionInfo}></ServerLog>, | |||||
| }, | |||||
| ]; | ]; | ||||
| if (versionInfo?.run_state === ServiceRunStatus.Running) { | |||||
| if (versionInfo?.page_path) { | |||||
| tabItems.push({ | |||||
| key: ModelDeploymentTabKey.Predict, | |||||
| label: '预测', | |||||
| icon: <KFIcon type="icon-yuce" />, | |||||
| children: <IframePage url={versionInfo?.page_path} showLoading={false}></IframePage>, | |||||
| }); | |||||
| } | |||||
| if (versionInfo?.doc_path) { | |||||
| tabItems.push({ | |||||
| key: ModelDeploymentTabKey.Guide, | |||||
| label: '调用指南', | |||||
| icon: <KFIcon type="icon-tiaoyongzhinan" />, | |||||
| children: <IframePage url={versionInfo?.doc_path}></IframePage>, | |||||
| }); | |||||
| } | |||||
| } | |||||
| tabItems.push({ | |||||
| key: ModelDeploymentTabKey.Log, | |||||
| label: '服务日志', | |||||
| icon: <KFIcon type="icon-fuwurizhi" />, | |||||
| children: <ServerLog info={versionInfo}></ServerLog>, | |||||
| }); | |||||
| return ( | return ( | ||||
| <div className={styles['service-version-info']}> | <div className={styles['service-version-info']}> | ||||
| <PageTitle title="服务版本详情"></PageTitle> | <PageTitle title="服务版本详情"></PageTitle> | ||||
| @@ -11,10 +11,18 @@ | |||||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| align-items: center; | |||||
| align-items: left; | |||||
| &--empty { | |||||
| align-items: center; | |||||
| } | |||||
| &__more { | &__more { | ||||
| padding: 20px 0; | padding: 20px 0; | ||||
| } | } | ||||
| &::-webkit-scrollbar-thumb { | |||||
| background: rgba(255, 255, 255, 0.5); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -3,6 +3,7 @@ import { getServiceVersionLogReq } from '@/services/modelDeployment'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { DoubleRightOutlined } from '@ant-design/icons'; | import { DoubleRightOutlined } from '@ant-design/icons'; | ||||
| import { Button, DatePicker, type TimeRangePickerProps } from 'antd'; | import { Button, DatePicker, type TimeRangePickerProps } from 'antd'; | ||||
| import classNames from 'classnames'; | |||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -106,6 +107,8 @@ function ServerLog({ info }: ServerLogProps) { | |||||
| } | } | ||||
| }; | }; | ||||
| const logContent = logData.map((v) => v.log_content).join(''); | |||||
| return ( | return ( | ||||
| <div className={styles['server-log']}> | <div className={styles['server-log']}> | ||||
| <div> | <div> | ||||
| @@ -122,9 +125,9 @@ function ServerLog({ info }: ServerLogProps) { | |||||
| 查询 | 查询 | ||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| {logData.length > 0 && ( | |||||
| {logContent ? ( | |||||
| <div className={styles['server-log__data']} id="server-log"> | <div className={styles['server-log__data']} id="server-log"> | ||||
| <div>{logData.map((v) => v.log_content).join('') || '暂无日志'}</div> | |||||
| <div>{logContent}</div> | |||||
| {hasMore && ( | {hasMore && ( | ||||
| <Button | <Button | ||||
| type="text" | type="text" | ||||
| @@ -137,6 +140,10 @@ function ServerLog({ info }: ServerLogProps) { | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| ) : ( | |||||
| <div className={classNames(styles['server-log__data'], styles['server-log__data--empty'])}> | |||||
| 暂无日志 | |||||
| </div> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -110,7 +110,7 @@ const EditPipeline = () => { | |||||
| // console.log(data); | // console.log(data); | ||||
| const errorNode = data.nodes.find((item) => item.formError === true); | const errorNode = data.nodes.find((item) => item.formError === true); | ||||
| if (errorNode) { | if (errorNode) { | ||||
| message.error(`【${errorNode.label}】节点必填项必须配置`); | |||||
| message.error(`【${errorNode.label}】节点配置验证失败`); | |||||
| const graphNode = graph.findById(errorNode.id); | const graphNode = graph.findById(errorNode.id); | ||||
| if (graphNode) { | if (graphNode) { | ||||
| openNodeDrawer(graphNode, true); | openNodeDrawer(graphNode, true); | ||||
| @@ -148,7 +148,7 @@ const GlobalParamsDrawer = forwardRef( | |||||
| > | > | ||||
| {getParamComponent(type)} | {getParamComponent(type)} | ||||
| </Form.Item> | </Form.Item> | ||||
| {type !== 3 && ( | |||||
| {/* {type !== 3 && ( | |||||
| <Form.Item | <Form.Item | ||||
| {...restField} | {...restField} | ||||
| name={[name, 'is_sensitive']} | name={[name, 'is_sensitive']} | ||||
| @@ -161,7 +161,7 @@ const GlobalParamsDrawer = forwardRef( | |||||
| <Radio value={0}>否</Radio> | <Radio value={0}>否</Radio> | ||||
| </Radio.Group> | </Radio.Group> | ||||
| </Form.Item> | </Form.Item> | ||||
| )} | |||||
| )} */} | |||||
| </> | </> | ||||
| ); | ); | ||||
| }} | }} | ||||
| @@ -19,6 +19,7 @@ import { openAntdModal } from '@/utils/modal'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { INode } from '@antv/g6'; | import { INode } from '@antv/g6'; | ||||
| import { Button, Drawer, Form, Input, MenuProps } from 'antd'; | import { Button, Drawer, Form, Input, MenuProps } from 'antd'; | ||||
| import { RuleObject } from 'antd/es/form'; | |||||
| import { NamePath } from 'antd/es/form/interface'; | import { NamePath } from 'antd/es/form/interface'; | ||||
| import { forwardRef, useImperativeHandle, useState } from 'react'; | import { forwardRef, useImperativeHandle, useState } from 'react'; | ||||
| import PropsLabel from '../PropsLabel'; | import PropsLabel from '../PropsLabel'; | ||||
| @@ -34,6 +35,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( | const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( | ||||
| {} as PipelineNodeModelSerialize, | {} as PipelineNodeModelSerialize, | ||||
| ); | ); | ||||
| const nodeId = Form.useWatch('id', form) as string; | |||||
| const hasTaskInfo = | |||||
| nodeId && | |||||
| !nodeId.startsWith('git-clone') && | |||||
| !nodeId.startsWith('dataset-export') && | |||||
| !nodeId.startsWith('model-export'); | |||||
| const [open, setOpen] = useState(false); | const [open, setOpen] = useState(false); | ||||
| const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | ||||
| @@ -45,10 +52,11 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| const fields = form.getFieldsValue(); | const fields = form.getFieldsValue(); | ||||
| // 保存字段顺序 | // 保存字段顺序 | ||||
| const control_strategy = { | |||||
| ...stagingItem.control_strategy, | |||||
| ...fields.control_strategy, | |||||
| }; | |||||
| // const control_strategy = { | |||||
| // ...stagingItem.control_strategy, | |||||
| // ...fields.control_strategy, | |||||
| // }; | |||||
| const in_parameters = { | const in_parameters = { | ||||
| ...stagingItem.in_parameters, | ...stagingItem.in_parameters, | ||||
| ...fields.in_parameters, | ...fields.in_parameters, | ||||
| @@ -63,7 +71,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| const res = { | const res = { | ||||
| ...stagingItem, | ...stagingItem, | ||||
| ...fields, | ...fields, | ||||
| control_strategy: JSON.stringify(control_strategy), | |||||
| // control_strategy: JSON.stringify(control_strategy), | |||||
| in_parameters: JSON.stringify(in_parameters), | in_parameters: JSON.stringify(in_parameters), | ||||
| out_parameters: JSON.stringify(out_parameters), | out_parameters: JSON.stringify(out_parameters), | ||||
| formError: !!error, | formError: !!error, | ||||
| @@ -90,7 +98,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| ...model, | ...model, | ||||
| in_parameters: JSON.parse(model.in_parameters), | in_parameters: JSON.parse(model.in_parameters), | ||||
| out_parameters: JSON.parse(model.out_parameters), | out_parameters: JSON.parse(model.out_parameters), | ||||
| control_strategy: JSON.parse(model.control_strategy), | |||||
| // control_strategy: JSON.parse(model.control_strategy), | |||||
| }; | }; | ||||
| // console.log('model', nodeData); | // console.log('model', nodeData); | ||||
| setStagingItem({ | setStagingItem({ | ||||
| @@ -309,22 +317,47 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| ); | ); | ||||
| }; | }; | ||||
| // 模型部署-服务版本验证 | |||||
| const serviceVersionValidator = (_rule: RuleObject, value: any) => { | |||||
| // 输入参数值都是对象,如果是手动输入,要求 /^[a-zA-Z0-9._-]+$/ | |||||
| if ( | |||||
| typeof value === 'object' && | |||||
| value && | |||||
| !value.fromSelect && | |||||
| value.value && | |||||
| !/^[a-zA-Z0-9._-]+$/.test(value.value) | |||||
| ) { | |||||
| return Promise.reject('服务版本只支持字母、数字、点(.)、下划线(_)、中横线(-)'); | |||||
| } | |||||
| return Promise.resolve(); | |||||
| }; | |||||
| // 必填项校验规则 | // 必填项校验规则 | ||||
| const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => { | const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => { | ||||
| return item.value.require | |||||
| const id = form.getFieldValue('id') as string; | |||||
| const rules = item.value.require | |||||
| ? [ | ? [ | ||||
| { | { | ||||
| validator: requiredValidator, | validator: requiredValidator, | ||||
| message: '必填项', | |||||
| }, | }, | ||||
| ] | ] | ||||
| : []; | : []; | ||||
| // 模型部署-服务版本验证 | |||||
| if (id.startsWith('model-deploy') && item.key === '--version') { | |||||
| rules.push({ | |||||
| validator: serviceVersionValidator, | |||||
| }); | |||||
| } | |||||
| return rules; | |||||
| }; | }; | ||||
| // 控制策略 | // 控制策略 | ||||
| const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( | |||||
| ([key, value]) => ({ key, value }), | |||||
| ); | |||||
| // const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( | |||||
| // ([key, value]) => ({ key, value }), | |||||
| // ); | |||||
| // 输入参数 | // 输入参数 | ||||
| const inParametersList = Object.entries(stagingItem.in_parameters ?? {}).map(([key, value]) => ({ | const inParametersList = Object.entries(stagingItem.in_parameters ?? {}).map(([key, value]) => ({ | ||||
| @@ -396,71 +429,73 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| > | > | ||||
| <Input disabled /> | <Input disabled /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="任务信息" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| <Form.Item label="镜像" required> | |||||
| <div className={styles['pipeline-drawer__ref-row']}> | |||||
| <Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}> | |||||
| <Input placeholder="请输入或选择镜像" allowClear /> | |||||
| {hasTaskInfo && ( | |||||
| <> | |||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="任务信息" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| <Form.Item label="镜像" required> | |||||
| <div className={styles['pipeline-drawer__ref-row']}> | |||||
| <Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}> | |||||
| <Input placeholder="请输入或选择镜像" allowClear /> | |||||
| </Form.Item> | |||||
| <Form.Item noStyle> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| icon={getSelectBtnIcon({ item_type: 'image' })} | |||||
| onClick={() => selectResource('image', { item_type: 'image' })} | |||||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||||
| > | |||||
| 选择镜像 | |||||
| </Button> | |||||
| </Form.Item> | |||||
| </div> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item noStyle> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| icon={getSelectBtnIcon({ item_type: 'image' })} | |||||
| onClick={() => selectResource('image', { item_type: 'image' })} | |||||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||||
| > | |||||
| 选择镜像 | |||||
| </Button> | |||||
| <Form.Item | |||||
| name="working_directory" | |||||
| label={ | |||||
| <PropsLabel | |||||
| menuItems={menuItems} | |||||
| title="工作目录" | |||||
| onClick={(value) => { | |||||
| handleParameterClick('working_directory', value); | |||||
| }} | |||||
| /> | |||||
| } | |||||
| > | |||||
| <Input placeholder="请输入工作目录" allowClear /> | |||||
| </Form.Item> | </Form.Item> | ||||
| </div> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| name="working_directory" | |||||
| label={ | |||||
| <PropsLabel | |||||
| menuItems={menuItems} | |||||
| title="工作目录" | |||||
| onClick={(value) => { | |||||
| handleParameterClick('working_directory', value); | |||||
| }} | |||||
| /> | |||||
| } | |||||
| > | |||||
| <Input placeholder="请输入工作目录" allowClear /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| name="command" | |||||
| label={ | |||||
| <PropsLabel | |||||
| menuItems={menuItems} | |||||
| title="启动命令" | |||||
| onClick={(value) => { | |||||
| handleParameterClick('command', value); | |||||
| }} | |||||
| /> | |||||
| } | |||||
| > | |||||
| <TextArea placeholder="请输入启动命令" allowClear /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="资源规格" | |||||
| name="resources_standard" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请选择资源规格', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| <Form.Item | |||||
| name="command" | |||||
| label={ | |||||
| <PropsLabel | |||||
| menuItems={menuItems} | |||||
| title="启动命令" | |||||
| onClick={(value) => { | |||||
| handleParameterClick('command', value); | |||||
| }} | |||||
| /> | |||||
| } | |||||
| > | |||||
| <TextArea placeholder="请输入启动命令" allowClear /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="资源规格" | |||||
| name="resources_standard" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请选择资源规格', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> | |||||
| </Form.Item> | |||||
| {/* <Form.Item | |||||
| name="mount_path" | name="mount_path" | ||||
| label={ | label={ | ||||
| <PropsLabel | <PropsLabel | ||||
| @@ -473,23 +508,23 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| } | } | ||||
| > | > | ||||
| <Input placeholder="请输入挂载路径" allowClear /> | <Input placeholder="请输入挂载路径" allowClear /> | ||||
| </Form.Item> | |||||
| <Form.Item | |||||
| name="env_variables" | |||||
| label={ | |||||
| <PropsLabel | |||||
| menuItems={menuItems} | |||||
| title="环境变量" | |||||
| onClick={(value) => { | |||||
| handleParameterClick('env_variables', value); | |||||
| }} | |||||
| /> | |||||
| } | |||||
| > | |||||
| <TextArea placeholder="请输入环境变量" allowClear /> | |||||
| </Form.Item> | |||||
| {/* 控制参数 */} | |||||
| {controlStrategyList.map((item) => ( | |||||
| </Form.Item> */} | |||||
| <Form.Item | |||||
| name="env_variables" | |||||
| label={ | |||||
| <PropsLabel | |||||
| menuItems={menuItems} | |||||
| title="环境变量" | |||||
| onClick={(value) => { | |||||
| handleParameterClick('env_variables', value); | |||||
| }} | |||||
| /> | |||||
| } | |||||
| > | |||||
| <TextArea placeholder="请输入环境变量" allowClear /> | |||||
| </Form.Item> | |||||
| {/* 控制参数 */} | |||||
| {/* {controlStrategyList.map((item) => ( | |||||
| <Form.Item | <Form.Item | ||||
| key={item.key} | key={item.key} | ||||
| name={['control_strategy', item.key]} | name={['control_strategy', item.key]} | ||||
| @@ -499,7 +534,10 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| > | > | ||||
| <ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput> | <ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput> | ||||
| </Form.Item> | </Form.Item> | ||||
| ))} | |||||
| ))} */} | |||||
| </> | |||||
| )} | |||||
| {/* 输入参数 */} | {/* 输入参数 */} | ||||
| {inParametersList.length > 0 && ( | {inParametersList.length > 0 && ( | ||||
| <> | <> | ||||
| @@ -190,7 +190,7 @@ const Pipeline = () => { | |||||
| }), | }), | ||||
| }, | }, | ||||
| { | { | ||||
| title: '流水线名称', | |||||
| title: '流水线模板名称', | |||||
| dataIndex: 'name', | dataIndex: 'name', | ||||
| key: 'name', | key: 'name', | ||||
| width: '50%', | width: '50%', | ||||
| @@ -199,7 +199,7 @@ const Pipeline = () => { | |||||
| }), | }), | ||||
| }, | }, | ||||
| { | { | ||||
| title: '流水线描述', | |||||
| title: '流水线模板描述', | |||||
| dataIndex: 'description', | dataIndex: 'description', | ||||
| key: 'description', | key: 'description', | ||||
| width: '50%', | width: '50%', | ||||
| @@ -113,7 +113,7 @@ const AuthUserTableList: React.FC = () => { | |||||
| dataIndex: 'createTime', | dataIndex: 'createTime', | ||||
| valueType: 'dateRange', | valueType: 'dateRange', | ||||
| render: (_, record) => { | render: (_, record) => { | ||||
| return <span>{record.createTime.toString()} </span>; | |||||
| return <span>{record.createTime?.toString()} </span>; | |||||
| }, | }, | ||||
| hideInSearch: true, | hideInSearch: true, | ||||
| }, | }, | ||||
| @@ -84,7 +84,7 @@ const UserSelectorModal: React.FC<DataScopeFormProps> = (props) => { | |||||
| valueType: 'dateRange', | valueType: 'dateRange', | ||||
| hideInSearch: true, | hideInSearch: true, | ||||
| render: (_, record) => { | render: (_, record) => { | ||||
| return <span>{record.createTime.toString()} </span>; | |||||
| return <span>{record.createTime?.toString()} </span>; | |||||
| }, | }, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -177,7 +177,7 @@ const RoleTableList: React.FC = () => { | |||||
| confirm({ | confirm({ | ||||
| title: `确认要${text}${record.roleName}角色吗?`, | title: `确认要${text}${record.roleName}角色吗?`, | ||||
| onOk() { | onOk() { | ||||
| changeRoleStatus(record.roleId, newStatus).then((resp) => { | |||||
| changeRoleStatus(record.roleId, record.roleKey, newStatus).then((resp) => { | |||||
| if (resp.code === 200) { | if (resp.code === 200) { | ||||
| messageApi.open({ | messageApi.open({ | ||||
| type: 'success', | type: 'success', | ||||
| @@ -240,7 +240,7 @@ const RoleTableList: React.FC = () => { | |||||
| dataIndex: 'createTime', | dataIndex: 'createTime', | ||||
| valueType: 'dateRange', | valueType: 'dateRange', | ||||
| render: (_, record) => { | render: (_, record) => { | ||||
| return <span>{record.createTime.toString()} </span>; | |||||
| return <span>{record.createTime?.toString()} </span>; | |||||
| }, | }, | ||||
| search: { | search: { | ||||
| transform: (value) => { | transform: (value) => { | ||||
| @@ -28,8 +28,11 @@ const DeptTree: React.FC<TreeProps> = (props) => { | |||||
| const res = await getDeptTree({}); | const res = await getDeptTree({}); | ||||
| const treeData = res.map((item: any) => ({ ...item, key: item.id })); | const treeData = res.map((item: any) => ({ ...item, key: item.id })); | ||||
| setTreeData(treeData); | setTreeData(treeData); | ||||
| setExpandedKeys([treeData[0].key]); | |||||
| setSelectedKeys([treeData[0].key]); | |||||
| if (treeData.length > 0) { | |||||
| onSelect(treeData[0]); | |||||
| setExpandedKeys([treeData[0].key]); | |||||
| setSelectedKeys([treeData[0].key]); | |||||
| } | |||||
| hide(); | hide(); | ||||
| return true; | return true; | ||||
| } catch (error) { | } catch (error) { | ||||
| @@ -41,12 +44,6 @@ const DeptTree: React.FC<TreeProps> = (props) => { | |||||
| fetchDeptList(); | fetchDeptList(); | ||||
| }, []); | }, []); | ||||
| useEffect(() => { | |||||
| if (treeData.length > 0) { | |||||
| onSelect(treeData[0]); | |||||
| } | |||||
| }, [treeData, onSelect]); | |||||
| const handleSelect = (keys: React.Key[], info: any) => { | const handleSelect = (keys: React.Key[], info: any) => { | ||||
| setSelectedKeys(keys); | setSelectedKeys(keys); | ||||
| onSelect(info.node); | onSelect(info.node); | ||||
| @@ -203,9 +203,8 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| pattern: /^[a-zA-Z](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/, | |||||
| message: | |||||
| '只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以字母开头,数字或字母结尾', | |||||
| pattern: /^[a-zA-Z][a-zA-Z0-9]{3,14}$/, | |||||
| message: '用户账号长度为4 ~ 15位,且必须以字母开头,只支持字母和数字', | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| /> | /> | ||||
| @@ -37,7 +37,18 @@ | |||||
| &__summary { | &__summary { | ||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| width: 33.33%; | |||||
| width: 40%; | |||||
| text-align: left; | |||||
| &:nth-child(3n+2) { | |||||
| text-align: center; | |||||
| width: 30%; | |||||
| } | |||||
| &:nth-child(3n) { | |||||
| text-align: right; | |||||
| width: 30%; | |||||
| } | |||||
| &__title { | &__title { | ||||
| margin-bottom: 12px; | margin-bottom: 12px; | ||||
| @@ -16,7 +16,7 @@ function AssetsManagement() { | |||||
| }; | }; | ||||
| const [res] = await to(getWorkspaceAssetCountReq(params)); | const [res] = await to(getWorkspaceAssetCountReq(params)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const { component, dataset, image, model, workflow } = res.data; | |||||
| const { dataset, image, model, workflow, codeConfig, service } = res.data; | |||||
| const items = [ | const items = [ | ||||
| { | { | ||||
| title: '数据集', | title: '数据集', | ||||
| @@ -30,10 +30,10 @@ function AssetsManagement() { | |||||
| title: '镜像', | title: '镜像', | ||||
| value: image, | value: image, | ||||
| }, | }, | ||||
| { | |||||
| title: '组件', | |||||
| value: component, | |||||
| }, | |||||
| // { | |||||
| // title: '组件', | |||||
| // value: component, | |||||
| // }, | |||||
| // { | // { | ||||
| // title: '代码配置', | // title: '代码配置', | ||||
| // value: 0, | // value: 0, | ||||
| @@ -42,6 +42,14 @@ function AssetsManagement() { | |||||
| title: '流水线模版', | title: '流水线模版', | ||||
| value: workflow, | value: workflow, | ||||
| }, | }, | ||||
| { | |||||
| title: '代码配置', | |||||
| value: codeConfig, | |||||
| }, | |||||
| { | |||||
| title: '服务', | |||||
| value: service, | |||||
| }, | |||||
| ]; | ]; | ||||
| setAssetCounts(items); | setAssetCounts(items); | ||||
| } | } | ||||
| @@ -53,7 +61,7 @@ function AssetsManagement() { | |||||
| return ( | return ( | ||||
| <div className={styles['assets-management']}> | <div className={styles['assets-management']}> | ||||
| <Flex justify="space-between"> | <Flex justify="space-between"> | ||||
| <div className={styles['assets-management__title']}>AI资产</div> | |||||
| <div className={styles['assets-management__title']}>多形态资源库</div> | |||||
| <Select | <Select | ||||
| size="small" | size="small" | ||||
| value={type} | value={type} | ||||
| @@ -67,11 +75,11 @@ function AssetsManagement() { | |||||
| /> | /> | ||||
| </Flex> | </Flex> | ||||
| {/* <div className={styles['assets-management__increase']}>今日新增数量:5</div> */} | {/* <div className={styles['assets-management__increase']}>今日新增数量:5</div> */} | ||||
| <Flex gap="22px 0" wrap="wrap" style={{ marginTop: '40px' }}> | |||||
| <Flex gap="22px 0" wrap="wrap" style={{ marginTop: '40px', padding: '0 8px' }}> | |||||
| {assetCounts.map((item, index) => ( | {assetCounts.map((item, index) => ( | ||||
| <div className={styles['assets-management__summary']} key={index}> | <div className={styles['assets-management__summary']} key={index}> | ||||
| <div className={styles['assets-management__summary__title']}>{item.title}</div> | <div className={styles['assets-management__summary__title']}>{item.title}</div> | ||||
| <div className={styles['assets-management__summary__value']}>{item.value}</div> | |||||
| <div className={styles['assets-management__summary__value']}>{item.value ?? '-'}</div> | |||||
| </div> | </div> | ||||
| ))} | ))} | ||||
| </Flex> | </Flex> | ||||
| @@ -1,6 +1,6 @@ | |||||
| .experiment-table { | .experiment-table { | ||||
| flex: 1; | flex: 1; | ||||
| min-width: 378px; | |||||
| min-width: 460px; | |||||
| height: 140px; | height: 140px; | ||||
| padding: 12px; | padding: 12px; | ||||
| background: @workspace-background; | background: @workspace-background; | ||||
| @@ -32,7 +32,7 @@ | |||||
| } | } | ||||
| &__status { | &__status { | ||||
| width: 15%; | |||||
| width: 25%; | |||||
| } | } | ||||
| &__duration { | &__duration { | ||||
| @@ -40,11 +40,11 @@ | |||||
| } | } | ||||
| &__date { | &__date { | ||||
| width: calc(60% - 60px); | |||||
| width: 40%; | |||||
| } | } | ||||
| &__operation { | &__operation { | ||||
| width: 60px; | |||||
| width: 10%; | |||||
| :global { | :global { | ||||
| .ant-btn-link { | .ant-btn-link { | ||||
| @@ -1,10 +1,9 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { ExperimentStatus, experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { ExperimentInstance } from '@/types'; | import { ExperimentInstance } from '@/types'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||||
| import { useNavigate } from '@umijs/max'; | import { useNavigate } from '@umijs/max'; | ||||
| import { Button, Empty } from 'antd'; | |||||
| import { Empty } from 'antd'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import ExperimentInstanceComponent from './instance'; | |||||
| type ExperimentTableProps = { | type ExperimentTableProps = { | ||||
| tableData: ExperimentInstance[]; | tableData: ExperimentInstance[]; | ||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| @@ -26,31 +25,11 @@ function ExperimentTable({ tableData = [], style }: ExperimentTableProps) { | |||||
| </div> | </div> | ||||
| {Array.isArray(tableData) && tableData.length > 0 ? ( | {Array.isArray(tableData) && tableData.length > 0 ? ( | ||||
| tableData.map((item) => ( | tableData.map((item) => ( | ||||
| <div className={styles['experiment-table__content']} key={item.id}> | |||||
| <div className={styles['experiment-table__status']} style={{ paddingLeft: '6.5px' }}> | |||||
| <img | |||||
| src={experimentStatusInfo[item.status as ExperimentStatus]?.icon} | |||||
| width={17} | |||||
| height={17} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| </div> | |||||
| <div className={styles['experiment-table__duration']}> | |||||
| {elapsedTime(item.create_time, item.finish_time)} | |||||
| </div> | |||||
| <div className={styles['experiment-table__date']}>{formatDate(item.create_time)}</div> | |||||
| <div className={styles['experiment-table__operation']}> | |||||
| <Button | |||||
| size="small" | |||||
| type="link" | |||||
| icon={<KFIcon type="icon-xiangqing2" font={14} />} | |||||
| onClick={() => gotoExperiment(item)} | |||||
| > | |||||
| 详情 | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| <ExperimentInstanceComponent | |||||
| instance={item} | |||||
| key={item.id} | |||||
| onClick={() => gotoExperiment(item)} | |||||
| /> | |||||
| )) | )) | ||||
| ) : ( | ) : ( | ||||
| <Empty | <Empty | ||||
| @@ -0,0 +1,76 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import RunDuration from '@/components/RunDuration'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { ExperimentInstance, NodeStatus } from '@/types'; | |||||
| import { formatDate } from '@/utils/date'; | |||||
| import { getExperimentInstanceStatus, getWorkflowStatus } from '@/utils/experiment'; | |||||
| import { Button } from 'antd'; | |||||
| import { useCallback, useState } from 'react'; | |||||
| import styles from './index.less'; | |||||
| const NodePrefix = 'workflow'; | |||||
| type ExperimentInstanceComponentProps = { | |||||
| instance: ExperimentInstance; | |||||
| onClick: () => void; | |||||
| }; | |||||
| function ExperimentInstanceComponent({ instance, onClick }: ExperimentInstanceComponentProps) { | |||||
| const { experiment_id, id, argo_ins_name, argo_ins_ns, nodes_status } = instance; | |||||
| const initialWorkflowStatus = getWorkflowStatus(nodes_status) as NodeStatus | undefined; | |||||
| const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>( | |||||
| initialWorkflowStatus, | |||||
| ); | |||||
| const status = getExperimentInstanceStatus(instance.status as ExperimentStatus, workflowStatus); | |||||
| const createTime = workflowStatus?.startedAt; | |||||
| const finishTime = workflowStatus?.finishedAt; | |||||
| const statusInfo = experimentStatusInfo[status]; | |||||
| const handleSSEMessage: MessageHandler = useCallback( | |||||
| ( | |||||
| _experimentId: number, | |||||
| _experimentInsId: number, | |||||
| _status: string, | |||||
| _finishTime: string, | |||||
| nodes, | |||||
| ) => { | |||||
| if (nodes) { | |||||
| // 设置总 workflow 状态 | |||||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||||
| node.displayName.startsWith(NodePrefix), | |||||
| ) as NodeStatus; | |||||
| if (workflowStatus) { | |||||
| setWorkflowStatus(workflowStatus); | |||||
| } | |||||
| } | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| useSSE(experiment_id, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||||
| return ( | |||||
| <div className={styles['experiment-table__content']} key={id}> | |||||
| <div className={styles['experiment-table__status']} style={{ paddingLeft: '6.5px' }}> | |||||
| <img src={statusInfo?.icon} width={17} height={17} draggable={false} alt="" /> | |||||
| </div> | |||||
| <div className={styles['experiment-table__duration']}> | |||||
| <RunDuration createTime={createTime} finishTime={finishTime} /> | |||||
| </div> | |||||
| <div className={styles['experiment-table__date']}>{formatDate(createTime)}</div> | |||||
| <div className={styles['experiment-table__operation']}> | |||||
| <Button | |||||
| size="small" | |||||
| type="link" | |||||
| icon={<KFIcon type="icon-xiangqing2" font={14} />} | |||||
| onClick={onClick} | |||||
| > | |||||
| 详情 | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentInstanceComponent; | |||||
| @@ -16,13 +16,13 @@ function QuickStart() { | |||||
| const changeScale = () => { | const changeScale = () => { | ||||
| // body 的宽度 - 菜单的宽度 - 两个 padding - 右边用户管理的宽度 - 右边用户管理的 marginLeft - 滚动条的宽度, | // body 的宽度 - 菜单的宽度 - 两个 padding - 右边用户管理的宽度 - 右边用户管理的 marginLeft - 滚动条的宽度, | ||||
| const width = document.body.offsetWidth - 256 - 80 - 60 - 326 - 15 - 8; | const width = document.body.offsetWidth - 256 - 80 - 60 - 326 - 15 - 8; | ||||
| if (width >= 1223) { | |||||
| const spaceX = (width - 192 * 5 - 60) / 7; | |||||
| if (width >= 1002) { | |||||
| const spaceX = (width - 192 * 4 - 60) / 6; | |||||
| setSpace(spaceX); | setSpace(spaceX); | ||||
| setCanvasWidth('100%'); | setCanvasWidth('100%'); | ||||
| setScale(1.0); | setScale(1.0); | ||||
| } else { | } else { | ||||
| const ratio = width / 1223; | |||||
| const ratio = width / 1002; | |||||
| setCanvasWidth('1223px'); | setCanvasWidth('1223px'); | ||||
| setSpace(29); | setSpace(29); | ||||
| setScale(ratio); | setScale(ratio); | ||||
| @@ -54,56 +54,56 @@ function QuickStart() { | |||||
| > | > | ||||
| <WorkFlow | <WorkFlow | ||||
| content="为开发者提供数据智能标注与数据回流服务" | content="为开发者提供数据智能标注与数据回流服务" | ||||
| buttonText="数据准备" | |||||
| buttonTop={40} | |||||
| buttonText="数据标注" | |||||
| buttonTop={20} | |||||
| x={left} | x={left} | ||||
| y={309} | y={309} | ||||
| onClick={() => navigate('/datasetPreparation/datasetAnnotation')} | onClick={() => navigate('/datasetPreparation/datasetAnnotation')} | ||||
| /> | /> | ||||
| <WorkFlow | |||||
| {/* <WorkFlow | |||||
| content="为开发者提供定制化编辑器,开发者可根据自己需求选择配置,保存编译器中的调试环境为镜像供训练使用" | content="为开发者提供定制化编辑器,开发者可根据自己需求选择配置,保存编译器中的调试环境为镜像供训练使用" | ||||
| buttonText="开发环境" | buttonText="开发环境" | ||||
| buttonTop={20} | buttonTop={20} | ||||
| x={left + 192 + space} | x={left + 192 + space} | ||||
| y={301} | y={301} | ||||
| onClick={() => navigate('/developmentEnvironment')} | onClick={() => navigate('/developmentEnvironment')} | ||||
| /> | |||||
| /> */} | |||||
| <WorkFlow | <WorkFlow | ||||
| content="为开发者提供定制化编辑器,开发者可根据自己需求选择配置,保存编译器中的调试环境为镜像供训练使用" | content="为开发者提供定制化编辑器,开发者可根据自己需求选择配置,保存编译器中的调试环境为镜像供训练使用" | ||||
| tips="可视化建模Designer" | tips="可视化建模Designer" | ||||
| buttonText="流水线" | |||||
| buttonText="流水线模板" | |||||
| buttonTop={20} | buttonTop={20} | ||||
| x={left + 2 * (192 + space)} | |||||
| x={left + 1 * (192 + space)} | |||||
| y={276} | y={276} | ||||
| onClick={() => navigate('/pipeline/template')} | onClick={() => navigate('/pipeline/template')} | ||||
| /> | /> | ||||
| <WorkFlow | <WorkFlow | ||||
| content="开发者可以在这里运行流水线模板,产生实验实例,对比实验训练过程与产生的实验训练数据" | content="开发者可以在这里运行流水线模板,产生实验实例,对比实验训练过程与产生的实验训练数据" | ||||
| buttonText="实验" | buttonText="实验" | ||||
| buttonTop={40} | |||||
| x={left + 3 * (192 + space)} | |||||
| buttonTop={20} | |||||
| x={left + 2 * (192 + space)} | |||||
| y={295} | y={295} | ||||
| onClick={() => navigate('/pipeline/experiment')} | onClick={() => navigate('/pipeline/experiment')} | ||||
| /> | /> | ||||
| <WorkFlow | <WorkFlow | ||||
| content="支持异构硬件(CPU/GPU)的模型加载,高吞吐,低延迟;支持大规模复杂模型的一键部署,实时弹性扩缩容;提供完整的运维监控体系。" | content="支持异构硬件(CPU/GPU)的模型加载,高吞吐,低延迟;支持大规模复杂模型的一键部署,实时弹性扩缩容;提供完整的运维监控体系。" | ||||
| tips="模型在线服务" | tips="模型在线服务" | ||||
| buttonText="模型在线部署" | |||||
| buttonText="服务" | |||||
| buttonTop={20} | buttonTop={20} | ||||
| x={left + 4 * (192 + space) + 60 + space} | |||||
| x={left + 3 * (192 + space) + 60 + space} | |||||
| y={263} | y={263} | ||||
| onClick={() => navigate('/dataset/modelDeployment')} | onClick={() => navigate('/dataset/modelDeployment')} | ||||
| /> | /> | ||||
| <div | <div | ||||
| className={styles['quick-start__content__canvas__model']} | className={styles['quick-start__content__canvas__model']} | ||||
| style={{ top: '358px', left: left + 4 * (192 + space) + 'px' }} | |||||
| style={{ top: '358px', left: left + 3 * (192 + space) + 'px' }} | |||||
| > | > | ||||
| <KFIcon type="icon-moxingguanli" font={38} /> | <KFIcon type="icon-moxingguanli" font={38} /> | ||||
| <span>模型管理</span> | <span>模型管理</span> | ||||
| </div> | </div> | ||||
| <div | <div | ||||
| className={styles['quick-start__content__canvas__task']} | className={styles['quick-start__content__canvas__task']} | ||||
| style={{ top: '110px', left: left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 }} | |||||
| style={{ top: '110px', left: left + 1 * (192 + space) + 56 + taskLeftArrowWidth + 16 }} | |||||
| > | > | ||||
| <KFIcon type="icon-tiaoduguanli" font={13} style={{ marginRight: '5px' }} /> | <KFIcon type="icon-tiaoduguanli" font={13} style={{ marginRight: '5px' }} /> | ||||
| <span>任务自动调度</span> | <span>任务自动调度</span> | ||||
| @@ -117,7 +117,7 @@ function QuickStart() { | |||||
| arrorwTop={-4} | arrorwTop={-4} | ||||
| borderBottom={1} | borderBottom={1} | ||||
| /> | /> | ||||
| <WorkArrow | |||||
| {/* <WorkArrow | |||||
| x={left + 2 * 192 + space + 1} | x={left + 2 * 192 + space + 1} | ||||
| y={378} | y={378} | ||||
| width={arrowWidth} | width={arrowWidth} | ||||
| @@ -125,9 +125,9 @@ function QuickStart() { | |||||
| arrowLeft={arrowWidth} | arrowLeft={arrowWidth} | ||||
| arrorwTop={-4} | arrorwTop={-4} | ||||
| borderBottom={1} | borderBottom={1} | ||||
| /> | |||||
| /> */} | |||||
| <WorkArrow | <WorkArrow | ||||
| x={left + 4 * 192 + 3 * space + 1} | |||||
| x={left + 3 * 192 + 2 * space + 1} | |||||
| y={378} | y={378} | ||||
| width={arrowWidth + 10} | width={arrowWidth + 10} | ||||
| height={1} | height={1} | ||||
| @@ -136,7 +136,7 @@ function QuickStart() { | |||||
| borderBottom={1} | borderBottom={1} | ||||
| /> | /> | ||||
| <WorkArrow | <WorkArrow | ||||
| x={left + 4 * 192 + 60 + 4 * space + 1 - 10} | |||||
| x={left + 3 * 192 + 60 + 3 * space + 1 - 10} | |||||
| y={378} | y={378} | ||||
| width={arrowWidth + 10} | width={arrowWidth + 10} | ||||
| height={1} | height={1} | ||||
| @@ -145,7 +145,7 @@ function QuickStart() { | |||||
| borderBottom={1} | borderBottom={1} | ||||
| /> | /> | ||||
| <WorkArrow | <WorkArrow | ||||
| x={left + 2 * (192 + space) + 56} | |||||
| x={left + 1 * (192 + space) + 56} | |||||
| y={139} | y={139} | ||||
| width={taskLeftArrowWidth} | width={taskLeftArrowWidth} | ||||
| height={120} | height={120} | ||||
| @@ -155,7 +155,7 @@ function QuickStart() { | |||||
| borderTop={1} | borderTop={1} | ||||
| /> | /> | ||||
| <WorkArrow | <WorkArrow | ||||
| x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4} | |||||
| x={left + 1 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4} | |||||
| y={127} | y={127} | ||||
| width={taskRightArrowWidth} | width={taskRightArrowWidth} | ||||
| height={156} | height={156} | ||||
| @@ -3,16 +3,16 @@ | |||||
| align-items: center; | align-items: center; | ||||
| justify-content: center; | justify-content: center; | ||||
| height: 140px; | height: 140px; | ||||
| padding: 0 16px; | |||||
| padding: 0 35px; | |||||
| // 媒体查询 | |||||
| @media screen and (max-width: 1600px) { | |||||
| flex: auto; | |||||
| } | |||||
| // // 媒体查询 | |||||
| // @media screen and (max-width: 1600px) { | |||||
| // flex: auto; | |||||
| // } | |||||
| &__icon { | &__icon { | ||||
| width: 63px; | |||||
| margin-right: 16px; | |||||
| width: 80px; | |||||
| margin-right: 20px; | |||||
| } | } | ||||
| &__title { | &__title { | ||||
| @@ -7,6 +7,7 @@ | |||||
| height: 228px; | height: 228px; | ||||
| padding: 0 20px; | padding: 0 20px; | ||||
| .backgroundFullImage(url(@/assets/img/user-points-bg.png)); | .backgroundFullImage(url(@/assets/img/user-points-bg.png)); | ||||
| margin-bottom: 16px; | |||||
| &__label { | &__label { | ||||
| margin-top: 60px; | margin-top: 60px; | ||||
| @@ -2,7 +2,7 @@ | |||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| margin-bottom: 16px; | margin-bottom: 16px; | ||||
| padding: 0 30px; | |||||
| padding: 20px 30px; | |||||
| background-image: url(@/assets/img/workspace-intro.png); | background-image: url(@/assets/img/workspace-intro.png); | ||||
| background-repeat: no-repeat; | background-repeat: no-repeat; | ||||
| background-position: top right; | background-position: top right; | ||||
| @@ -1,16 +1,15 @@ | |||||
| import { Button } from 'antd'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| function WorkspaceIntro() { | function WorkspaceIntro() { | ||||
| return ( | return ( | ||||
| <div className={styles['workspace-intro']}> | <div className={styles['workspace-intro']}> | ||||
| <div className={styles['workspace-intro__left']}> | <div className={styles['workspace-intro__left']}> | ||||
| <div className={styles['workspace-intro__title']}>自主实验平台</div> | |||||
| <div className={styles['workspace-intro__title']}>智能材料科研平台</div> | |||||
| <div className={styles['workspace-intro__content']}> | <div className={styles['workspace-intro__content']}> | ||||
| 材料领域的自主实验系统是一种用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能, | |||||
| 智能材料科研平台是用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能, | |||||
| 以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作 | 以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作 | ||||
| </div> | </div> | ||||
| <div className={styles['workspace-intro__buttons']}> | |||||
| {/* <div className={styles['workspace-intro__buttons']}> | |||||
| <Button | <Button | ||||
| type="primary" | type="primary" | ||||
| style={{ marginRight: '20px' }} | style={{ marginRight: '20px' }} | ||||
| @@ -40,7 +39,7 @@ function WorkspaceIntro() { | |||||
| > | > | ||||
| 分子材料自主实验系统 | 分子材料自主实验系统 | ||||
| </Button> | </Button> | ||||
| </div> | |||||
| </div> */} | |||||
| </div> | </div> | ||||
| <div className={styles['workspace-intro__right']}> | <div className={styles['workspace-intro__right']}> | ||||
| <img | <img | ||||
| @@ -27,7 +27,7 @@ | |||||
| &__statistics { | &__statistics { | ||||
| flex: none; | flex: none; | ||||
| min-width: 431px; | |||||
| // min-width: 500px; | |||||
| background: linear-gradient( | background: linear-gradient( | ||||
| 123.08deg, | 123.08deg, | ||||
| rgba(138, 138, 138, 0.06) 1.32%, | rgba(138, 138, 138, 0.06) 1.32%, | ||||
| @@ -35,10 +35,10 @@ | |||||
| ); | ); | ||||
| border-radius: 4px; | border-radius: 4px; | ||||
| // 媒体查询 | |||||
| @media screen and (max-width: 1600px) { | |||||
| flex: 1; | |||||
| } | |||||
| // // 媒体查询 | |||||
| // @media screen and (max-width: 1600px) { | |||||
| // flex: 1; | |||||
| // } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,18 +1,17 @@ | |||||
| import { useDraggable } from '@/hooks/useDraggable'; | |||||
| // import { useDraggable } from '@/hooks/useDraggable'; | |||||
| import { getWorkspaceOverviewReq } from '@/services/workspace'; | import { getWorkspaceOverviewReq } from '@/services/workspace'; | ||||
| import { ExperimentInstance } from '@/types'; | import { ExperimentInstance } from '@/types'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { Divider, Flex } from 'antd'; | import { Divider, Flex } from 'antd'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import Draggable from 'react-draggable'; | |||||
| // import Draggable from 'react-draggable'; | |||||
| import AssetsManagement from './components/AssetsManagement'; | import AssetsManagement from './components/AssetsManagement'; | ||||
| import ExperimentChart, { type ExperimentStatistics } from './components/ExperimentChart'; | import ExperimentChart, { type ExperimentStatistics } from './components/ExperimentChart'; | ||||
| import ExperitableTable from './components/ExperimentTable'; | import ExperitableTable from './components/ExperimentTable'; | ||||
| import QuickStart from './components/QuickStart'; | import QuickStart from './components/QuickStart'; | ||||
| import RobotFrame from './components/RobotFrame'; | |||||
| // import RobotFrame from './components/RobotFrame'; | |||||
| import TotalStatistics from './components/TotalStatistics'; | import TotalStatistics from './components/TotalStatistics'; | ||||
| import UserPoints from './components/UserPoints'; | import UserPoints from './components/UserPoints'; | ||||
| import UserSpace from './components/UserSpace'; | |||||
| import WorkspaceIntro from './components/WorkspaceIntro'; | import WorkspaceIntro from './components/WorkspaceIntro'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -25,10 +24,10 @@ type OverviewData = { | |||||
| function Workspace() { | function Workspace() { | ||||
| const [overviewData, setOverviewData] = useState<OverviewData>(); | const [overviewData, setOverviewData] = useState<OverviewData>(); | ||||
| const [robotFrameVisible, setRobotFrameVisible] = useState(false); | |||||
| const { handleStart, handleStop, handleDrag, handleClick } = useDraggable(() => | |||||
| setRobotFrameVisible((prev) => !prev), | |||||
| ); | |||||
| // const [robotFrameVisible, setRobotFrameVisible] = useState(false); | |||||
| // const { handleStart, handleStop, handleDrag, handleClick } = useDraggable(() => | |||||
| // setRobotFrameVisible((prev) => !prev), | |||||
| // ); | |||||
| const users: number[] = new Array(8).fill(0); | const users: number[] = new Array(8).fill(0); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -67,7 +66,6 @@ function Workspace() { | |||||
| count={overviewData?.runningExperimentInsCount} | count={overviewData?.runningExperimentInsCount} | ||||
| /> | /> | ||||
| </Flex> | </Flex> | ||||
| <ExperitableTable | <ExperitableTable | ||||
| tableData={overviewData?.latestExperimentInsList || []} | tableData={overviewData?.latestExperimentInsList || []} | ||||
| ></ExperitableTable> | ></ExperitableTable> | ||||
| @@ -76,16 +74,16 @@ function Workspace() { | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <UserPoints /> | |||||
| </Flex> | </Flex> | ||||
| <div className={styles['workspace__quick-start']}> | <div className={styles['workspace__quick-start']}> | ||||
| <QuickStart></QuickStart> | <QuickStart></QuickStart> | ||||
| <div className={styles['workspace__user']}> | <div className={styles['workspace__user']}> | ||||
| <UserSpace users={users}></UserSpace> | |||||
| <UserPoints /> | |||||
| {/* <UserSpace users={users}></UserSpace> */} | |||||
| <AssetsManagement></AssetsManagement> | <AssetsManagement></AssetsManagement> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <Draggable onStart={handleStart} onStop={handleStop} onDrag={handleDrag} bounds="body"> | |||||
| {/* <Draggable onStart={handleStart} onStop={handleStop} onDrag={handleDrag} bounds="body"> | |||||
| <img | <img | ||||
| className={styles['workspace__robot-img']} | className={styles['workspace__robot-img']} | ||||
| src={require('@/assets/img/robot.png')} | src={require('@/assets/img/robot.png')} | ||||
| @@ -98,7 +96,7 @@ function Workspace() { | |||||
| <RobotFrame | <RobotFrame | ||||
| visible={robotFrameVisible} | visible={robotFrameVisible} | ||||
| onClose={() => setRobotFrameVisible(false)} | onClose={() => setRobotFrameVisible(false)} | ||||
| ></RobotFrame> | |||||
| ></RobotFrame> */} | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -7,10 +7,11 @@ | |||||
| import { request } from '@umijs/max'; | import { request } from '@umijs/max'; | ||||
| // 分页查询超参数自动寻优 | // 分页查询超参数自动寻优 | ||||
| export function getActiveLearnListReq(params) { | |||||
| export function getActiveLearnListReq(params, skipLoading) { | |||||
| return request(`/api/mmp/activeLearn`, { | return request(`/api/mmp/activeLearn`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -54,10 +55,11 @@ export function runActiveLearnReq(id) { | |||||
| // ----------------------- 实验实例 ----------------------- | // ----------------------- 实验实例 ----------------------- | ||||
| // 获取实验实例列表 | // 获取实验实例列表 | ||||
| export function getActiveLearnInsListReq(params) { | |||||
| export function getActiveLearnInsListReq(params, skipLoading) { | |||||
| return request(`/api/mmp/activeLearnIns`, { | return request(`/api/mmp/activeLearnIns`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -99,3 +101,12 @@ export function getExpMetricsReq(data) { | |||||
| }); | }); | ||||
| } | } | ||||
| // 编辑实验实例 | |||||
| export function editActiveLearnInsReq(data) { | |||||
| return request(`/api/mmp/activeLearnIns`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| skipLoading: true, | |||||
| }); | |||||
| } | |||||
| @@ -7,10 +7,11 @@ | |||||
| import { request } from '@umijs/max'; | import { request } from '@umijs/max'; | ||||
| // 分页查询自动学习 | // 分页查询自动学习 | ||||
| export function getAutoMLListReq(params) { | |||||
| export function getAutoMLListReq(params, skipLoading) { | |||||
| return request(`/api/mmp/machineLearn`, { | return request(`/api/mmp/machineLearn`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading, | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -54,10 +55,11 @@ export function runAutoMLReq(id) { | |||||
| // ----------------------- 实验实例 ----------------------- | // ----------------------- 实验实例 ----------------------- | ||||
| // 获取实验实例列表 | // 获取实验实例列表 | ||||
| export function getExperimentInsListReq(params) { | |||||
| export function getExperimentInsListReq(params, skipLoading) { | |||||
| return request(`/api/mmp/machineLearnIns`, { | return request(`/api/mmp/machineLearnIns`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading, | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -89,3 +91,12 @@ export function batchDeleteExperimentInsReq(data) { | |||||
| data, | data, | ||||
| }); | }); | ||||
| } | } | ||||
| // 编辑实验实例 | |||||
| export function editExperimentInsReq(data) { | |||||
| return request(`/api/mmp/machineLearnIns`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| skipLoading: true, | |||||
| }); | |||||
| } | |||||
| @@ -58,6 +58,14 @@ export function addDatasetVersion(data) { | |||||
| }); | }); | ||||
| } | } | ||||
| // 编辑数据集版本 | |||||
| export function editDatasetVersion(data) { | |||||
| return request(`/api/mmp/newdataset/updateVersionDesc`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 下载数据集所有文件 | // 下载数据集所有文件 | ||||
| export function downloadAllFiles(params) { | export function downloadAllFiles(params) { | ||||
| return request(`/api/mmp/newdataset/downloadAllFiles`, { | return request(`/api/mmp/newdataset/downloadAllFiles`, { | ||||
| @@ -132,6 +140,15 @@ export function addModelVersion(data) { | |||||
| }); | }); | ||||
| } | } | ||||
| // 编辑模型版本 | |||||
| export function editModelVersion(data) { | |||||
| return request(`/api/mmp/newmodel/updateVersionDesc`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 删除模型版本 | // 删除模型版本 | ||||
| export function deleteModelVersion(params) { | export function deleteModelVersion(params) { | ||||
| return request(`/api/mmp/newmodel/deleteVersion`, { | return request(`/api/mmp/newmodel/deleteVersion`, { | ||||
| @@ -1,9 +1,10 @@ | |||||
| import { request } from '@umijs/max'; | import { request } from '@umijs/max'; | ||||
| // 查询实验列表 | // 查询实验列表 | ||||
| export function getExperiment(params) { | |||||
| export function getExperiment(params, skipLoading) { | |||||
| return request(`/api/mmp/experiment`, { | return request(`/api/mmp/experiment`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading, | |||||
| }); | }); | ||||
| } | } | ||||
| // 运行实验 | // 运行实验 | ||||
| @@ -28,10 +29,11 @@ export function deleteExperimentById(id) { | |||||
| }); | }); | ||||
| } | } | ||||
| // 根据id查询实验实例 | // 根据id查询实验实例 | ||||
| export function getQueryByExperimentId(params) { | |||||
| export function getQueryByExperimentId(params, skipLoading) { | |||||
| return request(`/api/mmp/experimentIns`, { | return request(`/api/mmp/experimentIns`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading | |||||
| }); | }); | ||||
| } | } | ||||
| // 根据id删除实验实例 | // 根据id删除实验实例 | ||||
| @@ -53,6 +55,16 @@ export function putQueryByExperimentInsId(id) { | |||||
| method: 'PUT', | method: 'PUT', | ||||
| }); | }); | ||||
| } | } | ||||
| // 编辑实验实例 | |||||
| export function editExperimentInsReq(data) { | |||||
| return request(`/api/mmp/experimentIns`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| skipLoading: true, | |||||
| }); | |||||
| } | |||||
| // 查询实验实例实时日志 | // 查询实验实例实时日志 | ||||
| export function getQueryByExperimentLog(data) { | export function getQueryByExperimentLog(data) { | ||||
| return request('/api/mmp/experimentIns/realTimeLog/', { | return request('/api/mmp/experimentIns/realTimeLog/', { | ||||
| @@ -7,10 +7,11 @@ | |||||
| import { request } from '@umijs/max'; | import { request } from '@umijs/max'; | ||||
| // 分页查询超参数自动寻优 | // 分页查询超参数自动寻优 | ||||
| export function getRayListReq(params) { | |||||
| export function getRayListReq(params, skipLoading) { | |||||
| return request(`/api/mmp/ray`, { | return request(`/api/mmp/ray`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -54,10 +55,11 @@ export function runRayReq(id) { | |||||
| // ----------------------- 实验实例 ----------------------- | // ----------------------- 实验实例 ----------------------- | ||||
| // 获取实验实例列表 | // 获取实验实例列表 | ||||
| export function getRayInsListReq(params) { | |||||
| export function getRayInsListReq(params, skipLoading) { | |||||
| return request(`/api/mmp/rayIns`, { | return request(`/api/mmp/rayIns`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params, | params, | ||||
| skipLoading, | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -97,3 +99,13 @@ export function getExpMetricsReq(data) { | |||||
| data, | data, | ||||
| }); | }); | ||||
| } | } | ||||
| // 编辑实验实例 | |||||
| export function editRayInsReq(data) { | |||||
| return request(`/api/mmp/rayIns`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| skipLoading: true, | |||||
| }); | |||||
| } | |||||
| @@ -70,9 +70,10 @@ export function updateRoleDataScope(data: Record<string, any>) { | |||||
| } | } | ||||
| // 角色状态修改 | // 角色状态修改 | ||||
| export function changeRoleStatus(roleId: number, status: string) { | |||||
| export function changeRoleStatus(roleId: number, roleKey: string, status: string) { | |||||
| const data = { | const data = { | ||||
| roleId, | roleId, | ||||
| roleKey, | |||||
| status, | status, | ||||
| }; | }; | ||||
| return request<API.Result>('/api/system/role/changeStatus', { | return request<API.Result>('/api/system/role/changeStatus', { | ||||
| @@ -52,6 +52,7 @@ export type ExperimentInstance = { | |||||
| nodes_result: { | nodes_result: { | ||||
| [key: string]: any; | [key: string]: any; | ||||
| }; | }; | ||||
| node_status: string; | |||||
| nodes_status: string; | nodes_status: string; | ||||
| global_param: PipelineGlobalParam[]; | global_param: PipelineGlobalParam[]; | ||||
| tensorBoardStatus?: TensorBoardStatus; | tensorBoardStatus?: TensorBoardStatus; | ||||
| @@ -111,9 +112,9 @@ export type KeysToCamelCase<T> = { | |||||
| // 序列化后的流水线节点 | // 序列化后的流水线节点 | ||||
| export type PipelineNodeModelSerialize = Omit< | export type PipelineNodeModelSerialize = Omit< | ||||
| PipelineNodeModel, | PipelineNodeModel, | ||||
| 'control_strategy' | 'in_parameters' | 'out_parameters' | |||||
| 'in_parameters' | 'out_parameters' | |||||
| > & { | > & { | ||||
| control_strategy: Record<string, PipelineNodeModelParameter>; | |||||
| // control_strategy: Record<string, PipelineNodeModelParameter>; | |||||
| in_parameters: Record<string, PipelineNodeModelParameter>; | in_parameters: Record<string, PipelineNodeModelParameter>; | ||||
| out_parameters: Record<string, PipelineNodeModelParameter>; | out_parameters: Record<string, PipelineNodeModelParameter>; | ||||
| }; | }; | ||||
| @@ -1,4 +1,3 @@ | |||||
| import { now } from '@/hooks/useServerTime'; | |||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| /** | /** | ||||
| @@ -13,8 +12,12 @@ export const elapsedTime = (begin?: string | Date | null, end?: string | Date | | |||||
| return '--'; | return '--'; | ||||
| } | } | ||||
| if (end === undefined || end === null) { | |||||
| return '--'; | |||||
| } | |||||
| const beginDate = dayjs(begin); | const beginDate = dayjs(begin); | ||||
| const endDate = end === undefined || end === null ? dayjs(now()) : dayjs(end); | |||||
| const endDate = dayjs(end); // end === undefined || end === null ? dayjs(now()) : dayjs(end); | |||||
| if (!beginDate.isValid() || !endDate.isValid()) { | if (!beginDate.isValid() || !endDate.isValid()) { | ||||
| return '--'; | return '--'; | ||||
| } | } | ||||
| @@ -0,0 +1,60 @@ | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { NodeStatus } from '@/types'; | |||||
| import { parseJsonText } from './index'; | |||||
| /** | |||||
| * 获取工作流节点 | |||||
| * | |||||
| * @param node_status - 流水线workflow节点,json字符串 | |||||
| * @return workflow 节点 | |||||
| */ | |||||
| export const getWorkflowStatus = (node_status?: string | null) => { | |||||
| if (!node_status) { | |||||
| return undefined; | |||||
| } | |||||
| const nodeStatusJson = parseJsonText(node_status); | |||||
| if (!nodeStatusJson) { | |||||
| return undefined; | |||||
| } | |||||
| for (const key in nodeStatusJson) { | |||||
| if (key.startsWith('workflow')) { | |||||
| return nodeStatusJson[key]; | |||||
| } | |||||
| } | |||||
| return undefined; | |||||
| }; | |||||
| /** | |||||
| * 获取实例状态 | |||||
| * 终止或者 workflowStatus 不存在时,取实例状态,否则取流水线状态 | |||||
| * | |||||
| * @param instanceStatus - 实例状态 | |||||
| * @param workflowStatus - 流水线workflow节点 | |||||
| * @return 实例状态 | |||||
| */ | |||||
| export const getExperimentInstanceStatus = ( | |||||
| instanceStatus: ExperimentStatus, | |||||
| workflowStatus?: NodeStatus, | |||||
| ): ExperimentStatus => { | |||||
| return instanceStatus === ExperimentStatus.Terminated || !workflowStatus | |||||
| ? instanceStatus | |||||
| : workflowStatus?.phase; | |||||
| }; | |||||
| /** | |||||
| * 获取实例状态 | |||||
| * 终止或者 workflowStatus 不存在时,取实例状态,否则取流水线状态 | |||||
| * | |||||
| * @param instanceStatus - 实例状态 | |||||
| * @param workflowStatus - 流水线workflow节点 | |||||
| * @return 实例状态 | |||||
| */ | |||||
| export const getInstanceStatusInList = ( | |||||
| instanceStatus: ExperimentStatus, | |||||
| node_status?: string | null, | |||||
| ): ExperimentStatus => { | |||||
| const workflowStatus = getWorkflowStatus(node_status); | |||||
| return getExperimentInstanceStatus(instanceStatus, workflowStatus); | |||||
| }; | |||||
| @@ -6,7 +6,6 @@ | |||||
| 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 位随机数 | ||||
| @@ -268,19 +267,25 @@ export const hasNoValue = (value?: any | null): boolean => { | |||||
| /** | /** | ||||
| * 获取 git 仓库的 url | * 获取 git 仓库的 url | ||||
| * | * | ||||
| * @param {string} url - the url of the git repository | |||||
| * @param {string} branch - the branch of the repository | |||||
| * @param {string} [url] - the url of the git repository | |||||
| * @param {string} [branch] - the branch of the repository | |||||
| * @return {string} the url of the repository | * @return {string} the url of the repository | ||||
| * | * | ||||
| * If `branch` is given, the url will be in the format of 'http://gitlab.com/user/repo/tree/branch'. | * If `branch` is given, the url will be in the format of 'http://gitlab.com/user/repo/tree/branch'. | ||||
| * Otherwise, the url will be in the format of 'http://gitlab.com/user/repo'. | * Otherwise, the url will be in the format of 'http://gitlab.com/user/repo'. | ||||
| */ | */ | ||||
| export const getGitUrl = (url: string, branch: string): string => { | |||||
| export const getGitUrl = (url?: string, branch?: string): string => { | |||||
| if (!url) { | if (!url) { | ||||
| return ''; | return ''; | ||||
| } | } | ||||
| const gitUrl = url.replace(/\.git$/, ''); | |||||
| return branch ? `${gitUrl}/tree/${branch}` : gitUrl; | |||||
| let gitUrlStr = url.replace(/\.git$/, ''); | |||||
| const gitUrl = new URL(gitUrlStr); | |||||
| if (gitUrl.port === '30202') { | |||||
| gitUrl.port = '30203'; // 30202 该为 30203 | |||||
| } | |||||
| gitUrlStr = gitUrl.href; | |||||
| return branch ? `${gitUrlStr.toString()}/tree/${branch}` : gitUrlStr; | |||||
| }; | }; | ||||
| /** | /** | ||||
| @@ -347,4 +352,3 @@ 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; | ||||
| }; | }; | ||||