Browse Source

Merge pull request '合并dev-zw' (#147) from dev-zw into dev

dev-complex-computation
cp3hnu 1 year ago
parent
commit
2d99a3b093
69 changed files with 3520 additions and 197 deletions
  1. +45
    -0
      package-lock.json
  2. +5
    -0
      package.json
  3. +31
    -0
      react-ui/config/routes.ts
  4. +5
    -4
      react-ui/src/app.tsx
  5. BIN
      react-ui/src/assets/img/dataset-config-icon.png
  6. BIN
      react-ui/src/assets/img/editor-parameter.png
  7. BIN
      react-ui/src/assets/img/mirror-basic.png
  8. BIN
      react-ui/src/assets/img/model-deployment.png
  9. BIN
      react-ui/src/assets/img/resample-icon.png
  10. BIN
      react-ui/src/assets/img/search-config-icon.png
  11. BIN
      react-ui/src/assets/img/trial-config-icon.png
  12. +113
    -0
      react-ui/src/components/BasicInfo/components.tsx
  13. +48
    -0
      react-ui/src/components/BasicInfo/format.ts
  14. +5
    -105
      react-ui/src/components/BasicInfo/index.tsx
  15. +14
    -0
      react-ui/src/components/BasicInfo/types.ts
  16. +3
    -1
      react-ui/src/components/BasicTableInfo/index.tsx
  17. +6
    -0
      react-ui/src/components/KFBreadcrumb/index.tsx
  18. +2
    -2
      react-ui/src/components/KFIcon/index.tsx
  19. +35
    -1
      react-ui/src/enums/index.ts
  20. +1
    -1
      react-ui/src/global.less
  21. +1
    -1
      react-ui/src/iconfont/iconfont.js
  22. +8
    -0
      react-ui/src/overrides.less
  23. +6
    -0
      react-ui/src/pages/Application/index.tsx
  24. +0
    -1
      react-ui/src/pages/Authorize/index.tsx
  25. +55
    -0
      react-ui/src/pages/AutoML/Create/index.less
  26. +219
    -0
      react-ui/src/pages/AutoML/Create/index.tsx
  27. +40
    -0
      react-ui/src/pages/AutoML/Info/index.less
  28. +61
    -0
      react-ui/src/pages/AutoML/Info/index.tsx
  29. +42
    -0
      react-ui/src/pages/AutoML/Instance/index.less
  30. +200
    -0
      react-ui/src/pages/AutoML/Instance/index.tsx
  31. +20
    -0
      react-ui/src/pages/AutoML/List/index.less
  32. +414
    -0
      react-ui/src/pages/AutoML/List/index.tsx
  33. +13
    -0
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.less
  34. +310
    -0
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx
  35. +40
    -0
      react-ui/src/pages/AutoML/components/ConfigInfo/index.less
  36. +48
    -0
      react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx
  37. +39
    -0
      react-ui/src/pages/AutoML/components/ConfigTitle/index.less
  38. +22
    -0
      react-ui/src/pages/AutoML/components/ConfigTitle/index.tsx
  39. +18
    -0
      react-ui/src/pages/AutoML/components/CopyingText/index.less
  40. +30
    -0
      react-ui/src/pages/AutoML/components/CopyingText/index.tsx
  41. +53
    -0
      react-ui/src/pages/AutoML/components/CreateForm/BasicConfig.tsx
  42. +59
    -0
      react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx
  43. +455
    -0
      react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx
  44. +130
    -0
      react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx
  45. +20
    -0
      react-ui/src/pages/AutoML/components/CreateForm/index.less
  46. +14
    -0
      react-ui/src/pages/AutoML/components/ExperimentHistory/index.less
  47. +132
    -0
      react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx
  48. +71
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.less
  49. +229
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx
  50. +0
    -0
      react-ui/src/pages/AutoML/components/ExperimentLog/index.less
  51. +0
    -0
      react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx
  52. +39
    -0
      react-ui/src/pages/AutoML/components/ExperimentResult/index.less
  53. +56
    -0
      react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx
  54. +85
    -0
      react-ui/src/pages/AutoML/types.ts
  55. +6
    -0
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less
  56. +10
    -8
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  57. +2
    -2
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  58. +5
    -1
      react-ui/src/pages/Experiment/components/LogList/index.less
  59. +2
    -1
      react-ui/src/pages/Experiment/index.jsx
  60. +61
    -51
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  61. +2
    -1
      react-ui/src/requestConfig.ts
  62. +93
    -0
      react-ui/src/services/autoML/index.js
  63. +20
    -0
      react-ui/src/services/file/index.js
  64. +17
    -16
      react-ui/src/styles/menu.less
  65. +10
    -0
      react-ui/src/types.ts
  66. +12
    -0
      react-ui/src/utils/clipboard.js
  67. +10
    -0
      react-ui/src/utils/functional.ts
  68. +26
    -1
      react-ui/src/utils/index.ts
  69. +2
    -0
      react-ui/src/utils/sessionStorage.ts

+ 45
- 0
package-lock.json View File

@@ -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=="
}
}
}

+ 5
- 0
package.json View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"clipboard": "~2.0.11"
}
}

+ 31
- 0
react-ui/config/routes.ts View File

@@ -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',
},
],
},
],
},
{


+ 5
- 4
react-ui/src/app.tsx View File

@@ -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();


BIN
react-ui/src/assets/img/dataset-config-icon.png View File

Before After
Width: 51  |  Height: 51  |  Size: 3.4 kB

BIN
react-ui/src/assets/img/editor-parameter.png View File

Before After
Width: 34  |  Height: 31  |  Size: 1.3 kB Width: 51  |  Height: 47  |  Size: 1.8 kB

BIN
react-ui/src/assets/img/mirror-basic.png View File

Before After
Width: 14  |  Height: 15  |  Size: 468 B Width: 51  |  Height: 51  |  Size: 1.5 kB

BIN
react-ui/src/assets/img/model-deployment.png View File

Before After
Width: 48  |  Height: 47  |  Size: 1.6 kB Width: 51  |  Height: 51  |  Size: 1.7 kB

BIN
react-ui/src/assets/img/resample-icon.png View File

Before After
Width: 51  |  Height: 51  |  Size: 1.3 kB

BIN
react-ui/src/assets/img/search-config-icon.png View File

Before After
Width: 51  |  Height: 51  |  Size: 1.6 kB

BIN
react-ui/src/assets/img/trial-config-icon.png View File

Before After
Width: 51  |  Height: 51  |  Size: 1.7 kB

+ 113
- 0
react-ui/src/components/BasicInfo/components.tsx View File

@@ -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>
);
}

+ 48
- 0
react-ui/src/components/BasicInfo/format.ts View File

@@ -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 : '--';
};
};

+ 5
- 105
react-ui/src/components/BasicInfo/index.tsx View File

@@ -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>
);
}

+ 14
- 0
react-ui/src/components/BasicInfo/types.ts View File

@@ -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;
};

+ 3
- 1
react-ui/src/components/BasicTableInfo/index.tsx View File

@@ -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 = {


+ 6
- 0
react-ui/src/components/KFBreadcrumb/index.tsx View File

@@ -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'; // 导入你的路由配置


+ 2
- 2
react-ui/src/components/KFIcon/index.tsx View File

@@ -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;

+ 35
- 1
react-ui/src/enums/index.ts View File

@@ -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 },
];

+ 1
- 1
react-ui/src/global.less View File

@@ -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;


+ 1
- 1
react-ui/src/iconfont/iconfont.js
File diff suppressed because it is too large
View File


+ 8
- 0
react-ui/src/overrides.less View File

@@ -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%;


+ 6
- 0
react-ui/src/pages/Application/index.tsx View File

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-09-02 08:42:57
* @Description: 应用开发
*/

import IframePage, { IframePageType } from '@/components/IFramePage';

function Application() {


+ 0
- 1
react-ui/src/pages/Authorize/index.tsx View File

@@ -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);


+ 55
- 0
react-ui/src/pages/AutoML/Create/index.less View File

@@ -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;
}
}
}
}

+ 219
- 0
react-ui/src/pages/AutoML/Create/index.tsx View File

@@ -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;

+ 40
- 0
react-ui/src/pages/AutoML/Info/index.less View File

@@ -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: '';
}
}
}

+ 61
- 0
react-ui/src/pages/AutoML/Info/index.tsx View File

@@ -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;

+ 42
- 0
react-ui/src/pages/AutoML/Instance/index.less View File

@@ -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;
}
}

+ 200
- 0
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -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;

+ 20
- 0
react-ui/src/pages/AutoML/List/index.less View File

@@ -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;
}
}
}

+ 414
- 0
react-ui/src/pages/AutoML/List/index.tsx View File

@@ -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;

+ 13
- 0
react-ui/src/pages/AutoML/components/AutoMLBasic/index.less View File

@@ -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;
}
}
}

+ 310
- 0
react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx View File

@@ -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;

+ 40
- 0
react-ui/src/pages/AutoML/components/ConfigInfo/index.less View File

@@ -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);
}
}
}
}
}

+ 48
- 0
react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx View File

@@ -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;

+ 39
- 0
react-ui/src/pages/AutoML/components/ConfigTitle/index.less View File

@@ -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: '';
}
}
}

+ 22
- 0
react-ui/src/pages/AutoML/components/ConfigTitle/index.tsx View File

@@ -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;

+ 18
- 0
react-ui/src/pages/AutoML/components/CopyingText/index.less View File

@@ -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;
}
}

+ 30
- 0
react-ui/src/pages/AutoML/components/CopyingText/index.tsx View File

@@ -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;

+ 53
- 0
react-ui/src/pages/AutoML/components/CreateForm/BasicConfig.tsx View File

@@ -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;

+ 59
- 0
react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx View File

@@ -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;

+ 455
- 0
react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx View File

@@ -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;

+ 130
- 0
react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx View File

@@ -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;

+ 20
- 0
react-ui/src/pages/AutoML/components/CreateForm/index.less View File

@@ -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;
}
}
}

+ 14
- 0
react-ui/src/pages/AutoML/components/ExperimentHistory/index.less View File

@@ -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%;
}
}
}

+ 132
- 0
react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx View File

@@ -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;

+ 71
- 0
react-ui/src/pages/AutoML/components/ExperimentInstance/index.less View File

@@ -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;
}

+ 229
- 0
react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx View File

@@ -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
react-ui/src/pages/AutoML/components/ExperimentLog/index.less View File


+ 0
- 0
react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx View File


+ 39
- 0
react-ui/src/pages/AutoML/components/ExperimentResult/index.less View File

@@ -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;
}
}
}
}

+ 56
- 0
react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx View File

@@ -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;

+ 85
- 0
react-ui/src/pages/AutoML/types.ts View File

@@ -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;
};

+ 6
- 0
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less View File

@@ -39,4 +39,10 @@
margin-right: 6px;
border-radius: 50%;
}

&__log {
height: 100%;
padding: 8px;
background: white;
}
}

+ 10
- 8
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -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 />,
},


+ 2
- 2
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -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}"}`,


+ 5
- 1
react-ui/src/pages/Experiment/components/LogList/index.less View File

@@ -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);
}
}

+ 2
- 1
react-ui/src/pages/Experiment/index.jsx View File

@@ -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


+ 61
- 51
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -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>
);


+ 2
- 1
react-ui/src/requestConfig.ts View File

@@ -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();


+ 93
- 0
react-ui/src/services/autoML/index.js View File

@@ -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
});
}


+ 20
- 0
react-ui/src/services/file/index.js View File

@@ -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
});
}

+ 17
- 16
react-ui/src/styles/menu.less View File

@@ -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;
}
}

+ 10
- 0
react-ui/src/types.ts View File

@@ -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;
};

+ 12
- 0
react-ui/src/utils/clipboard.js View File

@@ -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('复制失败');
});

+ 10
- 0
react-ui/src/utils/functional.ts View File

@@ -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 {


+ 26
- 1
react-ui/src/utils/index.ts View File

@@ -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;
};

+ 2
- 0
react-ui/src/utils/sessionStorage.ts View File

@@ -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);


Loading…
Cancel
Save