diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 7af61ab5..2f757e15 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -50,7 +50,7 @@ export async function getInitialState(): Promise { // 如果不是登录页面,执行 const { location } = history; - console.log('getInitialState', needAuth(location.pathname)); + // console.log('getInitialState', needAuth(location.pathname)); if (needAuth(location.pathname)) { const currentUser = await fetchUserInfo(); return { @@ -163,7 +163,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { const { location } = e; const menus = getRemoteMenu(); - console.log('onRouteChange', menus); + // console.log('onRouteChange', menus); if (menus === null && needAuth(location.pathname)) { history.go(0); } @@ -174,12 +174,12 @@ export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { }; export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { - console.log('patchClientRoutes', e); + // console.log('patchClientRoutes', e); patchRouteWithRemoteMenus(e.routes); }; export function render(oldRender: () => void) { - console.log('render'); + // console.log('render'); const token = getAccessToken(); if (!token || token?.length === 0) { oldRender(); diff --git a/react-ui/src/assets/img/resample-icon.png b/react-ui/src/assets/img/resample-icon.png new file mode 100644 index 00000000..fa24c1aa Binary files /dev/null and b/react-ui/src/assets/img/resample-icon.png differ diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index 26359678..34d5b51b 100644 --- a/react-ui/src/enums/index.ts +++ b/react-ui/src/enums/index.ts @@ -18,9 +18,9 @@ export enum AvailableRange { // 实验状态 export enum ExperimentStatus { + Pending = 'Pending', // 启动中 Running = 'Running', // 运行中 Succeeded = 'Succeeded', // 成功 - Pending = 'Pending', // 启动中 Failed = 'Failed', // 失败 Error = 'Error', // 错误 Terminated = 'Terminated', // 终止 diff --git a/react-ui/src/global.less b/react-ui/src/global.less index 62a7a90a..fbbfa34d 100644 --- a/react-ui/src/global.less +++ b/react-ui/src/global.less @@ -43,7 +43,7 @@ body { } .ant-pro-layout .ant-pro-sider-menu { - padding-top: 40px; + padding-top: 15px; } .ant-pro-global-header-logo-mix { padding-left: 12px; diff --git a/react-ui/src/iconfont/iconfont.js b/react-ui/src/iconfont/iconfont.js index 0e40d9d6..65e19f39 100644 --- a/react-ui/src/iconfont/iconfont.js +++ b/react-ui/src/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4511447='',(t=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}})(window); \ No newline at end of file +window._iconfont_svg_string_4511447='',(t=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}})(window); \ No newline at end of file diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx index 4d4d70d3..1dc2c3cc 100644 --- a/react-ui/src/pages/AutoML/Create/index.tsx +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -104,10 +104,15 @@ function CreateAutoML() { const exclude_classifier = formData['exclude_classifier']?.join(','); const exclude_feature_preprocessor = formData['exclude_feature_preprocessor']?.join(','); const exclude_regressor = formData['exclude_regressor']?.join(','); - const metrics = formData['metrics']?.reduce((acc, cur) => { - acc[cur.name] = cur.value; - return acc; - }, {} as Record); + const formMetrics = formData['metrics']; + const metrics = + formMetrics && Array.isArray(formMetrics) && formMetrics.length > 0 + ? formMetrics.reduce((acc, cur) => { + acc[cur.name] = cur.value; + return acc; + }, {} as Record) + : undefined; + const target_columns = trimCharacter(formData['target_columns'], ','); // 根据后台要求,修改表单数据 @@ -174,6 +179,16 @@ function CreateAutoML() { shuffle: false, ensemble_class: AutoMLEnsembleClass.Default, greater_is_better: true, + ensemble_size: 50, + ensemble_nbest: 50, + max_models_on_disc: 50, + memory_limit: 3072, + per_run_time_limit: 600, + time_left_for_this_task: 3600, + resampling_strategy: 'holdout', + test_size: 0.25, + train_size: 0.67, + seed: 1, }} > diff --git a/react-ui/src/pages/AutoML/Info/index.tsx b/react-ui/src/pages/AutoML/Info/index.tsx index caa2cc55..cc5247e2 100644 --- a/react-ui/src/pages/AutoML/Info/index.tsx +++ b/react-ui/src/pages/AutoML/Info/index.tsx @@ -4,16 +4,14 @@ * @Description: 自主机器学习详情 */ import KFIcon from '@/components/KFIcon'; +import PageTitle from '@/components/PageTitle'; import { CommonTabKeys } from '@/enums'; import { getAutoMLInfoReq } from '@/services/autoML'; -import themes from '@/styles/theme.less'; import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import { useParams } from '@umijs/max'; -import { Tabs } from 'antd'; import { useEffect, useState } from 'react'; import AutoMLBasic from '../components/AutoMLBasic'; -import AutoMLTable from '../components/AutoMLTable'; import { AutoMLData } from '../types'; import styles from './index.less'; @@ -52,19 +50,10 @@ function AutoMLInfo() { return (
-
- -
+
- {activeTab === CommonTabKeys.Public && } - {activeTab === CommonTabKeys.Private && } +
- {activeTab === CommonTabKeys.Private && ( -
- - Trial是一次独立的尝试,他会使用某组超参来运行 -
- )}
); } diff --git a/react-ui/src/pages/AutoML/Instance/index.less b/react-ui/src/pages/AutoML/Instance/index.less index 0c62da46..889faeb5 100644 --- a/react-ui/src/pages/AutoML/Instance/index.less +++ b/react-ui/src/pages/AutoML/Instance/index.less @@ -2,16 +2,41 @@ height: 100%; &__tabs { - height: 50px; - padding-left: 25px; - background-image: url(@/assets/img/page-title-bg.png); - background-repeat: no-repeat; - background-position: top center; - background-size: 100% 100%; + height: 100%; + :global { + .ant-tabs-nav-list { + width: 100%; + height: 50px; + padding-left: 15px; + background-image: url(@/assets/img/page-title-bg.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + } + + .ant-tabs-content-holder { + height: calc(100% - 50px); + .ant-tabs-content { + height: 100%; + .ant-tabs-tabpane { + height: 100%; + } + } + } + } + } + + &__basic { + height: calc(100% - 10px); + margin-top: 10px; } - &__content { - height: calc(100% - 60px); + &__log { + height: calc(100% - 10px); margin-top: 10px; + padding: 20px calc(@content-padding - 8px); + overflow-y: visible; + background-color: white; + border-radius: 10px; } } diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index a8707c34..376961bb 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -2,12 +2,13 @@ import KFIcon from '@/components/KFIcon'; import { AutoMLTaskType, ExperimentStatus } from '@/enums'; import LogList from '@/pages/Experiment/components/LogList'; import { getExperimentInsReq } from '@/services/autoML'; +import { NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import { useParams } from '@umijs/max'; import { Tabs } from 'antd'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import AutoMLBasic from '../components/AutoMLBasic'; import ExperimentHistory from '../components/ExperimentHistory'; import ExperimentResult from '../components/ExperimentResult'; @@ -28,11 +29,15 @@ function AutoMLInstance() { const params = useParams(); // const autoMLId = safeInvoke(Number)(params.autoMLId); const instanceId = safeInvoke(Number)(params.id); + const evtSourceRef = useRef(null); useEffect(() => { if (instanceId) { getExperimentInsInfo(); } + return () => { + closeSSE(); + }; }, []); // 获取实验实例详情 @@ -40,7 +45,7 @@ function AutoMLInstance() { const [res] = await to(getExperimentInsReq(instanceId)); if (res && res.data) { const info = res.data as AutoMLInstanceData; - const { param, node_status } = info; + const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; // 解析配置参数 const paramJson = parseJsonText(param); if (paramJson) { @@ -52,65 +57,142 @@ function AutoMLInstance() { Object.keys(nodeStatusJson).forEach((key) => { if (key.startsWith('auto-ml')) { const value = nodeStatusJson[key]; - value.nodeId = key; info.nodeStatus = value; } }); } setInstanceInfo(info); + // 运行中或者等待中,开启 SSE + if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { + setupSSE(argo_ins_name, argo_ins_ns); + } } }; - const tabItems = [ + const setupSSE = (name: string, namespace: string) => { + let { origin } = location; + if (process.env.NODE_ENV === 'development') { + origin = 'http://172.20.32.181:31213'; + } + const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); + const evtSource = new EventSource( + `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, + { withCredentials: false }, + ); + evtSource.onmessage = (event) => { + const data = event?.data; + if (!data) { + return; + } + const dataJson = parseJsonText(data); + if (dataJson) { + const nodes = dataJson?.result?.object?.status?.nodes; + if (nodes) { + const statusData = Object.values(nodes).find((node: any) => + node.displayName.startsWith('auto-ml'), + ) as NodeStatus; + if (statusData) { + setInstanceInfo((prev) => ({ + ...(prev as AutoMLInstanceData), + nodeStatus: statusData, + })); + + // 实验结束,关闭 SSE + if ( + statusData.phase !== ExperimentStatus.Pending && + statusData.phase !== ExperimentStatus.Running + ) { + closeSSE(); + getExperimentInsInfo(); + } + } + } + } + }; + evtSource.onerror = (error) => { + console.error('SSE error: ', error); + }; + + evtSourceRef.current = evtSource; + }; + + const closeSSE = () => { + if (evtSourceRef.current) { + evtSourceRef.current.close(); + evtSourceRef.current = null; + } + }; + + const basicTabItems = [ { key: TabKeys.Params, - label: '参数信息', + label: '基本信息', icon: , + children: ( + + ), }, { key: TabKeys.Log, label: '日志', - icon: , + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), }, + ]; + + const resultTabItems = [ { key: TabKeys.Result, label: '实验结果', - icon: , + icon: , + children: ( + + ), }, { key: TabKeys.History, - label: '运行历史', + label: 'Trial列表', icon: , + children: ( + + ), }, ]; + const tabItems = + instanceInfo?.status === ExperimentStatus.Succeeded + ? [...basicTabItems, ...resultTabItems] + : basicTabItems; + return (
-
- -
-
- {activeTab === TabKeys.Params && } - {activeTab === TabKeys.Log && instanceInfo && instanceInfo.nodeStatus && ( - - )} - {activeTab === TabKeys.Result && ( - - )} - {activeTab === TabKeys.History && ( - - )} -
+
); } diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index 02a5e69b..b6f214ae 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -1,6 +1,11 @@ import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; import { AutoMLData } from '@/pages/AutoML/types'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { type NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; +import { elapsedTime } from '@/utils/date'; +import { Flex } from 'antd'; +import classNames from 'classnames'; import { useMemo } from 'react'; import ConfigInfo, { formatBoolean, @@ -38,10 +43,12 @@ const formatMetricsWeight = (value: string) => { type AutoMLBasicProps = { info?: AutoMLData; - hasBasicInfo?: boolean; + className?: string; + isInstance?: boolean; + runStatus?: NodeStatus; }; -function AutoMLBasic({ info, hasBasicInfo = true }: AutoMLBasicProps) { +function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { const basicDatas: BasicInfoData[] = useMemo(() => { if (!info) { return []; @@ -142,7 +149,7 @@ function AutoMLBasic({ info, hasBasicInfo = true }: AutoMLBasicProps) { ellipsis: true, }, { - label: '时间限制(秒)', + label: '单次时间限制(秒)', value: info.per_run_time_limit, ellipsis: true, }, @@ -227,9 +234,59 @@ function AutoMLBasic({ info, hasBasicInfo = true }: AutoMLBasicProps) { ]; }, [info]); + const instanceDatas = useMemo(() => { + if (!runStatus) { + return []; + } + + return [ + { + label: '启动时间', + value: formatDate(runStatus.startedAt), + ellipsis: true, + }, + { + label: '执行时长', + value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), + ellipsis: true, + }, + { + label: '状态', + value: ( + + +
+ {experimentStatusInfo[runStatus?.phase]?.label} +
+
+ ), + ellipsis: true, + }, + ]; + }, [runStatus]); + return ( -
- {hasBasicInfo && ( +
+ {isInstance && runStatus ? ( + + ) : ( @@ -325,7 +325,7 @@ function ExecuteConfig() { @@ -339,13 +339,56 @@ function ExecuteConfig() { + + + + + + + + + + + + - - - - - - - - - - - - {/* +
{result}
- {images.map((item, index) => ( - - ))} + +
+ {images.map((item, index) => ( + + ))} +
); } diff --git a/react-ui/src/pages/AutoML/types.ts b/react-ui/src/pages/AutoML/types.ts index f068d168..339a9e51 100644 --- a/react-ui/src/pages/AutoML/types.ts +++ b/react-ui/src/pages/AutoML/types.ts @@ -1,4 +1,5 @@ import { type ParameterInputObject } from '@/components/ResourceSelect'; +import { type NodeStatus } from '@/types'; // 操作类型 export enum OperationType { @@ -80,5 +81,5 @@ export type AutoMLInstanceData = { create_time: string; update_time: string; finish_time: string; - nodeStatus?: { [key: string]: string }; + nodeStatus?: NodeStatus; }; diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less index 1d5bdc34..83a91180 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less @@ -39,4 +39,10 @@ margin-right: 6px; border-radius: 50%; } + + &__log { + height: 100%; + padding: 8px; + background: white; + } } diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx index 48cd0064..26da1c07 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx @@ -48,14 +48,16 @@ const ExperimentDrawer = ({ key: '1', label: '日志详情', children: ( - +
+ +
), icon: , }, diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 27f3354c..75d914f5 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -90,7 +90,7 @@ function LogGroup({ start_time: startTime, }; const res = await getExperimentPodsLog(params); - const { log_detail } = res.data; + const { log_detail } = res.data || {}; if (log_detail) { setLogList((oldList) => oldList.concat(log_detail)); diff --git a/react-ui/src/pages/Experiment/components/LogList/index.less b/react-ui/src/pages/Experiment/components/LogList/index.less index 3909c8de..18fcb21f 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.less +++ b/react-ui/src/pages/Experiment/components/LogList/index.less @@ -1,7 +1,7 @@ .log-list { height: 100%; - padding: 8px; overflow-y: auto; + background: #19253b; &__empty { padding: 15px; @@ -12,4 +12,8 @@ word-break: break-all; background: #19253b; } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.5); + } } diff --git a/react-ui/src/types.ts b/react-ui/src/types.ts index a7df0561..45884c02 100644 --- a/react-ui/src/types.ts +++ b/react-ui/src/types.ts @@ -114,3 +114,13 @@ export type ComputingResource = { standard: string; create_by: string; }; + +// 实验运行节点状态 +export type NodeStatus = { + id: string; // workflow Id + displayName: string; + name: string; + phase: ExperimentStatus; + startedAt: string; + finishedAt: string; +};