diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..d579dd5a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,45 @@ +{ + "name": "ci4sManagement-cloud", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "clipboard": "~2.0.11" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..32560620 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "clipboard": "~2.0.11" + } +} diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index 0639985b..cf4e1aaf 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -145,6 +145,37 @@ export default [ }, ], }, + { + name: '自动机器学习', + path: 'automl', + routes: [ + { + name: '自动机器学习', + path: '', + component: './AutoML/List/index', + }, + { + name: '自动机器学习详情', + path: 'info/:id', + component: './AutoML/Info/index', + }, + { + name: '创建实验', + path: 'create', + component: './AutoML/Create/index', + }, + { + name: '编辑实验', + path: 'edit/:id', + component: './AutoML/Create/index', + }, + { + name: '实验实例', + path: 'instance/:autoMLId/:id', + component: './AutoML/Instance/index', + }, + ], + }, ], }, { diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index c018253e..2f757e15 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -20,6 +20,7 @@ import './styles/menu.less'; export { requestConfig as request } from './requestConfig'; // const isDev = process.env.NODE_ENV === 'development'; import { type GlobalInitialState } from '@/types'; +import '@/utils/clipboard'; import { menuItemRender } from '@/utils/menuRender'; import ErrorBoundary from './components/ErrorBoundary'; import { needAuth } from './utils'; @@ -49,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 { @@ -162,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); } @@ -173,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/dataset-config-icon.png b/react-ui/src/assets/img/dataset-config-icon.png new file mode 100644 index 00000000..1afc4f72 Binary files /dev/null and b/react-ui/src/assets/img/dataset-config-icon.png differ diff --git a/react-ui/src/assets/img/editor-parameter.png b/react-ui/src/assets/img/editor-parameter.png index b5fd9f41..06fb4f2b 100644 Binary files a/react-ui/src/assets/img/editor-parameter.png and b/react-ui/src/assets/img/editor-parameter.png differ diff --git a/react-ui/src/assets/img/mirror-basic.png b/react-ui/src/assets/img/mirror-basic.png index d9ca34a9..f022b2ac 100644 Binary files a/react-ui/src/assets/img/mirror-basic.png and b/react-ui/src/assets/img/mirror-basic.png differ diff --git a/react-ui/src/assets/img/model-deployment.png b/react-ui/src/assets/img/model-deployment.png index bf8511c8..59a32d5f 100644 Binary files a/react-ui/src/assets/img/model-deployment.png and b/react-ui/src/assets/img/model-deployment.png differ 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/assets/img/search-config-icon.png b/react-ui/src/assets/img/search-config-icon.png new file mode 100644 index 00000000..6cc8a78b Binary files /dev/null and b/react-ui/src/assets/img/search-config-icon.png differ diff --git a/react-ui/src/assets/img/trial-config-icon.png b/react-ui/src/assets/img/trial-config-icon.png new file mode 100644 index 00000000..69408563 Binary files /dev/null and b/react-ui/src/assets/img/trial-config-icon.png differ diff --git a/react-ui/src/components/BasicInfo/components.tsx b/react-ui/src/components/BasicInfo/components.tsx new file mode 100644 index 00000000..b8932a25 --- /dev/null +++ b/react-ui/src/components/BasicInfo/components.tsx @@ -0,0 +1,113 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-29 09:27:19 + * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 + */ + +import { Link } from '@umijs/max'; +import { Typography } from 'antd'; +import React from 'react'; +import { type BasicInfoData, type BasicInfoLink } from './types'; + +type BasicInfoItemProps = { + data: BasicInfoData; + labelWidth: number; + classPrefix: string; +}; + +export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { + const { label, value, format, ellipsis } = data; + const formatValue = format ? format(value) : value; + const myClassName = `${classPrefix}__item`; + let valueComponent = undefined; + if (Array.isArray(formatValue)) { + valueComponent = ( +
+ {formatValue.map((item: BasicInfoLink) => ( + + ))} +
+ ); + } else if (React.isValidElement(formatValue)) { + // 这个判断必须在下面的判断之前 + valueComponent = ( + + ); + } else if (typeof formatValue === 'object' && formatValue) { + valueComponent = ( + + ); + } else { + valueComponent = ( + + ); + } + return ( +
+
+ {label} +
+ {valueComponent} +
+ ); +} + +type BasicInfoItemValueProps = { + ellipsis?: boolean; + classPrefix: string; + value: string | React.ReactNode; + link?: string; + url?: string; +}; + +export function BasicInfoItemValue({ + value, + link, + url, + ellipsis, + classPrefix, +}: BasicInfoItemValueProps) { + const myClassName = `${classPrefix}__item__value`; + let component = undefined; + if (url && value) { + component = ( + + {value} + + ); + } else if (link && value) { + component = ( + + {value} + + ); + } else if (React.isValidElement(value)) { + return value; + } else { + component = {value ?? '--'}; + } + + return ( +
+ + {component} + +
+ ); +} diff --git a/react-ui/src/components/BasicInfo/format.ts b/react-ui/src/components/BasicInfo/format.ts new file mode 100644 index 00000000..0dae2422 --- /dev/null +++ b/react-ui/src/components/BasicInfo/format.ts @@ -0,0 +1,48 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-29 09:27:19 + * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的常用转化格式 + */ + +// 格式化日期 +export { formatDate } from '@/utils/date'; + +/** + * 格式化字符串数组 + * @param value - 字符串数组 + * @returns 逗号分隔的字符串 + */ +export const formatList = (value: string[] | null | undefined): string => { + if ( + value === undefined || + value === null || + Array.isArray(value) === false || + value.length === 0 + ) { + return '--'; + } + return value.join(','); +}; + +/** + * 格式化布尔值 + * @param value - 布尔值 + * @returns "是" 或 "否" + */ +export const formatBoolean = (value: boolean): string => { + return value ? '是' : '否'; +}; + +type FormatEnum = (value: string | number) => string; + +/** + * 格式化枚举 + * @param options - 枚举选项 + * @returns 格式化枚举函数 + */ +export const formatEnum = (options: { value: string | number; label: string }[]): FormatEnum => { + return (value: string | number) => { + const option = options.find((item) => item.value === value); + return option ? option.label : '--'; + }; +}; diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index bd07db34..1336d0b6 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -1,20 +1,10 @@ -import { Link } from '@umijs/max'; -import { Typography } from 'antd'; import classNames from 'classnames'; +import React from 'react'; +import { BasicInfoItem } from './components'; import './index.less'; - -export type BasicInfoLink = { - value: string; - link?: string; - url?: string; -}; - -export type BasicInfoData = { - label: string; - value?: any; - ellipsis?: boolean; - format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; -}; +import type { BasicInfoData, BasicInfoLink } from './types'; +export * from './format'; +export type { BasicInfoData, BasicInfoLink }; type BasicInfoProps = { datas: BasicInfoData[]; @@ -23,17 +13,6 @@ type BasicInfoProps = { labelWidth: number; }; -type BasicInfoItemProps = { - data: BasicInfoData; - labelWidth: number; - classPrefix: string; -}; - -type BasicInfoItemValueProps = BasicInfoLink & { - ellipsis?: boolean; - classPrefix: string; -}; - export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { return (
@@ -48,82 +27,3 @@ export default function BasicInfo({ datas, className, style, labelWidth }: Basic
); } - -export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { - const { label, value, format, ellipsis } = data; - const formatValue = format ? format(value) : value; - const myClassName = `${classPrefix}__item`; - let valueComponent = undefined; - if (Array.isArray(formatValue)) { - valueComponent = ( -
- {formatValue.map((item: BasicInfoLink) => ( - - ))} -
- ); - } else if (typeof formatValue === 'object' && formatValue) { - valueComponent = ( - - ); - } else { - valueComponent = ( - - ); - } - return ( -
-
- {label} -
- {valueComponent} -
- ); -} - -export function BasicInfoItemValue({ - value, - link, - url, - ellipsis, - classPrefix, -}: BasicInfoItemValueProps) { - const myClassName = `${classPrefix}__item__value`; - let component = undefined; - if (url && value) { - component = ( - - {value} - - ); - } else if (link && value) { - component = ( - - {value} - - ); - } else { - component = {value ?? '--'}; - } - - return ( -
- - {component} - -
- ); -} diff --git a/react-ui/src/components/BasicInfo/types.ts b/react-ui/src/components/BasicInfo/types.ts new file mode 100644 index 00000000..a7c10ba0 --- /dev/null +++ b/react-ui/src/components/BasicInfo/types.ts @@ -0,0 +1,14 @@ +// 基础信息 +export type BasicInfoData = { + label: string; + value?: any; + ellipsis?: boolean; + format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; +}; + +// 值为链接的类型 +export type BasicInfoLink = { + value: string; + link?: string; + url?: string; +}; diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index df167ae2..104bc2bb 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,6 +1,8 @@ import classNames from 'classnames'; -import { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo'; +import { BasicInfoItem } from '../BasicInfo/components'; +import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; import './index.less'; +export * from '../BasicInfo/format'; export type { BasicInfoData, BasicInfoLink }; type BasicTableInfoProps = { diff --git a/react-ui/src/components/KFBreadcrumb/index.tsx b/react-ui/src/components/KFBreadcrumb/index.tsx index c6322025..dc87efc2 100644 --- a/react-ui/src/components/KFBreadcrumb/index.tsx +++ b/react-ui/src/components/KFBreadcrumb/index.tsx @@ -1,3 +1,9 @@ +/* + * @Author: 赵伟 + * @Date: 2024-09-02 08:42:57 + * @Description: 自定义面包屑,暂时不用,使用了 ProBreadcrumb + */ + import { Breadcrumb, type BreadcrumbProps } from 'antd'; import { Link, matchPath, useLocation } from 'umi'; // import routes from '../../../config/config'; // 导入你的路由配置 diff --git a/react-ui/src/components/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx index e50dabec..d84257a7 100644 --- a/react-ui/src/components/KFIcon/index.tsx +++ b/react-ui/src/components/KFIcon/index.tsx @@ -21,13 +21,13 @@ interface KFIconProps extends IconFontProps { className?: string; } -function KFIcon({ type, font = 15, color = '', style = {}, className }: KFIconProps) { +function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { const iconStyle = { ...style, fontSize: font, color, }; - return ; + return ; } export default KFIcon; diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index e489515c..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', // 终止 @@ -71,6 +71,7 @@ export enum DevEditorStatus { Unknown = 'Unknown', // 未启动 } +// 服务类型 export enum ServiceType { Video = 'video', Image = 'image', @@ -84,3 +85,36 @@ export const serviceTypeOptions = [ { label: '音频', value: ServiceType.Audio }, { label: '文本', value: ServiceType.Text }, ]; + +// 自动化任务类型 +export enum AutoMLTaskType { + Classification = 'classification', + Regression = 'regression', +} + +export const autoMLTaskTypeOptions = [ + { label: '分类', value: AutoMLTaskType.Classification }, + { label: '回归', value: AutoMLTaskType.Regression }, +]; + +// 自动化任务集成策略 +export enum AutoMLEnsembleClass { + Default = 'default', + SingleBest = 'SingleBest', +} + +export const autoMLEnsembleClassOptions = [ + { label: '集成模型', value: AutoMLEnsembleClass.Default }, + { label: '单一最佳模型', value: AutoMLEnsembleClass.SingleBest }, +]; + +// 自动化任务重采样策略 +export enum AutoMLResamplingStrategy { + Holdout = 'holdout', + CrossValid = 'crossValid', +} + +export const autoMLResamplingStrategyOptions = [ + { label: 'holdout', value: AutoMLResamplingStrategy.Holdout }, + { label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid }, +]; 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 c9da6580..7097beaf 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,z,v,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):(z=function(){document.removeEventListener("DOMContentLoaded",z,!1),l()},document.addEventListener("DOMContentLoaded",z,!1)):document.attachEvent&&(v=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,v())}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/overrides.less b/react-ui/src/overrides.less index f676890e..e129b4a7 100644 --- a/react-ui/src/overrides.less +++ b/react-ui/src/overrides.less @@ -204,6 +204,14 @@ margin-inline-start: 12px; } +.ant-pro-layout .ant-pro-sider-logo-collapsed { + padding: 16px 12px; +} + +.ant-pro-base-menu-inline .ant-pro-base-menu-inline-menu-item { + transition: padding 0.1s !important; +} + // PageContainer 里的 ProTable 只滑动内容区域 .system-menu.ant-pro-page-container { height: 100%; diff --git a/react-ui/src/pages/Application/index.tsx b/react-ui/src/pages/Application/index.tsx index 65a1efae..20147094 100644 --- a/react-ui/src/pages/Application/index.tsx +++ b/react-ui/src/pages/Application/index.tsx @@ -1,3 +1,9 @@ +/* + * @Author: 赵伟 + * @Date: 2024-09-02 08:42:57 + * @Description: 应用开发 + */ + import IframePage, { IframePageType } from '@/components/IFramePage'; function Application() { diff --git a/react-ui/src/pages/Authorize/index.tsx b/react-ui/src/pages/Authorize/index.tsx index f3624f32..e42a0f1b 100644 --- a/react-ui/src/pages/Authorize/index.tsx +++ b/react-ui/src/pages/Authorize/index.tsx @@ -22,7 +22,6 @@ function Authorize() { code, }; const [res] = await to(loginByOauth2Req(params)); - debugger; if (res && res.data) { const { access_token, expires_in } = res.data; setSessionToken(access_token, access_token, expires_in); diff --git a/react-ui/src/pages/AutoML/Create/index.less b/react-ui/src/pages/AutoML/Create/index.less new file mode 100644 index 00000000..f8d15d2e --- /dev/null +++ b/react-ui/src/pages/AutoML/Create/index.less @@ -0,0 +1,55 @@ +.create-automl { + height: 100%; + + &__content { + height: calc(100% - 60px); + margin-top: 10px; + padding: 30px 30px 10px; + overflow: auto; + color: @text-color; + font-size: @font-size-content; + background-color: white; + border-radius: 10px; + + &__type { + color: @text-color; + font-size: @font-size-input-lg; + } + + :global { + .ant-input-number { + width: 100%; + } + + .ant-form-item { + margin-bottom: 20px; + } + + .image-url { + margin-top: -15px; + .ant-form-item-label > label::after { + content: ''; + } + } + + .ant-btn-variant-text:disabled { + color: rgba(0, 0, 0, 0.25); + } + + .ant-btn-variant-text { + color: #565658; + } + + .ant-btn.ant-btn-icon-only .anticon { + font-size: 20px; + } + + .anticon-question-circle { + margin-top: -12px; + margin-left: 1px !important; + color: @text-color-tertiary !important; + font-size: 12px !important; + } + } + } +} diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx new file mode 100644 index 00000000..1dc2c3cc --- /dev/null +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -0,0 +1,219 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 创建服务版本 + */ +import PageTitle from '@/components/PageTitle'; + +import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; +import { addAutoMLReq, getAutoMLInfoReq, updateAutoMLReq } from '@/services/autoML'; +import { convertEmptyStringToUndefined, parseJsonText, trimCharacter } from '@/utils'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import SessionStorage from '@/utils/sessionStorage'; +import { useNavigate, useParams } from '@umijs/max'; +import { App, Button, Form } from 'antd'; +import { omit } from 'lodash'; +import { useEffect } from 'react'; +import BasicConfig from '../components/CreateForm/BasicConfig'; +import DatasetConfig from '../components/CreateForm/DatasetConfig'; +import ExecuteConfig from '../components/CreateForm/ExecuteConfig'; +import TrialConfig from '../components/CreateForm/TrialConfig'; +import { AutoMLData, FormData } from '../types'; +import styles from './index.less'; + +function CreateAutoML() { + const navigate = useNavigate(); + const [form] = Form.useForm(); + const { message } = App.useApp(); + const params = useParams(); + const id = safeInvoke(Number)(params.id); + + useEffect(() => { + // 复制和新建 + const recordId = SessionStorage.getItem(SessionStorage.autoMLRecordIDKey); + if (recordId && !Number.isNaN(Number(recordId))) { + getAutoMLInfo(Number(recordId), true); + } + return () => { + SessionStorage.removeItem(SessionStorage.autoMLRecordIDKey); + }; + }, []); + + useEffect(() => { + // 编辑 + if (id && !Number.isNaN(id)) { + getAutoMLInfo(id, false); + } + }, [id]); + + // 获取服务详情 + const getAutoMLInfo = async (id: number, isCopy = false) => { + const [res] = await to(getAutoMLInfoReq({ id })); + if (res && res.data) { + const autoMLInfo: AutoMLData = res.data; + const { + include_classifier: include_classifier_str, + include_feature_preprocessor: include_feature_preprocessor_str, + include_regressor: include_regressor_str, + exclude_classifier: exclude_classifier_str, + exclude_feature_preprocessor: exclude_feature_preprocessor_str, + exclude_regressor: exclude_regressor_str, + metrics: metrics_str, + ml_name: ml_name_str, + ...rest + } = autoMLInfo; + const include_classifier = include_classifier_str?.split(',').filter(Boolean); + const include_feature_preprocessor = include_feature_preprocessor_str + ?.split(',') + .filter(Boolean); + const include_regressor = include_regressor_str?.split(',').filter(Boolean); + const exclude_classifier = exclude_classifier_str?.split(',').filter(Boolean); + const exclude_feature_preprocessor = exclude_feature_preprocessor_str + ?.split(',') + .filter(Boolean); + const exclude_regressor = exclude_regressor_str?.split(',').filter(Boolean); + const metricsObj = safeInvoke(parseJsonText)(metrics_str) ?? {}; + const metrics = Object.entries(metricsObj).map(([key, value]) => ({ + name: key, + value, + })); + const ml_name = isCopy ? `${ml_name_str}-copy` : ml_name_str; + + const formData = { + ...rest, + include_classifier, + include_feature_preprocessor, + include_regressor, + exclude_classifier, + exclude_feature_preprocessor, + exclude_regressor, + metrics, + ml_name, + }; + + form.setFieldsValue(formData); + } + }; + + // 创建、更新、复制实验 + const createExperiment = async (formData: FormData) => { + const include_classifier = formData['include_classifier']?.join(','); + const include_feature_preprocessor = formData['include_feature_preprocessor']?.join(','); + const include_regressor = formData['include_regressor']?.join(','); + const exclude_classifier = formData['exclude_classifier']?.join(','); + const exclude_feature_preprocessor = formData['exclude_feature_preprocessor']?.join(','); + const exclude_regressor = formData['exclude_regressor']?.join(','); + 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'], ','); + + // 根据后台要求,修改表单数据 + const object = { + ...omit(formData), + include_classifier: convertEmptyStringToUndefined(include_classifier), + include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), + include_regressor: convertEmptyStringToUndefined(include_regressor), + exclude_classifier: convertEmptyStringToUndefined(exclude_classifier), + exclude_feature_preprocessor: convertEmptyStringToUndefined(exclude_feature_preprocessor), + exclude_regressor: convertEmptyStringToUndefined(exclude_regressor), + metrics: metrics ? JSON.stringify(metrics) : undefined, + target_columns, + }; + + const params = id + ? { + id: id, + ...object, + } + : object; + + const request = id ? updateAutoMLReq : addAutoMLReq; + const [res] = await to(request(params)); + if (res) { + message.success('操作成功'); + navigate(-1); + } + }; + + // 提交 + const handleSubmit = (values: FormData) => { + createExperiment(values); + }; + + // 取消 + const cancel = () => { + navigate(-1); + }; + + let buttonText = '新建'; + let title = '新增实验'; + if (id) { + title = '编辑实验'; + buttonText = '更新'; + } + + return ( +
+ +
+
+
+ + + + + + + + + + +
+
+
+ ); +} + +export default CreateAutoML; diff --git a/react-ui/src/pages/AutoML/Info/index.less b/react-ui/src/pages/AutoML/Info/index.less new file mode 100644 index 00000000..e27756ef --- /dev/null +++ b/react-ui/src/pages/AutoML/Info/index.less @@ -0,0 +1,40 @@ +.auto-ml-info { + position: relative; + 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%; + } + + &__content { + height: calc(100% - 60px); + margin-top: 10px; + } + + &__tips { + position: absolute; + top: 11px; + left: 256px; + padding: 3px 12px; + color: #565658; + font-size: @font-size-content; + background: .addAlpha(@primary-color, 0.09) []; + border-radius: 4px; + + &::before { + position: absolute; + top: 10px; + left: -6px; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-right: 6px solid .addAlpha(@primary-color, 0.09) []; + border-bottom: 4px solid transparent; + content: ''; + } + } +} diff --git a/react-ui/src/pages/AutoML/Info/index.tsx b/react-ui/src/pages/AutoML/Info/index.tsx new file mode 100644 index 00000000..cc5247e2 --- /dev/null +++ b/react-ui/src/pages/AutoML/Info/index.tsx @@ -0,0 +1,61 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 自主机器学习详情 + */ +import KFIcon from '@/components/KFIcon'; +import PageTitle from '@/components/PageTitle'; +import { CommonTabKeys } from '@/enums'; +import { getAutoMLInfoReq } from '@/services/autoML'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useParams } from '@umijs/max'; +import { useEffect, useState } from 'react'; +import AutoMLBasic from '../components/AutoMLBasic'; +import { AutoMLData } from '../types'; +import styles from './index.less'; + +function AutoMLInfo() { + const [activeTab, setActiveTab] = useState(CommonTabKeys.Public); + const params = useParams(); + const autoMLId = safeInvoke(Number)(params.id); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); + + const tabItems = [ + { + key: CommonTabKeys.Public, + label: '基本信息', + icon: , + }, + { + key: CommonTabKeys.Private, + label: 'Trial列表', + icon: , + }, + ]; + + useEffect(() => { + if (autoMLId) { + getAutoMLInfo(); + } + }, []); + + // 获取自动机器学习详情 + const getAutoMLInfo = async () => { + const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); + if (res && res.data) { + setAutoMLInfo(res.data); + } + }; + + return ( +
+ +
+ +
+
+ ); +} + +export default AutoMLInfo; diff --git a/react-ui/src/pages/AutoML/Instance/index.less b/react-ui/src/pages/AutoML/Instance/index.less new file mode 100644 index 00000000..889faeb5 --- /dev/null +++ b/react-ui/src/pages/AutoML/Instance/index.less @@ -0,0 +1,42 @@ +.auto-ml-instance { + height: 100%; + + &__tabs { + 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; + } + + &__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 new file mode 100644 index 00000000..7b416de1 --- /dev/null +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -0,0 +1,200 @@ +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, useRef, useState } from 'react'; +import AutoMLBasic from '../components/AutoMLBasic'; +import ExperimentHistory from '../components/ExperimentHistory'; +import ExperimentResult from '../components/ExperimentResult'; +import { AutoMLData, AutoMLInstanceData } from '../types'; +import styles from './index.less'; + +enum TabKeys { + Params = 'params', + Log = 'log', + Result = 'result', + History = 'history', +} + +function AutoMLInstance() { + const [activeTab, setActiveTab] = useState(TabKeys.Params); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); + const [instanceInfo, setInstanceInfo] = useState(undefined); + 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(); + }; + }, []); + + // 获取实验实例详情 + const getExperimentInsInfo = async () => { + const [res] = await to(getExperimentInsReq(instanceId)); + if (res && res.data) { + const info = res.data as AutoMLInstanceData; + const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; + // 解析配置参数 + const paramJson = parseJsonText(param); + if (paramJson) { + setAutoMLInfo(paramJson); + } + // 进行节点状态 + const nodeStatusJson = parseJsonText(node_status); + if (nodeStatusJson) { + Object.keys(nodeStatusJson).forEach((key) => { + if (key.startsWith('auto-ml')) { + const value = nodeStatusJson[key]; + info.nodeStatus = value; + } + }); + } + setInstanceInfo(info); + // 运行中或者等待中,开启 SSE + if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { + setupSSE(argo_ins_name, argo_ins_ns); + } + } + }; + + 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: '基本信息', + icon: , + children: ( + + ), + }, + { + key: TabKeys.Log, + label: '日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + ]; + + const resultTabItems = [ + { + key: TabKeys.Result, + label: '实验结果', + icon: , + children: ( + + ), + }, + { + key: TabKeys.History, + label: 'Trial 列表', + icon: , + children: ( + + ), + }, + ]; + + const tabItems = + instanceInfo?.status === ExperimentStatus.Succeeded + ? [...basicTabItems, ...resultTabItems] + : basicTabItems; + + return ( +
+ +
+ ); +} + +export default AutoMLInstance; diff --git a/react-ui/src/pages/AutoML/List/index.less b/react-ui/src/pages/AutoML/List/index.less new file mode 100644 index 00000000..735e3442 --- /dev/null +++ b/react-ui/src/pages/AutoML/List/index.less @@ -0,0 +1,20 @@ +.auto-ml-list { + height: 100%; + &__content { + height: calc(100% - 60px); + margin-top: 10px; + padding: 20px @content-padding 0; + background-color: white; + border-radius: 10px; + + &__filter { + display: flex; + align-items: center; + } + + &__table { + height: calc(100% - 32px - 28px); + margin-top: 28px; + } + } +} diff --git a/react-ui/src/pages/AutoML/List/index.tsx b/react-ui/src/pages/AutoML/List/index.tsx new file mode 100644 index 00000000..aa269e50 --- /dev/null +++ b/react-ui/src/pages/AutoML/List/index.tsx @@ -0,0 +1,414 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 自主机器学习列表 + */ +import KFIcon from '@/components/KFIcon'; +import PageTitle from '@/components/PageTitle'; +import { ExperimentStatus } from '@/enums'; +import { useCacheState } from '@/hooks/pageCacheState'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { + deleteAutoMLReq, + getAutoMLListReq, + getExperimentInsListReq, + runAutoMLReq, +} from '@/services/autoML'; +import themes from '@/styles/theme.less'; +import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; +import { to } from '@/utils/promise'; +import SessionStorage from '@/utils/sessionStorage'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { modalConfirm } from '@/utils/ui'; +import { useNavigate } from '@umijs/max'; +import { + App, + Button, + ConfigProvider, + Input, + Table, + type TablePaginationConfig, + type TableProps, +} from 'antd'; +import { type SearchProps } from 'antd/es/input'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +import ExperimentInstance from '../components/ExperimentInstance'; +import { AutoMLData } from '../types'; +import styles from './index.less'; + +function AutoMLList() { + const navigate = useNavigate(); + const { message } = App.useApp(); + const [cacheState, setCacheState] = useCacheState(); + const [searchText, setSearchText] = useState(cacheState?.searchText); + const [inputText, setInputText] = useState(cacheState?.searchText); + const [tableData, setTableData] = useState([]); + const [total, setTotal] = useState(0); + const [experimentInsList, setExperimentInsList] = useState([]); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); + const [experimentInsTotal, setExperimentInsTotal] = useState(0); + const [pagination, setPagination] = useState( + cacheState?.pagination ?? { + current: 1, + pageSize: 10, + }, + ); + + useEffect(() => { + getAutoMLList(); + }, [pagination, searchText]); + + // 获取自主机器学习列表 + const getAutoMLList = async () => { + const params: Record = { + page: pagination.current! - 1, + size: pagination.pageSize, + ml_name: searchText, + }; + const [res] = await to(getAutoMLListReq(params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setTableData(content); + setTotal(totalElements); + } + }; + + // 搜索 + const onSearch: SearchProps['onSearch'] = (value) => { + setSearchText(value); + }; + + // 删除模型部署 + const deleteAutoML = async (record: AutoMLData) => { + const [res] = await to(deleteAutoMLReq(record.id)); + if (res) { + message.success('删除成功'); + // 如果是一页的唯一数据,删除时,请求第一页的数据 + // 否则直接刷新这一页的数据 + // 避免回到第一页 + if (tableData.length > 1) { + setPagination((prev) => ({ + ...prev, + current: 1, + })); + } else { + getAutoMLList(); + } + } + }; + + // 处理删除 + const handleAutoMLDelete = (record: AutoMLData) => { + modalConfirm({ + title: '删除后,该实验将不可恢复', + content: '是否确认删除?', + onOk: () => { + deleteAutoML(record); + }, + }); + }; + + // 创建、编辑、复制自动机器学习 + const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { + setCacheState({ + pagination, + searchText, + }); + + if (record) { + if (isCopy) { + SessionStorage.setItem(SessionStorage.autoMLRecordIDKey, record.id, false); + navigate(`/pipeline/autoML/create`); + } else { + navigate(`/pipeline/autoML/edit/${record.id}`); + } + } else { + SessionStorage.setItem(SessionStorage.autoMLRecordIDKey, '', false); + navigate(`/pipeline/autoML/create`); + } + }; + + // 查看自动机器学习详情 + const gotoDetail = (record: AutoMLData) => { + setCacheState({ + pagination, + searchText, + }); + + navigate(`/pipeline/autoML/info/${record.id}`); + }; + + // 启动自动机器学习 + const startAutoML = async (record: AutoMLData) => { + const [res] = await to(runAutoMLReq(record.id)); + if (res) { + message.success('运行成功'); + setExpandedRowKeys([record.id]); + refreshExperimentList(); + refreshExperimentIns(record.id); + } + }; + + // --------------------------- 实验实例 --------------------------- + // 获取实验实例列表 + const getExperimentInsList = async (autoMLId: number, page: number) => { + const params = { + autoMlId: autoMLId, + page: page, + size: 5, + }; + const [res] = await to(getExperimentInsListReq(params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + try { + if (page === 0) { + setExperimentInsList(content); + } else { + setExperimentInsList((prev) => [...prev, ...content]); + } + setExperimentInsTotal(totalElements); + } catch (error) { + console.error('JSON parse error: ', error); + } + } + }; + // 展开实例 + const handleExpandChange = (expanded: boolean, record: AutoMLData) => { + setExperimentInsList([]); + if (expanded) { + setExpandedRowKeys([record.id]); + getExperimentInsList(record.id, 0); + } else { + setExpandedRowKeys([]); + } + }; + + // 跳转到实验实例详情 + const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { + navigate({ pathname: `/pipeline/automl/instance/${autoML.id}/${record.id}` }); + }; + + // 刷新实验实例列表 + const refreshExperimentIns = (experimentId: number) => { + getExperimentInsList(experimentId, 0); + }; + + // 加载更多实验实例 + const loadMoreExperimentIns = () => { + const page = Math.round(experimentInsList.length / 5); + const autoMLId = expandedRowKeys[0]; + getExperimentInsList(autoMLId, page); + }; + + // 实验实例终止 + const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { + // 刷新实验列表 + refreshExperimentList(); + setExperimentInsList((prevList) => { + return prevList.map((item) => { + if (item.id === experimentIns.id) { + return { + ...item, + status: ExperimentStatus.Terminated, + }; + } + return item; + }); + }); + }; + + // 刷新实验列表状态, + // 目前是直接刷新实验列表,后续需要优化,只刷新状态 + const refreshExperimentList = () => { + getAutoMLList(); + }; + + // --------------------------- Table --------------------------- + // 分页切换 + const handleTableChange: TableProps['onChange'] = ( + pagination, + _filters, + _sorter, + { action }, + ) => { + if (action === 'paginate') { + setPagination(pagination); + } + }; + + const columns: TableProps['columns'] = [ + { + title: '实验名称', + dataIndex: 'ml_name', + key: 'ml_name', + width: '16%', + render: tableCellRender(false, TableCellValueType.Link, { + onClick: gotoDetail, + }), + }, + { + title: '实验描述', + dataIndex: 'ml_description', + key: 'ml_description', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + + { + title: '创建时间', + dataIndex: 'update_time', + key: 'update_time', + width: '20%', + render: tableCellRender(true, TableCellValueType.Date), + ellipsis: { showTitle: false }, + }, + { + title: '最近五次运行状态', + dataIndex: 'status_list', + key: 'status_list', + width: 200, + render: (text) => { + const newText: string[] = text && text.replace(/\s+/g, '').split(','); + return ( + <> + {newText && newText.length > 0 + ? newText.map((item, index) => { + return ( + + ); + }) + : null} + + ); + }, + }, + { + title: '操作', + dataIndex: 'operation', + width: 360, + key: 'operation', + render: (_: any, record: AutoMLData) => ( +
+ + + + + + + +
+ ), + }, + ]; + + return ( +
+ +
+
+ setInputText(e.target.value)} + style={{ width: 300 }} + value={inputText} + allowClear + /> + +
+
+ `共${total}条`, + }} + onChange={handleTableChange} + expandable={{ + expandedRowRender: (record) => ( + gotoInstanceInfo(record, item)} + onRemove={() => { + refreshExperimentIns(record.id); + refreshExperimentList(); + }} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > + ), + onExpand: (e, a) => { + handleExpandChange(e, a); + }, + expandedRowKeys: expandedRowKeys, + rowExpandable: () => true, + }} + rowKey="id" + /> + + + + ); +} + +export default AutoMLList; diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.less b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.less new file mode 100644 index 00000000..cbd05bcc --- /dev/null +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.less @@ -0,0 +1,13 @@ +.auto-ml-basic { + height: 100%; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + :global { + .kf-basic-info__item__value__text { + white-space: pre; + } + } +} diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx new file mode 100644 index 00000000..b6f214ae --- /dev/null +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -0,0 +1,310 @@ +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, + formatDate, + formatEnum, + type BasicInfoData, +} from '../ConfigInfo'; +import styles from './index.less'; + +// 格式化数据集 +const formatDataset = (dataset: { name: string; version: string }) => { + if (!dataset || !dataset.name || !dataset.version) { + return '--'; + } + return `${dataset.name}:${dataset.version}`; +}; + +// 格式化优化方向 +const formatOptimizeMode = (value: boolean) => { + return value ? '越大越好' : '越小越好'; +}; + +const formatMetricsWeight = (value: string) => { + if (!value) { + return '--'; + } + const json = parseJsonText(value); + if (!json) { + return '--'; + } + return Object.entries(json) + .map(([key, value]) => `${key}:${value}`) + .join('\n'); +}; + +type AutoMLBasicProps = { + info?: AutoMLData; + className?: string; + isInstance?: boolean; + runStatus?: NodeStatus; +}; + +function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { + const basicDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + + return [ + { + label: '实验名称', + value: info.ml_name, + ellipsis: true, + }, + { + label: '实验描述', + value: info.ml_description, + ellipsis: true, + }, + { + label: '创建人', + value: info.create_by, + ellipsis: true, + }, + { + label: '创建时间', + value: info.create_time, + ellipsis: true, + format: formatDate, + }, + { + label: '更新时间', + value: info.update_time, + ellipsis: true, + format: formatDate, + }, + ]; + }, [info]); + + const configDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '任务类型', + value: info.task_type, + ellipsis: true, + format: formatEnum(autoMLTaskTypeOptions), + }, + { + label: '特征预处理算法', + value: info.include_feature_preprocessor, + ellipsis: true, + }, + { + label: '排除的特征预处理算法', + value: info.exclude_feature_preprocessor, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.include_regressor + : info.include_classifier, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.exclude_regressor + : info.exclude_classifier, + ellipsis: true, + }, + { + label: '集成方式', + value: info.ensemble_class, + ellipsis: true, + format: formatEnum(autoMLEnsembleClassOptions), + }, + { + label: '集成模型数量', + value: info.ensemble_size, + ellipsis: true, + }, + { + label: '集成最佳模型数量', + value: info.ensemble_nbest, + ellipsis: true, + }, + { + label: '最大数量', + value: info.max_models_on_disc, + ellipsis: true, + }, + { + label: '内存限制(MB)', + value: info.memory_limit, + ellipsis: true, + }, + { + label: '单次时间限制(秒)', + value: info.per_run_time_limit, + ellipsis: true, + }, + { + label: '搜索时间限制(秒)', + value: info.time_left_for_this_task, + ellipsis: true, + }, + { + label: '重采样策略', + value: info.resampling_strategy, + ellipsis: true, + }, + { + label: '交叉验证折数', + value: info.folds, + ellipsis: true, + }, + { + label: '是否打乱', + value: info.shuffle, + ellipsis: true, + format: formatBoolean, + }, + { + label: '训练集比率', + value: info.train_size, + ellipsis: true, + }, + { + label: '测试集比率', + value: info.test_size, + ellipsis: true, + }, + { + label: '计算指标', + value: info.scoring_functions, + ellipsis: true, + }, + { + label: '随机种子', + value: info.seed, + ellipsis: true, + }, + + { + label: '数据集', + value: info.dataset, + ellipsis: true, + format: formatDataset, + }, + { + label: '预测目标列', + value: info.target_columns, + ellipsis: true, + }, + ]; + }, [info]); + + const metricsData = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '指标名称', + value: info.metric_name, + ellipsis: true, + }, + { + label: '优化方向', + value: info.greater_is_better, + ellipsis: true, + format: formatOptimizeMode, + }, + { + label: '指标权重', + value: info.metrics, + ellipsis: true, + format: formatMetricsWeight, + }, + ]; + }, [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 ( +
+ {isInstance && runStatus ? ( + + ) : ( + + )} + + +
+ ); +} + +export default AutoMLBasic; diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.less b/react-ui/src/pages/AutoML/components/ConfigInfo/index.less new file mode 100644 index 00000000..7722651f --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ConfigInfo/index.less @@ -0,0 +1,40 @@ +.config-info { + flex: 1; + min-width: 0; + + &__content { + padding: 20px; + padding: 20px @content-padding; + background-color: white; + border: 1px solid @border-color-base; + border-radius: 0 0 4px 4px; + } + + :global { + .kf-basic-info { + width: 100%; + + &__item { + &__label { + font-size: @font-size; + text-align: left; + text-align-last: left; + } + &__value { + min-width: 0; + font-size: @font-size; + } + } + } + } + + &--three-column { + :global { + .kf-basic-info { + &__item { + width: calc((100% - 80px) / 3); + } + } + } + } +} diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx new file mode 100644 index 00000000..2fbb6825 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx @@ -0,0 +1,48 @@ +import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; +import classNames from 'classnames'; +import { useEffect } from 'react'; +import ConfigTitle from '../ConfigTitle'; +import styles from './index.less'; +export * from '@/components/BasicInfo/format'; +export type { BasicInfoData }; + +type ConfigInfoProps = { + title: string; + data: BasicInfoData[]; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + labelWidth: number; + threeColumn?: boolean; +}; + +function ConfigInfo({ + title, + data, + className, + style, + children, + labelWidth, + threeColumn = false, +}: ConfigInfoProps) { + useEffect(() => {}, []); + + return ( +
+ +
+ + {children} +
+
+ ); +} + +export default ConfigInfo; diff --git a/react-ui/src/pages/AutoML/components/ConfigTitle/index.less b/react-ui/src/pages/AutoML/components/ConfigTitle/index.less new file mode 100644 index 00000000..5b894c43 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ConfigTitle/index.less @@ -0,0 +1,39 @@ +.config-title { + width: 100%; + height: 56px; + padding-left: @content-padding; + background: linear-gradient( + 179.03deg, + rgba(199, 223, 255, 0.12) 0%, + rgba(22, 100, 255, 0.04) 100% + ); + border: 1px solid #e8effb; + border-radius: 4px 4px 0 0; + + &__img { + width: 16px; + height: 16px; + margin-right: 10px; + } + + &__text { + position: relative; + color: @text-color; + font-weight: 500; + font-size: @font-size-title; + + &::after { + position: absolute; + bottom: 6px; + left: 0; + width: 100%; + height: 6px; + background: linear-gradient( + to right, + .addAlpha(@primary-color, 0.4) [] 0, + .addAlpha(@primary-color, 0) [] 100% + ); + content: ''; + } + } +} diff --git a/react-ui/src/pages/AutoML/components/ConfigTitle/index.tsx b/react-ui/src/pages/AutoML/components/ConfigTitle/index.tsx new file mode 100644 index 00000000..546eca88 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ConfigTitle/index.tsx @@ -0,0 +1,22 @@ +import { Flex } from 'antd'; +import styles from './index.less'; + +type ConfigTitleProps = { + title: string; +}; + +function ConfigTitle({ title }: ConfigTitleProps) { + return ( + + + {title} + + ); +} + +export default ConfigTitle; diff --git a/react-ui/src/pages/AutoML/components/CopyingText/index.less b/react-ui/src/pages/AutoML/components/CopyingText/index.less new file mode 100644 index 00000000..951b37dd --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CopyingText/index.less @@ -0,0 +1,18 @@ +.copying-text { + display: flex; + flex: 1; + align-items: center; + min-width: 0; + margin-left: 16px; + + &__text { + color: @text-color; + font-size: 15px; + } + + &__icon { + margin-left: 6px; + font-size: 14px; + cursor: pointer; + } +} diff --git a/react-ui/src/pages/AutoML/components/CopyingText/index.tsx b/react-ui/src/pages/AutoML/components/CopyingText/index.tsx new file mode 100644 index 00000000..b4c56f4e --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CopyingText/index.tsx @@ -0,0 +1,30 @@ +import KFIcon from '@/components/KFIcon'; +import { Typography } from 'antd'; +import styles from './index.less'; + +export type CopyingTextProps = { + text: string; +}; + +function CopyingText({ text }: CopyingTextProps) { + return ( +
+ + {text} + + +
+ ); +} + +export default CopyingText; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/BasicConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/BasicConfig.tsx new file mode 100644 index 00000000..aec61d3f --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/BasicConfig.tsx @@ -0,0 +1,53 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { Col, Form, Input, Row } from 'antd'; +function BasicConfig() { + return ( + <> + + + + + + + + + + + + + + + + + ); +} + +export default BasicConfig; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx new file mode 100644 index 00000000..b3b3f2dd --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx @@ -0,0 +1,59 @@ +import ResourceSelect, { + ResourceSelectorType, + requiredValidator, +} from '@/components/ResourceSelect'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { Col, Form, Input, Row } from 'antd'; + +function DatasetConfig() { + return ( + <> + + + + + + + + + + + + + + + + + ); +} + +export default DatasetConfig; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx new file mode 100644 index 00000000..d03445f3 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx @@ -0,0 +1,455 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { + AutoMLEnsembleClass, + AutoMLResamplingStrategy, + AutoMLTaskType, + autoMLEnsembleClassOptions, + autoMLResamplingStrategyOptions, + autoMLTaskTypeOptions, +} from '@/enums'; +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', +].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', +].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 })); + +// 分类指标 +export const classificationMetrics = [ + 'accuracy', + 'balanced_accuracy', + 'roc_auc', + 'average_precision', + 'log_loss', + 'precision_macro', + 'precision_micro', + 'precision_samples', + 'precision_weighted', + 'recall_macro', + 'recall_micro', + 'recall_samples', + 'recall_weighted', + 'f1_macro', + 'f1_micro', + 'f1_samples', + 'f1_weighted', +].map((name) => ({ label: name, value: name })); + +// 回归指标 +export const regressionMetrics = [ + 'mean_absolute_error', + 'mean_squared_error', + 'root_mean_squared_error', + 'mean_squared_log_error', + 'median_absolute_error', + 'r2', +].map((name) => ({ label: name, value: name })); + +function ExecuteConfig() { + const form = Form.useFormInstance(); + const task_type = Form.useWatch('task_type', form); + const include_classifier = Form.useWatch('include_classifier', form); + const exclude_classifier = Form.useWatch('exclude_classifier', form); + const include_regressor = Form.useWatch('include_regressor', form); + const exclude_regressor = Form.useWatch('exclude_regressor', form); + const include_feature_preprocessor = Form.useWatch('include_feature_preprocessor', form); + const exclude_feature_preprocessor = Form.useWatch('exclude_feature_preprocessor', form); + + return ( + <> + + + + + form.resetFields(['metrics'])} + > + + + + + + + + 0} + mode="multiple" + showSearch + /> + + + + + + {({ getFieldValue }) => { + return getFieldValue('task_type') === AutoMLTaskType.Classification ? ( + <> + + + + 0} + showSearch + /> + + + + + ) : ( + <> + + + + 0} + showSearch + /> + + + + + ); + }} + + + + + + + + + + + + {({ getFieldValue }) => { + return getFieldValue('ensemble_class') === AutoMLEnsembleClass.Default ? ( + <> + + + + + + + + + + + + + + + + + ) : null; + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {({ getFieldValue }) => { + return getFieldValue('resampling_strategy') === AutoMLResamplingStrategy.CrossValid ? ( + + + + + + + + ) : null; + }} + + + + + + + + + + + + + + + + + + + ); +} + +export default ExecuteConfig; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx new file mode 100644 index 00000000..c9e5b61f --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx @@ -0,0 +1,130 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { AutoMLTaskType } from '@/enums'; +import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; +import { classificationMetrics, regressionMetrics } from './ExecuteConfig'; +import styles from './index.less'; + +function TrialConfig() { + const form = Form.useFormInstance(); + const task_type = Form.useWatch('task_type', form); + const metrics = Form.useWatch('metrics', form) || []; + const selectedMetrics = metrics + .map((item: { name: string; value: number }) => item?.name) + .filter(Boolean); + const allMetricsOptions = + task_type === AutoMLTaskType.Classification ? classificationMetrics : regressionMetrics; + const metricsOptions = allMetricsOptions.filter((item) => !selectedMetrics.includes(item.label)); + + return ( + <> + + + + + + + + + + + + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }, index) => ( + + +
+ + + + ); +} + +export default ExperimentHistory; diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less new file mode 100644 index 00000000..499b8424 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less @@ -0,0 +1,71 @@ +.tableExpandBox { + display: flex; + align-items: center; + width: 100%; + padding: 0 0 0 33px; + color: @text-color; + font-size: 14px; + + & > div { + padding: 0 16px; + } + + .check { + width: calc((100% + 32px + 33px) / 6.25 / 2); + } + + .index { + width: calc((100% + 32px + 33px) / 6.25 / 2); + } + + .description { + display: flex; + flex: 1; + align-items: center; + } + + .startTime { + .singleLine(); + width: calc(20% + 10px); + } + + .status { + width: 200px; + } + + .operation { + position: relative; + width: 344px; + } +} + +.tableExpandBoxContent { + height: 45px; + background-color: #fff; + border: 1px solid #eaeaea; + + & + & { + border-top: none; + } + + .statusBox { + display: flex; + align-items: center; + width: 200px; + + .statusIcon { + visibility: hidden; + transition: all 0.2s; + } + } + .statusBox:hover .statusIcon { + visibility: visible; + } +} + +.loadMoreBox { + display: flex; + align-items: center; + justify-content: center; + margin: 16px auto 0; +} diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx new file mode 100644 index 00000000..72fe3473 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx @@ -0,0 +1,229 @@ +import KFIcon from '@/components/KFIcon'; +import { ExperimentStatus } from '@/enums'; +import { useCheck } from '@/hooks'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { + batchDeleteExperimentInsReq, + deleteExperimentInsReq, + stopExperimentInsReq, +} from '@/services/autoML'; +import themes from '@/styles/theme.less'; +import { type ExperimentInstance } from '@/types'; +import { elapsedTime, formatDate } from '@/utils/date'; +import { to } from '@/utils/promise'; +import { modalConfirm } from '@/utils/ui'; +import { DoubleRightOutlined } from '@ant-design/icons'; +import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useMemo } from 'react'; +import styles from './index.less'; + +type ExperimentInstanceProps = { + experimentInsList?: ExperimentInstance[]; + experimentInsTotal: number; + onClickInstance?: (instance: ExperimentInstance) => void; + onRemove?: () => void; + onTerminate?: (instance: ExperimentInstance) => void; + onLoadMore?: () => void; +}; + +function ExperimentInstanceComponent({ + experimentInsList, + experimentInsTotal, + onClickInstance, + onRemove, + onTerminate, + onLoadMore, +}: ExperimentInstanceProps) { + const { message } = App.useApp(); + const allIntanceIds = useMemo(() => { + return experimentInsList?.map((item) => item.id) || []; + }, [experimentInsList]); + const [ + selectedIns, + setSelectedIns, + checked, + indeterminate, + checkAll, + isSingleChecked, + checkSingle, + ] = useCheck(allIntanceIds); + + useEffect(() => { + // 关闭时清空 + if (allIntanceIds.length === 0) { + setSelectedIns([]); + } + }, [experimentInsList]); + + // 删除实验实例确认 + const handleRemove = (instance: ExperimentInstance) => { + modalConfirm({ + title: '确定删除该条实例吗?', + onOk: () => { + deleteExperimentInstance(instance.id); + }, + }); + }; + + // 删除实验实例 + const deleteExperimentInstance = async (id: number) => { + const [res] = await to(deleteExperimentInsReq(id)); + if (res) { + message.success('删除成功'); + onRemove?.(); + } + }; + + // 批量删除实验实例确认 + const handleDeleteAll = () => { + modalConfirm({ + title: '确定批量删除选中的实例吗?', + onOk: () => { + batchDeleteExperimentInstances(); + }, + }); + }; + + // 批量删除实验实例 + const batchDeleteExperimentInstances = async () => { + const [res] = await to(batchDeleteExperimentInsReq(selectedIns)); + if (res) { + message.success('删除成功'); + setSelectedIns([]); + onRemove?.(); + } + }; + + // 终止实验实例 + const terminateExperimentInstance = async (instance: ExperimentInstance) => { + const [res] = await to(stopExperimentInsReq(instance.id)); + if (res) { + message.success('终止成功'); + onTerminate?.(instance); + } + }; + + if (!experimentInsList || experimentInsList.length === 0) { + return null; + } + + return ( +
+
+
+ +
+
序号
+
运行时长
+
开始时间
+
状态
+
+ 操作 + {selectedIns.length > 0 && ( + + )} +
+
+ + {experimentInsList.map((item, index) => ( +
+
+ checkSingle(item.id)} + > +
+ onClickInstance?.(item)} + > + {index + 1} + +
+ {elapsedTime(item.create_time, item.finish_time)} +
+
+ + {formatDate(item.create_time)} + +
+
+ + + {experimentStatusInfo[item.status as ExperimentStatus]?.label} + +
+
+ + + + +
+
+ ))} + {experimentInsTotal > experimentInsList.length ? ( +
+ +
+ ) : null} +
+ ); +} + +export default ExperimentInstanceComponent; diff --git a/react-ui/src/pages/AutoML/components/ExperimentLog/index.less b/react-ui/src/pages/AutoML/components/ExperimentLog/index.less new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less new file mode 100644 index 00000000..6d88dea3 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less @@ -0,0 +1,39 @@ +.experiment-result { + height: calc(100% - 10px); + margin-top: 10px; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + &__text { + width: 100%; + height: 460px; + margin-bottom: 16px; + padding: 20px @content-padding; + overflow: auto; + white-space: pre-wrap; + border: 1px solid @border-color-base; + border-radius: 0 0 4px 4px; + } + + &__image-container { + display: flex; + align-items: flex-start; + width: 100%; + padding: 20px @content-padding; + overflow-x: auto; + border: 1px solid @border-color-base; + border-radius: 0 0 4px 4px; + + &__image { + height: 248px; + margin-right: 20px; + border: 1px solid rgba(96, 107, 122, 0.3); + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx new file mode 100644 index 00000000..933f6fbd --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx @@ -0,0 +1,56 @@ +import { getFileReq } from '@/services/file'; +import { to } from '@/utils/promise'; +import { useEffect, useMemo, useState } from 'react'; +import ConfigTitle from '../ConfigTitle'; +import styles from './index.less'; + +type ExperimentResultProps = { + fileUrl?: string; + imageUrl?: string; +}; + +function ExperimentResult({ fileUrl, imageUrl }: ExperimentResultProps) { + const [result, setResult] = useState(''); + + const images = useMemo(() => { + if (imageUrl) { + return imageUrl.split(',').map((item) => item.trim()); + } + return []; + }, [imageUrl]); + + useEffect(() => { + if (fileUrl) { + getResultFile(); + } + }, [fileUrl]); + + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; + + return ( +
+ +
{result}
+ +
+ {images.map((item, index) => ( + + ))} +
+
+ ); +} + +export default ExperimentResult; diff --git a/react-ui/src/pages/AutoML/types.ts b/react-ui/src/pages/AutoML/types.ts new file mode 100644 index 00000000..339a9e51 --- /dev/null +++ b/react-ui/src/pages/AutoML/types.ts @@ -0,0 +1,85 @@ +import { type ParameterInputObject } from '@/components/ResourceSelect'; +import { type NodeStatus } from '@/types'; + +// 操作类型 +export enum OperationType { + Create = 'Create', // 创建 + Update = 'Update', // 更新 +} + +// 表单数据 +export type FormData = { + ml_name: string; // 实验名称 + ml_description: string; // 实验描述 + ensemble_class?: string; // 集成方式 + ensemble_nbest?: string; // 集成最佳模型数量 + ensemble_size?: number; // 集成模型数量 + include_classifier?: string[]; // 分类算法 + include_feature_preprocessor?: string[]; // 特征预处理算法 + include_regressor?: string[]; // 回归算法 + exclude_classifier?: string[]; + exclude_feature_preprocessor?: string[]; + exclude_regressor?: string[]; + max_models_on_disc?: number; // 最大数量 + memory_limit?: number; // 内存限制(MB) + per_run_time_limit?: number; // 时间限制(秒) + resampling_strategy?: string; // 重采样策略 + folds?: number; // 交叉验证折数 + scoring_functions?: string; // 计算指标 + shuffle?: boolean; // 是否打乱 + seed?: number; // 随机种子 + task_type: string; // 任务类型 + test_size?: number; // 测试集比率 + train_size?: number; // 训练集比率 + time_left_for_this_task: number; // 搜索时间限制(秒) + metric_name?: string; // 指标名称 + greater_is_better: boolean; // 指标优化方向 + metrics?: { name: string; value: number }[]; // 指标权重 + dataset: ParameterInputObject; // 数据集 + target_columns: string; // 预测目标列 +}; + +export type AutoMLData = { + id: number; + progress: number; + run_state: string; + state: number; + metrics?: string; + include_classifier?: string; + include_feature_preprocessor?: string; + include_regressor?: string; + exclude_classifier?: string; + exclude_feature_preprocessor?: string; + exclude_regressor?: string; + dataset?: string; + create_by?: string; + create_time?: string; + update_by?: string; + update_time?: string; + status_list: string; // 最近五次运行状态 +} & Omit< + FormData, + 'metrics|dataset|include_classifier|include_feature_preprocessor|include_regressor|exclude_classifier|exclude_feature_preprocessor|exclude_regressor' +>; + +// 自动机器学习实验实例 +export type AutoMLInstanceData = { + id: number; + auto_ml_id: number; + result_path: string; + model_path: string; + img_path: string; + run_history_path: string; + state: number; + status: string; + node_status: string; + node_result: string; + param: string; + source: string | null; + argo_ins_name: string; + argo_ins_ns: string; + create_time: string; + update_time: string; + finish_time: 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 b9a93e8f..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)); @@ -135,7 +135,7 @@ function LogGroup({ const setupSockect = () => { let { host } = location; if (process.env.NODE_ENV === 'development') { - host = '172.20.32.185:31213'; + host = '172.20.32.181:31213'; } const socket = new WebSocket( `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, 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/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 76ffd4c6..998fb436 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -271,6 +271,7 @@ function Experiment() { const [res] = await to(runExperiments(id)); if (res) { message.success('运行成功'); + refreshExperimentList(); refreshExperimentIns(id); } else { message.error('运行失败'); @@ -385,7 +386,7 @@ function Experiment() { key: 'status_list', width: 200, render: (text) => { - let newText = text && text.replace(/\s+/g, '').split(','); + const newText = text && text.replace(/\s+/g, '').split(','); return ( <> {newText && newText.length > 0 diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index 8cc9f441..11fdcb9c 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -503,59 +503,69 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete ))} -
- -
- {inParametersList.map((item) => ( - -
- - {item.value.type === 'select' ? ( - - ) : ( - - )} + {/* 输入参数 */} + {inParametersList.length > 0 && ( + <> +
+ +
+ {inParametersList.map((item) => ( + +
+ + {item.value.type === 'select' ? ( + + ) : ( + + )} + + {item.value.type === 'ref' && ( + + + + )} +
- {item.value.type === 'ref' && ( - - - - )} + ))} + + )} + {/* 输出参数 */} + {outParametersList.length > 0 && ( + <> +
+
-
- ))} -
- -
- {outParametersList.map((item) => ( - - - - ))} + {outParametersList.map((item) => ( + + + + ))} + + )} ); diff --git a/react-ui/src/requestConfig.ts b/react-ui/src/requestConfig.ts index b268bc47..6d298d33 100644 --- a/react-ui/src/requestConfig.ts +++ b/react-ui/src/requestConfig.ts @@ -58,11 +58,12 @@ export const requestConfig: RequestConfig = { const options = config as RequestOptions; const skipErrorHandler = options?.skipErrorHandler; const skipLoading = options?.skipLoading; + const skipValidating = options?.skipValidating; if (!skipLoading) { Loading.hide(); } if (status >= 200 && status < 300) { - if (data && (data instanceof Blob || data.code === 200)) { + if (data && (skipValidating || data instanceof Blob || data.code === 200)) { return response; } else if (data && data.code === 401) { clearSessionToken(); diff --git a/react-ui/src/services/autoML/index.js b/react-ui/src/services/autoML/index.js new file mode 100644 index 00000000..a4e39a99 --- /dev/null +++ b/react-ui/src/services/autoML/index.js @@ -0,0 +1,93 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-18 10:18:27 + * @Description: 自动机器学习请求 + */ + +import { request } from '@umijs/max'; + + +// 分页查询自动学习 +export function getAutoMLListReq(params) { + return request(`/api/mmp/autoML`, { + method: 'GET', + params, + }); +} + +// 查询自动学习详情 +export function getAutoMLInfoReq(params) { + return request(`/api/mmp/autoML/getAutoMlDetail`, { + method: 'GET', + params, + }); +} + +// 新增自动学习 +export function addAutoMLReq(data) { + return request(`/api/mmp/autoML`, { + method: 'POST', + data, + }); +} + +// 编辑自动学习 +export function updateAutoMLReq(data) { + return request(`/api/mmp/autoML`, { + method: 'PUT', + data, + }); +} + +// 删除自动学习 +export function deleteAutoMLReq(id) { + return request(`/api/mmp/autoML/${id}`, { + method: 'DELETE', + }); +} + +// 运行自动学习 +export function runAutoMLReq(id) { + return request(`/api/mmp/autoML/run/${id}`, { + method: 'POST', + }); +} + +// ----------------------- 实验实例 ----------------------- +// 获取实验实例列表 +export function getExperimentInsListReq(params) { + return request(`/api/mmp/autoMLIns`, { + method: 'GET', + params, + }); +} + +// 查询实验实例详情 +export function getExperimentInsReq(id) { + return request(`/api/mmp/autoMLIns/${id}`, { + method: 'GET', + }); +} + +// 停止实验实例 +export function stopExperimentInsReq(id) { + return request(`/api/mmp/autoMLIns/${id}`, { + method: 'PUT', + }); +} + +// 删除实验实例 +export function deleteExperimentInsReq(id) { + return request(`/api/mmp/autoMLIns/${id}`, { + method: 'DELETE', + }); +} + +// 批量删除实验实例 +export function batchDeleteExperimentInsReq(data) { + return request(`/api/mmp/autoMLIns/batchDelete`, { + method: 'DELETE', + data + }); +} + diff --git a/react-ui/src/services/file/index.js b/react-ui/src/services/file/index.js new file mode 100644 index 00000000..a6786007 --- /dev/null +++ b/react-ui/src/services/file/index.js @@ -0,0 +1,20 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-30 11:43:26 + * @Description: 请求文件,比如 json 文件 + */ + + +import { request } from '@umijs/max'; + +// 获取文件,不需要token,非结构化数据 +export function getFileReq(url, config) { + return request(url, { + method: 'GET', + headers: { + isToken: false, + }, + skipValidating: true, + ...config + }); +} \ No newline at end of file diff --git a/react-ui/src/styles/menu.less b/react-ui/src/styles/menu.less index 1d547ff1..02fe0c17 100644 --- a/react-ui/src/styles/menu.less +++ b/react-ui/src/styles/menu.less @@ -15,16 +15,6 @@ display: none !important; margin-left: 0 !important; } - - &:hover { - .anticon.kf-menu-item__default-icon { - display: none !important; - } - .anticon.kf-menu-item__active-icon { - display: inline !important; - opacity: 1; - } - } } } @@ -42,18 +32,29 @@ } } +.ant-menu-submenu .ant-menu-submenu-title:hover, +.ant-menu-item:hover { + color: @primary-color !important; + + .kf-menu-item { + .anticon.kf-menu-item__default-icon { + display: none !important; + } + + .anticon.kf-menu-item__active-icon { + display: inline !important; + opacity: 1; + } + } +} + .ant-pro-base-menu-vertical-collapsed { .kf-menu-item { justify-content: center; + width: 100%; .kf-menu-item__name { display: none !important; } } } - -.ant-menu-submenu { - .ant-menu-submenu-title:hover { - color: @primary-color !important; - } -} 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; +}; diff --git a/react-ui/src/utils/clipboard.js b/react-ui/src/utils/clipboard.js new file mode 100644 index 00000000..177fcbce --- /dev/null +++ b/react-ui/src/utils/clipboard.js @@ -0,0 +1,12 @@ +import ClipboardJS from 'clipboard'; +import { message } from "antd"; + +const clipboard = new ClipboardJS('#copying'); + +clipboard.on('success', () => { + message.success('复制成功'); +}); + +clipboard.on('error', () => { + message.error('复制失败'); +}); \ No newline at end of file diff --git a/react-ui/src/utils/functional.ts b/react-ui/src/utils/functional.ts index 01514db7..6128c897 100644 --- a/react-ui/src/utils/functional.ts +++ b/react-ui/src/utils/functional.ts @@ -4,6 +4,16 @@ * @Description: 函数式编程 */ +/** + * Safely invokes a function with a given value, returning the result of the + * function or the provided value if it is `undefined` or `null`. + * + * @template T - The type of the input value. + * @template M - The type of the output value. + * @param {function} fn - The function to be invoked with the input value. + * @returns {function} A function that takes a value, invokes `fn` with it if + * it's not `undefined` or `null`, and returns the result or the original value. + */ export function safeInvoke( fn: (value: T) => M | undefined | null, ): (value: T | undefined | null) => M | undefined | null { diff --git a/react-ui/src/utils/index.ts b/react-ui/src/utils/index.ts index 67ca10d2..3deb9832 100644 --- a/react-ui/src/utils/index.ts +++ b/react-ui/src/utils/index.ts @@ -198,7 +198,7 @@ export const fittingString = (str: string, maxWidth: number, fontSize: number): * @param {any} str - the string to be checked * @return {boolean} true if the string is empty, undefined, or null, false otherwise */ -export const isEmptyString = (str: any): boolean => { +export const isEmpty = (str: any): boolean => { return str === '' || str === undefined || str === null; }; @@ -241,3 +241,28 @@ export const tableSorter = (a: any, b: any) => { } return 0; }; + +/** + * Trim the given character from both ends of the given string. + * + * @param {string} ch - the character to trim + * @param {string} str - the string to trim + * @return {string} the trimmed string + */ +export const trimCharacter = (str: string, ch: string): string => { + if (str === null || str === undefined) { + return str; + } + const reg = new RegExp(`^${ch}|${ch}$`, 'g'); + return str.trim().replace(reg, ''); +}; + +/** + * Converts an empty string to undefined. + * + * @param {string} [value] - The string to convert. + * @return {string | undefined} The converted string or undefined. + */ +export const convertEmptyStringToUndefined = (value?: string): string | undefined => { + return value === '' ? undefined : value; +}; diff --git a/react-ui/src/utils/sessionStorage.ts b/react-ui/src/utils/sessionStorage.ts index 8ffa8836..b71a35fd 100644 --- a/react-ui/src/utils/sessionStorage.ts +++ b/react-ui/src/utils/sessionStorage.ts @@ -11,6 +11,8 @@ export default class SessionStorage { static readonly editorUrlKey = 'editor-url'; // 客户端信息 static readonly clientInfoKey = 'client-info'; + // 自动机器学习记录ID + static readonly autoMLRecordIDKey = 'auto-ml-record-id'; static getItem(key: string, isObject: boolean = false) { const jsonStr = sessionStorage.getItem(key);