| @@ -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==" | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| { | |||
| "dependencies": { | |||
| "clipboard": "~2.0.11" | |||
| } | |||
| } | |||
| @@ -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', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -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<GlobalInitialState> { | |||
| // 如果不是登录页面,执行 | |||
| 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(); | |||
| @@ -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 = ( | |||
| <div className={`${myClassName}__value-container`}> | |||
| {formatValue.map((item: BasicInfoLink) => ( | |||
| <BasicInfoItemValue | |||
| key={item.value} | |||
| value={item.value} | |||
| link={item.link} | |||
| url={item.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } else if (React.isValidElement(formatValue)) { | |||
| // 这个判断必须在下面的判断之前 | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue | |||
| value={formatValue.value} | |||
| link={formatValue.link} | |||
| url={formatValue.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ); | |||
| } else { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className={myClassName} key={label}> | |||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||
| {label} | |||
| </div> | |||
| {valueComponent} | |||
| </div> | |||
| ); | |||
| } | |||
| 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 = ( | |||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||
| {value} | |||
| </a> | |||
| ); | |||
| } else if (link && value) { | |||
| component = ( | |||
| <Link to={link} className={`${myClassName}__link`}> | |||
| {value} | |||
| </Link> | |||
| ); | |||
| } else if (React.isValidElement(value)) { | |||
| return value; | |||
| } else { | |||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||
| } | |||
| return ( | |||
| <div className={myClassName}> | |||
| <Typography.Text | |||
| ellipsis={ellipsis ? { tooltip: value } : false} | |||
| style={{ fontSize: 'inherit' }} | |||
| > | |||
| {component} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -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 : '--'; | |||
| }; | |||
| }; | |||
| @@ -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 ( | |||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||
| @@ -48,82 +27,3 @@ export default function BasicInfo({ datas, className, style, labelWidth }: Basic | |||
| </div> | |||
| ); | |||
| } | |||
| 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 = ( | |||
| <div className={`${myClassName}__value-container`}> | |||
| {formatValue.map((item: BasicInfoLink) => ( | |||
| <BasicInfoItemValue | |||
| key={item.value} | |||
| value={item.value} | |||
| link={item.link} | |||
| url={item.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue | |||
| value={formatValue.value} | |||
| link={formatValue.link} | |||
| url={formatValue.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ); | |||
| } else { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className={myClassName} key={label}> | |||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||
| {label} | |||
| </div> | |||
| {valueComponent} | |||
| </div> | |||
| ); | |||
| } | |||
| export function BasicInfoItemValue({ | |||
| value, | |||
| link, | |||
| url, | |||
| ellipsis, | |||
| classPrefix, | |||
| }: BasicInfoItemValueProps) { | |||
| const myClassName = `${classPrefix}__item__value`; | |||
| let component = undefined; | |||
| if (url && value) { | |||
| component = ( | |||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||
| {value} | |||
| </a> | |||
| ); | |||
| } else if (link && value) { | |||
| component = ( | |||
| <Link to={link} className={`${myClassName}__link`}> | |||
| {value} | |||
| </Link> | |||
| ); | |||
| } else { | |||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||
| } | |||
| return ( | |||
| <div className={myClassName}> | |||
| <Typography.Text ellipsis={ellipsis ? { tooltip: value } : false}> | |||
| {component} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -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; | |||
| }; | |||
| @@ -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 = { | |||
| @@ -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'; // 导入你的路由配置 | |||
| @@ -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 <Icon type={type} className={className} style={iconStyle} />; | |||
| return <Icon {...rest} type={type} className={className} style={iconStyle} />; | |||
| } | |||
| export default KFIcon; | |||
| @@ -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 }, | |||
| ]; | |||
| @@ -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; | |||
| @@ -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%; | |||
| @@ -1,3 +1,9 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-09-02 08:42:57 | |||
| * @Description: 应用开发 | |||
| */ | |||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||
| function Application() { | |||
| @@ -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); | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string, number>) | |||
| : 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 ( | |||
| <div className={styles['create-automl']}> | |||
| <PageTitle title={title}></PageTitle> | |||
| <div className={styles['create-automl__content']}> | |||
| <div> | |||
| <Form | |||
| name="create-automl" | |||
| labelCol={{ flex: '160px' }} | |||
| labelAlign="left" | |||
| form={form} | |||
| onFinish={handleSubmit} | |||
| size="large" | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| initialValues={{ | |||
| task_type: AutoMLTaskType.Classification, | |||
| 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, | |||
| }} | |||
| > | |||
| <BasicConfig /> | |||
| <ExecuteConfig /> | |||
| <TrialConfig /> | |||
| <DatasetConfig /> | |||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||
| <Button type="primary" htmlType="submit"> | |||
| {buttonText} | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| htmlType="button" | |||
| onClick={cancel} | |||
| style={{ marginLeft: '20px' }} | |||
| > | |||
| 取消 | |||
| </Button> | |||
| </Form.Item> | |||
| </Form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CreateAutoML; | |||
| @@ -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: ''; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string>(CommonTabKeys.Public); | |||
| const params = useParams(); | |||
| const autoMLId = safeInvoke(Number)(params.id); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||
| const tabItems = [ | |||
| { | |||
| key: CommonTabKeys.Public, | |||
| label: '基本信息', | |||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||
| }, | |||
| { | |||
| key: CommonTabKeys.Private, | |||
| label: 'Trial列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| }, | |||
| ]; | |||
| useEffect(() => { | |||
| if (autoMLId) { | |||
| getAutoMLInfo(); | |||
| } | |||
| }, []); | |||
| // 获取自动机器学习详情 | |||
| const getAutoMLInfo = async () => { | |||
| const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); | |||
| if (res && res.data) { | |||
| setAutoMLInfo(res.data); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['auto-ml-info']}> | |||
| <PageTitle title="实验详情"></PageTitle> | |||
| <div className={styles['auto-ml-info__content']}> | |||
| <AutoMLBasic info={autoMLInfo} /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLInfo; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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<string>(TabKeys.Params); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const params = useParams(); | |||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||
| const instanceId = safeInvoke(Number)(params.id); | |||
| const evtSourceRef = useRef<EventSource | null>(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: <KFIcon type="icon-jibenxinxi" />, | |||
| children: ( | |||
| <AutoMLBasic | |||
| className={styles['auto-ml-instance__basic']} | |||
| info={autoMLInfo} | |||
| runStatus={instanceInfo?.nodeStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.Log, | |||
| label: '日志', | |||
| icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={instanceInfo.nodeStatus.displayName} | |||
| workflowId={instanceInfo.nodeStatus.id} | |||
| instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} | |||
| instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| const resultTabItems = [ | |||
| { | |||
| key: TabKeys.Result, | |||
| label: '实验结果', | |||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | |||
| children: ( | |||
| <ExperimentResult fileUrl={instanceInfo?.result_path} imageUrl={instanceInfo?.img_path} /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| fileUrl={instanceInfo?.run_history_path} | |||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||
| /> | |||
| ), | |||
| }, | |||
| ]; | |||
| const tabItems = | |||
| instanceInfo?.status === ExperimentStatus.Succeeded | |||
| ? [...basicTabItems, ...resultTabItems] | |||
| : basicTabItems; | |||
| return ( | |||
| <div className={styles['auto-ml-instance']}> | |||
| <Tabs | |||
| className={styles['auto-ml-instance__tabs']} | |||
| items={tabItems} | |||
| activeKey={activeTab} | |||
| onChange={setActiveTab} | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLInstance; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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<AutoMLData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| useEffect(() => { | |||
| getAutoMLList(); | |||
| }, [pagination, searchText]); | |||
| // 获取自主机器学习列表 | |||
| const getAutoMLList = async () => { | |||
| const params: Record<string, any> = { | |||
| 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<AutoMLData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const columns: TableProps<AutoMLData>['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 ( | |||
| <img | |||
| style={{ width: '17px', marginRight: '6px' }} | |||
| key={index} | |||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| ); | |||
| }) | |||
| : null} | |||
| </> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 360, | |||
| key: 'operation', | |||
| render: (_: any, record: AutoMLData) => ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="start" | |||
| icon={<KFIcon type="icon-yunhang" />} | |||
| onClick={() => startAutoML(record)} | |||
| > | |||
| 运行 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| onClick={() => createAutoML(record, false)} | |||
| > | |||
| 编辑 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="copy" | |||
| icon={<KFIcon type="icon-fuzhi" />} | |||
| onClick={() => createAutoML(record, true)} | |||
| > | |||
| 复制 | |||
| </Button> | |||
| <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| colorLink: themes['warningColor'], | |||
| }, | |||
| }} | |||
| > | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="remove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleAutoMLDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| </ConfigProvider> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['auto-ml-list']}> | |||
| <PageTitle title="自动机器学习列表"></PageTitle> | |||
| <div className={styles['auto-ml-list__content']}> | |||
| <div className={styles['auto-ml-list__content__filter']}> | |||
| <Input.Search | |||
| placeholder="按实验名称筛选" | |||
| onSearch={onSearch} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| style={{ width: 300 }} | |||
| value={inputText} | |||
| allowClear | |||
| /> | |||
| <Button | |||
| style={{ marginLeft: '20px' }} | |||
| type="default" | |||
| onClick={() => createAutoML()} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新建实验 | |||
| </Button> | |||
| </div> | |||
| <div | |||
| className={classNames('vertical-scroll-table', styles['auto-ml-list__content__table'])} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| expandable={{ | |||
| expandedRowRender: (record) => ( | |||
| <ExperimentInstance | |||
| experimentInsList={experimentInsList} | |||
| experimentInsTotal={experimentInsTotal} | |||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||
| onRemove={() => { | |||
| refreshExperimentIns(record.id); | |||
| refreshExperimentList(); | |||
| }} | |||
| onTerminate={handleInstanceTerminate} | |||
| onLoadMore={() => loadMoreExperimentIns()} | |||
| ></ExperimentInstance> | |||
| ), | |||
| onExpand: (e, a) => { | |||
| handleExpandChange(e, a); | |||
| }, | |||
| expandedRowKeys: expandedRowKeys, | |||
| rowExpandable: () => true, | |||
| }} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLList; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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: ( | |||
| <Flex align="center"> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[runStatus.phase]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div | |||
| style={{ | |||
| color: experimentStatusInfo[runStatus?.phase]?.color, | |||
| fontSize: '15px', | |||
| lineHeight: 1.6, | |||
| }} | |||
| > | |||
| {experimentStatusInfo[runStatus?.phase]?.label} | |||
| </div> | |||
| </Flex> | |||
| ), | |||
| ellipsis: true, | |||
| }, | |||
| ]; | |||
| }, [runStatus]); | |||
| return ( | |||
| <div className={classNames(styles['auto-ml-basic'], className)}> | |||
| {isInstance && runStatus ? ( | |||
| <ConfigInfo | |||
| title="运行信息" | |||
| data={instanceDatas} | |||
| labelWidth={70} | |||
| threeColumn | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| ) : ( | |||
| <ConfigInfo | |||
| title="基本信息" | |||
| data={basicDatas} | |||
| labelWidth={70} | |||
| threeColumn | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| )} | |||
| <ConfigInfo | |||
| title="配置信息" | |||
| data={configDatas} | |||
| labelWidth={150} | |||
| threeColumn | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} threeColumn /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLBasic; | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <div | |||
| className={classNames( | |||
| styles['config-info'], | |||
| { [styles['config-info--three-column']]: threeColumn }, | |||
| className, | |||
| )} | |||
| style={style} | |||
| > | |||
| <ConfigTitle title={title} /> | |||
| <div className={styles['config-info__content']}> | |||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | |||
| {children} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ConfigInfo; | |||
| @@ -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: ''; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| import { Flex } from 'antd'; | |||
| import styles from './index.less'; | |||
| type ConfigTitleProps = { | |||
| title: string; | |||
| }; | |||
| function ConfigTitle({ title }: ConfigTitleProps) { | |||
| return ( | |||
| <Flex align="center" className={styles['config-title']}> | |||
| <img | |||
| src={require('@/assets/img/code-name-icon.png')} | |||
| className={styles['config-title__img']} | |||
| alt="" | |||
| draggable={false} | |||
| /> | |||
| <span className={styles['config-title__text']}>{title}</span> | |||
| </Flex> | |||
| ); | |||
| } | |||
| export default ConfigTitle; | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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 ( | |||
| <div className={styles['copying-text']}> | |||
| <Typography.Text | |||
| ellipsis={{ tooltip: text }} | |||
| style={{ color: 'inherit' }} | |||
| className={styles['copying-text__text']} | |||
| > | |||
| {text} | |||
| </Typography.Text> | |||
| <KFIcon | |||
| id="copying" | |||
| data-clipboard-text={text} | |||
| type="icon-fuzhi2" | |||
| className={styles['copying-text__icon']} | |||
| color="#606b7a" | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CopyingText; | |||
| @@ -0,0 +1,53 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { Col, Form, Input, Row } from 'antd'; | |||
| function BasicConfig() { | |||
| return ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="基本信息" | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '26px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="实验名称" | |||
| name="ml_name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入实验名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入实验名称" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={20}> | |||
| <Form.Item | |||
| label="实验描述" | |||
| name="ml_description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入实验描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| placeholder="请输入实验描述" | |||
| maxLength={256} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default BasicConfig; | |||
| @@ -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 ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="数据集配置" | |||
| image={require('@/assets/img/dataset-config-icon.png')} | |||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="数据集" | |||
| name="dataset" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '请选择数据集', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="预测目标列" | |||
| name="target_columns" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入预测目标列', | |||
| }, | |||
| ]} | |||
| tooltip="数据集 csv 文件中哪几列是预测目标列,逗号分隔" | |||
| > | |||
| <Input placeholder="请输入预测目标列" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default DatasetConfig; | |||
| @@ -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 ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="执行配置" | |||
| image={require('@/assets/img/model-deployment.png')} | |||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="任务类型" | |||
| name="task_type" | |||
| rules={[{ required: true, message: '请选择任务类型' }]} | |||
| > | |||
| <Radio.Group | |||
| options={autoMLTaskTypeOptions} | |||
| onChange={() => form.resetFields(['metrics'])} | |||
| ></Radio.Group> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="特征预处理算法" | |||
| name="include_feature_preprocessor" | |||
| tooltip="如果不选,则使用所有可能的特征预处理算法。否则,将只使用包含的特征预处理算法" | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择特征预处理算法" | |||
| options={featureAlgorithms} | |||
| disabled={exclude_feature_preprocessor?.length > 0} | |||
| mode="multiple" | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="排除特征预处理算法" | |||
| name="exclude_feature_preprocessor" | |||
| tooltip="如果不选,则使用所有可能的特征预处理算法。否则,将排除包含的特征预处理算法" | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="排除特征预处理算法" | |||
| options={featureAlgorithms} | |||
| disabled={include_feature_preprocessor?.length > 0} | |||
| mode="multiple" | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Form.Item dependencies={['task_type']} noStyle> | |||
| {({ getFieldValue }) => { | |||
| return getFieldValue('task_type') === AutoMLTaskType.Classification ? ( | |||
| <> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="分类算法" | |||
| name="include_classifier" | |||
| tooltip="如果不选,则使用所有可能的分类算法。否则,将只使用包含的算法" | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择分类算法" | |||
| options={classificationAlgorithms} | |||
| mode="multiple" | |||
| disabled={exclude_classifier?.length > 0} | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="排除分类算法" | |||
| name="exclude_classifier" | |||
| tooltip="如果不选,则使用所有可能的分类算法。否则,将排除包含的算法" | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="排除分类算法" | |||
| options={classificationAlgorithms} | |||
| mode="multiple" | |||
| disabled={include_classifier?.length > 0} | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ) : ( | |||
| <> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="回归算法" | |||
| name="include_regressor" | |||
| tooltip="如果不选,则使用所有可能的回归算法。否则,将只使用包含的算法" | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择回归算法" | |||
| options={regressorAlgorithms} | |||
| mode="multiple" | |||
| disabled={exclude_regressor?.length > 0} | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="排除的回归算法" | |||
| name="exclude_regressor" | |||
| tooltip="如果不选,则使用所有可能的回归算法。否则,将排除包含的算法" | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="排除回归算法" | |||
| options={regressorAlgorithms} | |||
| mode="multiple" | |||
| disabled={include_regressor?.length > 0} | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="集成方式" | |||
| name="ensemble_class" | |||
| tooltip="仅使用单个最佳模型还是集成模型" | |||
| > | |||
| <Radio.Group options={autoMLEnsembleClassOptions}></Radio.Group> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Form.Item dependencies={['ensemble_class']} noStyle> | |||
| {({ getFieldValue }) => { | |||
| return getFieldValue('ensemble_class') === AutoMLEnsembleClass.Default ? ( | |||
| <> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="集成模型数量" | |||
| name="ensemble_size" | |||
| tooltip="集成模型数量,如果设置为0,则没有集成。默认50" | |||
| > | |||
| <InputNumber placeholder="请输入集成模型数量" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="集成最佳模型数量" | |||
| name="ensemble_nbest" | |||
| tooltip="仅集成最佳的N个模型" | |||
| > | |||
| <InputNumber placeholder="请输入集成最佳模型数量" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ) : null; | |||
| }} | |||
| </Form.Item> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="最大数量" | |||
| name="max_models_on_disc" | |||
| tooltip="定义在磁盘中保存的模型的最大数量。额外的模型数量将被永久删除,它设置了一个集成可以使用多少个模型的上限。必须是大于等于1的整数,默认50" | |||
| > | |||
| <InputNumber placeholder="请输入最大数量" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="内存限制(MB)" | |||
| name="memory_limit" | |||
| tooltip="机器学习算法的内存限制(MB)。如果自动机器学习试图分配超过memory_limit MB,它将停止拟合机器学习算法。默认3072" | |||
| > | |||
| <InputNumber placeholder="请输入内存限制" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="单次时间限制(秒)" | |||
| name="per_run_time_limit" | |||
| tooltip="单次调用机器学习模型的时间限制(以秒为单位)。如果机器学习算法运行超过时间限制,将终止模型拟合,默认600" | |||
| > | |||
| <InputNumber placeholder="请输入时间限制" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="搜索时间限制(秒)" | |||
| name="time_left_for_this_task" | |||
| tooltip="搜索合适模型的时间限制(以秒为单位)。通过增加这个值,自动机器学习有更高的机会找到更好的模型。默认3600。" | |||
| > | |||
| <InputNumber placeholder="请输入搜索时间限制" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="测试集比率" | |||
| name="test_size" | |||
| tooltip="将数据划分为训练数据和测试数据,测试数据集所占比例,0到1之间" | |||
| > | |||
| <InputNumber placeholder="请输入测试集比率" min={0} max={1} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="计算指标" name="scoring_functions" tooltip="需要计算并打印的指标"> | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择计算指标" | |||
| options={ | |||
| task_type === AutoMLTaskType.Classification | |||
| ? classificationMetrics | |||
| : regressionMetrics | |||
| } | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="随机种子" name="seed" tooltip="随机种子,将决定输出文件名"> | |||
| <InputNumber placeholder="请输入随机种子" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <SubAreaTitle | |||
| title="重采样策略" | |||
| image={require('@/assets/img/resample-icon.png')} | |||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="重采样策略" | |||
| name="resampling_strategy" | |||
| tooltip="重采样策略,分为holdout和crossValid。holdout指定训练数据划分为训练集和验证集的比例。crossValid为交叉验证。" | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择重采样策略" | |||
| options={autoMLResamplingStrategyOptions} | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Form.Item dependencies={['resampling_strategy']} noStyle> | |||
| {({ getFieldValue }) => { | |||
| return getFieldValue('resampling_strategy') === AutoMLResamplingStrategy.CrossValid ? ( | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="交叉验证折数" | |||
| name="folds" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入交叉验证折数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入交叉验证折数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| ) : null; | |||
| }} | |||
| </Form.Item> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="是否打乱" name="shuffle" tooltip="拆分数据前是否打乱顺序"> | |||
| <Switch /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="训练集比率" | |||
| name="train_size" | |||
| tooltip="重采样划分训练集和验证集,训练集的比率,0到1之间" | |||
| > | |||
| <InputNumber placeholder="请输入训练集比率" min={0} max={1} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default ExecuteConfig; | |||
| @@ -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 ( | |||
| <> | |||
| <SubAreaTitle | |||
| title="优化指标" | |||
| image={require('@/assets/img/trial-config-icon.png')} | |||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||
| ></SubAreaTitle> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="指标名称" name="metric_name"> | |||
| <Input placeholder="请输入指标名称" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="指标权重" tooltip="用户可自定义优化指标的组合"> | |||
| <Form.List name="metrics"> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }, index) => ( | |||
| <Flex key={key} align="flex-start" className={styles['metrics-weight']}> | |||
| <Form.Item | |||
| style={{ flex: 1, marginBottom: 0, minWidth: 0 }} | |||
| {...restField} | |||
| name={[name, 'name']} | |||
| rules={[{ required: true, message: '请选择指标' }]} | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择指标" | |||
| popupMatchSelectWidth={false} | |||
| options={metricsOptions} | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| <span style={{ margin: '0 8px', lineHeight: '46px' }}>:</span> | |||
| <Form.Item | |||
| style={{ flex: 1, marginBottom: 0, minWidth: 0 }} | |||
| {...restField} | |||
| name={[name, 'value']} | |||
| rules={[{ required: true, message: '请输入指标权重' }]} | |||
| > | |||
| <InputNumber placeholder="请输入指标权重" min={0} precision={0} /> | |||
| </Form.Item> | |||
| <Flex | |||
| style={{ width: '76px', marginLeft: '18px', height: '46px' }} | |||
| align="center" | |||
| > | |||
| <Button | |||
| style={{ | |||
| marginRight: '3px', | |||
| }} | |||
| shape="circle" | |||
| size="middle" | |||
| type="text" | |||
| onClick={() => remove(name)} | |||
| icon={<MinusCircleOutlined />} | |||
| ></Button> | |||
| {index === fields.length - 1 && ( | |||
| <Button | |||
| shape="circle" | |||
| size="middle" | |||
| type="text" | |||
| onClick={() => add()} | |||
| icon={<PlusCircleOutlined />} | |||
| ></Button> | |||
| )} | |||
| </Flex> | |||
| </Flex> | |||
| ))} | |||
| {fields.length === 0 && ( | |||
| <Form.Item className={styles['add-weight']}> | |||
| <Button | |||
| className={styles['add-weight__button']} | |||
| color="primary" | |||
| variant="dashed" | |||
| onClick={() => add()} | |||
| block | |||
| icon={<PlusCircleOutlined />} | |||
| > | |||
| 添加指标权重 | |||
| </Button> | |||
| </Form.Item> | |||
| )} | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={0}> | |||
| <Col span={24}> | |||
| <Form.Item | |||
| label="优化方向" | |||
| name="greater_is_better" | |||
| rules={[{ required: true, message: '请选择优化方向' }]} | |||
| tooltip="指标组合优化的方向,是越大越好还是越小越好。" | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={true}>越大越好</Radio> | |||
| <Radio value={false}>越小越好</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| </> | |||
| ); | |||
| } | |||
| export default TrialConfig; | |||
| @@ -0,0 +1,20 @@ | |||
| .metrics-weight { | |||
| margin-bottom: 20px; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| } | |||
| .add-weight { | |||
| margin-bottom: 0 !important; | |||
| // 增加样式权重 | |||
| & &__button { | |||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||
| box-shadow: none !important; | |||
| &:hover { | |||
| border-style: solid; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| .experiment-history { | |||
| height: calc(100% - 10px); | |||
| margin-top: 10px; | |||
| &__content { | |||
| height: 100%; | |||
| padding: 20px @content-padding; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__table { | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,132 @@ | |||
| import { getFileReq } from '@/services/file'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender from '@/utils/table'; | |||
| import { Table, type TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentHistoryProps = { | |||
| fileUrl?: string; | |||
| isClassification: boolean; | |||
| }; | |||
| type TableData = { | |||
| id?: string; | |||
| accuracy?: number; | |||
| duration?: number; | |||
| train_loss?: number; | |||
| status?: string; | |||
| feature?: string; | |||
| althorithm?: string; | |||
| }; | |||
| function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| useEffect(() => { | |||
| if (fileUrl) { | |||
| getHistoryFile(); | |||
| } | |||
| }, [fileUrl]); | |||
| // 获取实验运行历史记录 | |||
| const getHistoryFile = async () => { | |||
| const [res] = await to(getFileReq(fileUrl)); | |||
| if (res) { | |||
| const data: any[] = res.data; | |||
| const list: TableData[] = data.map((item) => { | |||
| return { | |||
| id: item[0]?.[0], | |||
| accuracy: item[1]?.[5]?.accuracy, | |||
| duration: item[1]?.[5]?.duration, | |||
| train_loss: item[1]?.[5]?.train_loss, | |||
| status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], | |||
| }; | |||
| }); | |||
| list.forEach((item) => { | |||
| if (!item.id) return; | |||
| const config = (res as any).configs?.[item.id]; | |||
| item.feature = config?.['feature_preprocessor:__choice__']; | |||
| item.althorithm = isClassification | |||
| ? config?.['classifier:__choice__'] | |||
| : config?.['regressor:__choice__']; | |||
| }); | |||
| setTableData(list); | |||
| } | |||
| }; | |||
| const columns: TableProps<TableData>['columns'] = [ | |||
| { | |||
| title: 'ID', | |||
| dataIndex: 'id', | |||
| key: 'id', | |||
| width: 80, | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '准确率', | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '耗时', | |||
| dataIndex: 'duration', | |||
| key: 'duration', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '训练损失', | |||
| dataIndex: 'train_loss', | |||
| key: 'train_loss', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '特征处理', | |||
| dataIndex: 'feature', | |||
| key: 'feature', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '算法', | |||
| dataIndex: 'althorithm', | |||
| key: 'althorithm', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| render: tableCellRender(false), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| styles['experiment-history__content__table'], | |||
| )} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| pagination={false} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentHistory; | |||
| @@ -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; | |||
| } | |||
| @@ -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 ( | |||
| <div> | |||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||
| <div className={styles.check}> | |||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||
| </div> | |||
| <div className={styles.index}>序号</div> | |||
| <div className={styles.description}>运行时长</div> | |||
| <div className={styles.startTime}>开始时间</div> | |||
| <div className={styles.status}>状态</div> | |||
| <div className={styles.operation}> | |||
| <span>操作</span> | |||
| {selectedIns.length > 0 && ( | |||
| <Button | |||
| style={{ position: 'absolute', right: '0' }} | |||
| color="primary" | |||
| variant="filled" | |||
| size="small" | |||
| onClick={handleDeleteAll} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| )} | |||
| </div> | |||
| </div> | |||
| {experimentInsList.map((item, index) => ( | |||
| <div | |||
| key={item.id} | |||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | |||
| > | |||
| <div className={styles.check}> | |||
| <Checkbox | |||
| checked={isSingleChecked(item.id)} | |||
| onChange={() => checkSingle(item.id)} | |||
| ></Checkbox> | |||
| </div> | |||
| <a | |||
| className={styles.index} | |||
| style={{ padding: '0 16px' }} | |||
| onClick={() => onClickInstance?.(item)} | |||
| > | |||
| {index + 1} | |||
| </a> | |||
| <div className={styles.description}> | |||
| {elapsedTime(item.create_time, item.finish_time)} | |||
| </div> | |||
| <div className={styles.startTime}> | |||
| <Tooltip title={formatDate(item.create_time)}> | |||
| <span>{formatDate(item.create_time)}</span> | |||
| </Tooltip> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[item.status as ExperimentStatus]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span | |||
| style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }} | |||
| className={styles.statusIcon} | |||
| > | |||
| {experimentStatusInfo[item.status as ExperimentStatus]?.label} | |||
| </span> | |||
| </div> | |||
| <div className={styles.operation}> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="stop" | |||
| disabled={ | |||
| item.status === ExperimentStatus.Succeeded || | |||
| item.status === ExperimentStatus.Failed || | |||
| item.status === ExperimentStatus.Terminated | |||
| } | |||
| icon={<KFIcon type="icon-zhongzhi" />} | |||
| onClick={() => terminateExperimentInstance(item)} | |||
| > | |||
| 终止 | |||
| </Button> | |||
| <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| colorLink: themes['warningColor'], | |||
| }, | |||
| }} | |||
| > | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="batchRemove" | |||
| disabled={ | |||
| item.status === ExperimentStatus.Running || | |||
| item.status === ExperimentStatus.Pending | |||
| } | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleRemove(item)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| </ConfigProvider> | |||
| </div> | |||
| </div> | |||
| ))} | |||
| {experimentInsTotal > experimentInsList.length ? ( | |||
| <div className={styles.loadMoreBox}> | |||
| <Button type="link" onClick={onLoadMore}> | |||
| 更多 | |||
| <DoubleRightOutlined rotate={90} /> | |||
| </Button> | |||
| </div> | |||
| ) : null} | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentInstanceComponent; | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string | undefined>(''); | |||
| 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 ( | |||
| <div className={styles['experiment-result']}> | |||
| <ConfigTitle title="实验结果"></ConfigTitle> | |||
| <div className={styles['experiment-result__text']}>{result}</div> | |||
| <ConfigTitle title="可视化结果"></ConfigTitle> | |||
| <div className={styles['experiment-result__image-container']}> | |||
| {images.map((item, index) => ( | |||
| <img | |||
| key={index} | |||
| className={styles['experiment-result__image-container__image']} | |||
| src={item} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentResult; | |||
| @@ -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; | |||
| }; | |||
| @@ -39,4 +39,10 @@ | |||
| margin-right: 6px; | |||
| border-radius: 50%; | |||
| } | |||
| &__log { | |||
| height: 100%; | |||
| padding: 8px; | |||
| background: white; | |||
| } | |||
| } | |||
| @@ -48,14 +48,16 @@ const ExperimentDrawer = ({ | |||
| key: '1', | |||
| label: '日志详情', | |||
| children: ( | |||
| <LogList | |||
| instanceName={instanceName} | |||
| instanceNamespace={instanceNamespace} | |||
| pipelineNodeId={instanceNodeData.id} | |||
| workflowId={workflowId} | |||
| instanceNodeStartTime={instanceNodeStartTime} | |||
| instanceNodeStatus={instanceNodeStatus} | |||
| ></LogList> | |||
| <div className={styles['experiment-drawer__log']}> | |||
| <LogList | |||
| instanceName={instanceName} | |||
| instanceNamespace={instanceNamespace} | |||
| pipelineNodeId={instanceNodeData.id} | |||
| workflowId={workflowId} | |||
| instanceNodeStartTime={instanceNodeStartTime} | |||
| instanceNodeStatus={instanceNodeStatus} | |||
| ></LogList> | |||
| </div> | |||
| ), | |||
| icon: <ProfileOutlined />, | |||
| }, | |||
| @@ -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}"}`, | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -503,59 +503,69 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </Form.Item> | |||
| ))} | |||
| <div className={styles['pipeline-drawer__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输入参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {inParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| label={getLabel(item, 'in_parameters')} | |||
| required={item.value.require ? true : false} | |||
| > | |||
| <div className={styles['pipeline-drawer__ref-row']}> | |||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||
| {item.value.type === 'select' ? ( | |||
| <ParameterSelect /> | |||
| ) : ( | |||
| <ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput> | |||
| )} | |||
| {/* 输入参数 */} | |||
| {inParametersList.length > 0 && ( | |||
| <> | |||
| <div className={styles['pipeline-drawer__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输入参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {inParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| label={getLabel(item, 'in_parameters')} | |||
| required={item.value.require ? true : false} | |||
| > | |||
| <div className={styles['pipeline-drawer__ref-row']}> | |||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||
| {item.value.type === 'select' ? ( | |||
| <ParameterSelect /> | |||
| ) : ( | |||
| <ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput> | |||
| )} | |||
| </Form.Item> | |||
| {item.value.type === 'ref' && ( | |||
| <Form.Item noStyle> | |||
| <Button | |||
| size="small" | |||
| type="link" | |||
| icon={getSelectBtnIcon(item.value)} | |||
| onClick={() => selectRefData(['in_parameters', item.key], item.value)} | |||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||
| > | |||
| {item.value.label} | |||
| </Button> | |||
| </Form.Item> | |||
| )} | |||
| </div> | |||
| </Form.Item> | |||
| {item.value.type === 'ref' && ( | |||
| <Form.Item noStyle> | |||
| <Button | |||
| size="small" | |||
| type="link" | |||
| icon={getSelectBtnIcon(item.value)} | |||
| onClick={() => selectRefData(['in_parameters', item.key], item.value)} | |||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||
| > | |||
| {item.value.label} | |||
| </Button> | |||
| </Form.Item> | |||
| )} | |||
| ))} | |||
| </> | |||
| )} | |||
| {/* 输出参数 */} | |||
| {outParametersList.length > 0 && ( | |||
| <> | |||
| <div className={styles['pipeline-drawer__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输出参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| </Form.Item> | |||
| ))} | |||
| <div className={styles['pipeline-drawer__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输出参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {outParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['out_parameters', item.key]} | |||
| required={item.value.require ? true : false} | |||
| label={getLabel(item, 'out_parameters')} | |||
| rules={getFormRules(item)} | |||
| > | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </Form.Item> | |||
| ))} | |||
| {outParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['out_parameters', item.key]} | |||
| required={item.value.require ? true : false} | |||
| label={getLabel(item, 'out_parameters')} | |||
| rules={getFormRules(item)} | |||
| > | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </Form.Item> | |||
| ))} | |||
| </> | |||
| )} | |||
| </Form> | |||
| </Drawer> | |||
| ); | |||
| @@ -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(); | |||
| @@ -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 | |||
| }); | |||
| } | |||
| @@ -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 | |||
| }); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| }; | |||
| @@ -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('复制失败'); | |||
| }); | |||
| @@ -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<T, M>( | |||
| fn: (value: T) => M | undefined | null, | |||
| ): (value: T | undefined | null) => M | undefined | null { | |||
| @@ -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; | |||
| }; | |||
| @@ -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); | |||