Browse Source

Merge remote-tracking branch 'origin/dev'

master-arm
chenzhihang 1 year ago
parent
commit
2a2f4b13c4
100 changed files with 3658 additions and 1516 deletions
  1. +1
    -0
      react-ui/config/config.ts
  2. +70
    -13
      react-ui/config/routes.ts
  3. +0
    -176
      react-ui/mock/listTableList.ts
  4. +0
    -1
      react-ui/package.json
  5. BIN
      react-ui/public/assets/images/component-icon-9-Failed.png
  6. BIN
      react-ui/public/assets/images/component-icon-9-Omitted.png
  7. BIN
      react-ui/public/assets/images/component-icon-9-Pending.png
  8. BIN
      react-ui/public/assets/images/component-icon-9-Running.png
  9. BIN
      react-ui/public/assets/images/component-icon-9-Skipped.png
  10. BIN
      react-ui/public/assets/images/component-icon-9-Succeeded.png
  11. BIN
      react-ui/public/assets/images/component-icon-9.png
  12. +5
    -4
      react-ui/src/app.tsx
  13. BIN
      react-ui/src/assets/img/dataset-config-icon.png
  14. BIN
      react-ui/src/assets/img/editor-parameter.png
  15. BIN
      react-ui/src/assets/img/mirror-basic.png
  16. BIN
      react-ui/src/assets/img/model-deployment.png
  17. BIN
      react-ui/src/assets/img/resample-icon.png
  18. BIN
      react-ui/src/assets/img/search-config-icon.png
  19. BIN
      react-ui/src/assets/img/trial-config-icon.png
  20. +0
    -37
      react-ui/src/components/ArrayTableCell/index.tsx
  21. +113
    -0
      react-ui/src/components/BasicInfo/components.tsx
  22. +48
    -0
      react-ui/src/components/BasicInfo/format.ts
  23. +5
    -105
      react-ui/src/components/BasicInfo/index.tsx
  24. +14
    -0
      react-ui/src/components/BasicInfo/types.ts
  25. +3
    -1
      react-ui/src/components/BasicTableInfo/index.tsx
  26. +1
    -1
      react-ui/src/components/CodeSelect/index.tsx
  27. +0
    -45
      react-ui/src/components/Footer/index.tsx
  28. +0
    -64
      react-ui/src/components/IconSelector/Category.tsx
  29. +0
    -44
      react-ui/src/components/IconSelector/CopyableIcon.tsx
  30. +0
    -237
      react-ui/src/components/IconSelector/IconPicSearcher.tsx
  31. +0
    -223
      react-ui/src/components/IconSelector/fields.ts
  32. +0
    -146
      react-ui/src/components/IconSelector/index.tsx
  33. +0
    -137
      react-ui/src/components/IconSelector/style.less
  34. +0
    -40
      react-ui/src/components/IconSelector/themeIcons.tsx
  35. +11
    -0
      react-ui/src/components/InfoGroup/index.less
  36. +34
    -0
      react-ui/src/components/InfoGroup/index.tsx
  37. +39
    -0
      react-ui/src/components/InfoGroupTitle/index.less
  38. +25
    -0
      react-ui/src/components/InfoGroupTitle/index.tsx
  39. +6
    -0
      react-ui/src/components/KFBreadcrumb/index.tsx
  40. +0
    -7
      react-ui/src/components/KFConfirmModal/index.less
  41. +0
    -37
      react-ui/src/components/KFConfirmModal/index.tsx
  42. +2
    -2
      react-ui/src/components/KFIcon/index.tsx
  43. +22
    -0
      react-ui/src/components/ParameterSelect/config.tsx
  44. +35
    -1
      react-ui/src/enums/index.ts
  45. +1
    -1
      react-ui/src/global.less
  46. +15
    -1
      react-ui/src/hooks/resource.ts
  47. +6
    -0
      react-ui/src/hooks/sessionStorage.ts
  48. +1
    -1
      react-ui/src/iconfont/iconfont.js
  49. +8
    -0
      react-ui/src/overrides.less
  50. +6
    -0
      react-ui/src/pages/Application/index.tsx
  51. +0
    -1
      react-ui/src/pages/Authorize/index.tsx
  52. +55
    -0
      react-ui/src/pages/AutoML/Create/index.less
  53. +214
    -0
      react-ui/src/pages/AutoML/Create/index.tsx
  54. +40
    -0
      react-ui/src/pages/AutoML/Info/index.less
  55. +61
    -0
      react-ui/src/pages/AutoML/Info/index.tsx
  56. +42
    -0
      react-ui/src/pages/AutoML/Instance/index.less
  57. +215
    -0
      react-ui/src/pages/AutoML/Instance/index.tsx
  58. +20
    -0
      react-ui/src/pages/AutoML/List/index.less
  59. +411
    -0
      react-ui/src/pages/AutoML/List/index.tsx
  60. +13
    -0
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.less
  61. +308
    -0
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx
  62. +20
    -0
      react-ui/src/pages/AutoML/components/ConfigInfo/index.less
  63. +26
    -0
      react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx
  64. +18
    -0
      react-ui/src/pages/AutoML/components/CopyingText/index.less
  65. +30
    -0
      react-ui/src/pages/AutoML/components/CopyingText/index.tsx
  66. +53
    -0
      react-ui/src/pages/AutoML/components/CreateForm/BasicConfig.tsx
  67. +59
    -0
      react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx
  68. +455
    -0
      react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx
  69. +130
    -0
      react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx
  70. +20
    -0
      react-ui/src/pages/AutoML/components/CreateForm/index.less
  71. +14
    -0
      react-ui/src/pages/AutoML/components/ExperimentHistory/index.less
  72. +132
    -0
      react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx
  73. +71
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.less
  74. +229
    -0
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx
  75. +0
    -0
      react-ui/src/pages/AutoML/components/ExperimentLog/index.less
  76. +0
    -0
      react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx
  77. +52
    -0
      react-ui/src/pages/AutoML/components/ExperimentResult/index.less
  78. +83
    -0
      react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx
  79. +85
    -0
      react-ui/src/pages/AutoML/types.ts
  80. +3
    -0
      react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx
  81. +4
    -0
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  82. +2
    -2
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  83. +1
    -0
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  84. +3
    -23
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  85. +12
    -0
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less
  86. +13
    -10
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  87. +4
    -4
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  88. +5
    -1
      react-ui/src/pages/Experiment/components/LogList/index.less
  89. +2
    -1
      react-ui/src/pages/Experiment/index.jsx
  90. +35
    -28
      react-ui/src/pages/ModelDeployment/CreateService/index.tsx
  91. +35
    -37
      react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx
  92. +10
    -15
      react-ui/src/pages/ModelDeployment/List/index.tsx
  93. +16
    -5
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  94. +2
    -2
      react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx
  95. +1
    -1
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx
  96. +82
    -60
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  97. +5
    -0
      react-ui/src/pages/System/User/edit.tsx
  98. +1
    -1
      react-ui/src/pages/System/User/index.tsx
  99. +2
    -1
      react-ui/src/requestConfig.ts
  100. +93
    -0
      react-ui/src/services/autoML/index.js

+ 1
- 0
react-ui/config/config.ts View File

@@ -156,4 +156,5 @@ export default defineConfig({
},
javascriptEnabled: true,
},
valtio: {},
});

+ 70
- 13
react-ui/config/routes.ts View File

@@ -145,6 +145,42 @@ 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: 'copy/:id',
component: './AutoML/Create/index',
},
{
name: '实验实例详情',
path: 'instance/:autoMLId/:id',
component: './AutoML/Instance/index',
},
],
},
],
},
{
@@ -230,25 +266,46 @@ export default [
path: '',
component: './ModelDeployment/List',
},
{
name: '服务详情',
path: 'serviceInfo/:id',
component: './ModelDeployment/ServiceInfo',
},
{
name: '服务版本详情',
path: 'versionInfo/:id',
component: './ModelDeployment/VersionInfo',
},
{
name: '创建推理服务',
path: 'createService',
component: './ModelDeployment/CreateService',
},
{
name: '新增服务版本',
path: 'addVersion/:id',
component: './ModelDeployment/CreateVersion',
name: '编辑推理服务',
path: 'editService/:serviceId',
component: './ModelDeployment/CreateService',
},
{
name: '服务详情',
path: 'serviceInfo/:serviceId',
routes: [
{
name: '服务详情',
path: '',
component: './ModelDeployment/ServiceInfo',
},
{
name: '新增服务版本',
path: 'createVersion',
component: './ModelDeployment/CreateVersion',
},
{
name: '更新服务版本',
path: 'updateVersion',
component: './ModelDeployment/CreateVersion',
},
{
name: '重启服务版本',
path: 'restartVersion',
component: './ModelDeployment/CreateVersion',
},
{
name: '服务版本详情',
path: 'versionInfo/:id',
component: './ModelDeployment/VersionInfo',
},
],
},
],
},


+ 0
- 176
react-ui/mock/listTableList.ts View File

@@ -1,176 +0,0 @@
import { Request, Response } from 'express';
import moment from 'moment';
import { parse } from 'url';

// mock tableListDataSource
const genList = (current: number, pageSize: number) => {
const tableListDataSource: API.RuleListItem[] = [];

for (let i = 0; i < pageSize; i += 1) {
const index = (current - 1) * 10 + i;
tableListDataSource.push({
key: index,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${index}`,
owner: '曲丽丽',
desc: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: moment().format('YYYY-MM-DD'),
createdAt: moment().format('YYYY-MM-DD'),
progress: Math.ceil(Math.random() * 100),
});
}
tableListDataSource.reverse();
return tableListDataSource;
};

let tableListDataSource = genList(1, 100);

function getRule(req: Request, res: Response, u: string) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
const params = parse(realUrl, true).query as unknown as API.PageParams &
API.RuleListItem & {
sorter: any;
filter: any;
};

let dataSource = [...tableListDataSource].slice(
((current as number) - 1) * (pageSize as number),
(current as number) * (pageSize as number),
);
if (params.sorter) {
const sorter = JSON.parse(params.sorter);
dataSource = dataSource.sort((prev, next) => {
let sortNumber = 0;
(Object.keys(sorter) as Array<keyof API.RuleListItem>).forEach((key) => {
let nextSort = next?.[key] as number;
let preSort = prev?.[key] as number;
if (sorter[key] === 'descend') {
if (preSort - nextSort > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (preSort - nextSort > 0) {
sortNumber += 1;
} else {
sortNumber += -1;
}
});
return sortNumber;
});
}
if (params.filter) {
const filter = JSON.parse(params.filter as any) as {
[key: string]: string[];
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return (Object.keys(filter) as Array<keyof API.RuleListItem>).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}

if (params.name) {
dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
}
const result = {
data: dataSource,
total: tableListDataSource.length,
success: true,
pageSize,
current: parseInt(`${params.current}`, 10) || 1,
};

return res.json(result);
}

function postRule(req: Request, res: Response, u: string, b: Request) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}

const body = (b && b.body) || req.body;
const { method, name, desc, key } = body;

switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {
const i = Math.ceil(Math.random() * 10000);
const newRule: API.RuleListItem = {
key: tableListDataSource.length,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name,
owner: '曲丽丽',
desc,
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 2,
updatedAt: moment().format('YYYY-MM-DD'),
createdAt: moment().format('YYYY-MM-DD'),
progress: Math.ceil(Math.random() * 100),
};
tableListDataSource.unshift(newRule);
return res.json(newRule);
})();
return;

case 'update':
(() => {
let newRule = {};
tableListDataSource = tableListDataSource.map((item) => {
if (item.key === key) {
newRule = { ...item, desc, name };
return { ...item, desc, name };
}
return item;
});
return res.json(newRule);
})();
return;
default:
break;
}

const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};

res.json(result);
}

export default {
'GET /api/rule': getRule,
'POST /api/rule': postRule,
};

+ 0
- 1
react-ui/package.json View File

@@ -67,7 +67,6 @@
"fabric": "^5.3.0",
"highlight.js": "^11.7.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"omit.js": "^2.0.2",
"pnpm": "^8.9.0",
"query-string": "^8.1.0",


BIN
react-ui/public/assets/images/component-icon-9-Failed.png View File

Before After
Width: 108  |  Height: 108  |  Size: 4.0 kB

BIN
react-ui/public/assets/images/component-icon-9-Omitted.png View File

Before After
Width: 108  |  Height: 108  |  Size: 3.8 kB

BIN
react-ui/public/assets/images/component-icon-9-Pending.png View File

Before After
Width: 108  |  Height: 108  |  Size: 4.0 kB

BIN
react-ui/public/assets/images/component-icon-9-Running.png View File

Before After
Width: 108  |  Height: 108  |  Size: 3.8 kB

BIN
react-ui/public/assets/images/component-icon-9-Skipped.png View File

Before After
Width: 108  |  Height: 108  |  Size: 3.8 kB

BIN
react-ui/public/assets/images/component-icon-9-Succeeded.png View File

Before After
Width: 108  |  Height: 108  |  Size: 4.1 kB

BIN
react-ui/public/assets/images/component-icon-9.png View File

Before After
Width: 72  |  Height: 72  |  Size: 2.3 kB

+ 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

+ 0
- 37
react-ui/src/components/ArrayTableCell/index.tsx View File

@@ -1,37 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-28 14:18:11
* @Description: 自定义 Table 数组类单元格
*/

import { Tooltip } from 'antd';

function ArrayTableCell(ellipsis: boolean = false, property?: string) {
return (value?: any | null) => {
if (
value === undefined ||
value === null ||
Array.isArray(value) === false ||
value.length === 0
) {
return <span>--</span>;
}

let list = value;
if (property && typeof value[0] === 'object') {
list = value.map((item) => item[property]);
}
const text = list.join(',');
if (ellipsis) {
return (
<Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
<span>{text}</span>;
</Tooltip>
);
} else {
return <span>{text}</span>;
}
};
}

export default ArrayTableCell;

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


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

@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-10-08 15:36:08
* @Description: 代码配置选择表单组件
* @Description: 流水线选择代码配置表单
*/

import KFIcon from '@/components/KFIcon';


+ 0
- 45
react-ui/src/components/Footer/index.tsx View File

@@ -1,45 +0,0 @@
import { GithubOutlined } from '@ant-design/icons';
import { DefaultFooter } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import React from 'react';

const Footer: React.FC = () => {
const intl = useIntl();
const defaultMessage = intl.formatMessage({
id: 'app.copyright.produced',
defaultMessage: '蚂蚁集团体验技术部出品',
});

const currentYear = new Date().getFullYear();

return (
<DefaultFooter
style={{
background: 'none',
}}
copyright={`${currentYear} ${defaultMessage}`}
links={[
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
blankTarget: true,
},
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
/>
);
};

export default Footer;

+ 0
- 64
react-ui/src/components/IconSelector/Category.tsx View File

@@ -1,64 +0,0 @@
import { useIntl } from '@umijs/max';
import * as React from 'react';
import CopyableIcon from './CopyableIcon';
import type { CategoriesKeys } from './fields';
import type { ThemeType } from './index';
import styles from './style.less';

interface CategoryProps {
title: CategoriesKeys;
icons: string[];
theme: ThemeType;
newIcons: string[];
onSelect: (type: string, name: string) => any;
}

const Category: React.FC<CategoryProps> = (props) => {
const { icons, title, newIcons, theme } = props;
const intl = useIntl();
const [justCopied, setJustCopied] = React.useState<string | null>(null);
const copyId = React.useRef<NodeJS.Timeout | null>(null);
const onSelect = React.useCallback((type: string, text: string) => {
const { onSelect } = props;
if (onSelect) {
onSelect(type, text);
}
setJustCopied(type);
copyId.current = setTimeout(() => {
setJustCopied(null);
}, 2000);
}, []);
React.useEffect(
() => () => {
if (copyId.current) {
clearTimeout(copyId.current);
}
},
[],
);

return (
<div>
<h4>
{intl.formatMessage({
id: `app.docs.components.icon.category.${title}`,
defaultMessage: '信息',
})}
</h4>
<ul className={styles.anticonsList}>
{icons.map((name) => (
<CopyableIcon
key={name}
name={name}
theme={theme}
isNew={newIcons.includes(name)}
justCopied={justCopied}
onSelect={onSelect}
/>
))}
</ul>
</div>
);
};

export default Category;

+ 0
- 44
react-ui/src/components/IconSelector/CopyableIcon.tsx View File

@@ -1,44 +0,0 @@
import * as AntdIcons from '@ant-design/icons';
import { Tooltip } from 'antd';
import classNames from 'classnames';
import * as React from 'react';
import type { ThemeType } from './index';
import styles from './style.less';

const allIcons: {
[key: string]: any;
} = AntdIcons;

export interface CopyableIconProps {
name: string;
isNew: boolean;
theme: ThemeType;
justCopied: string | null;
onSelect: (type: string, text: string) => any;
}

const CopyableIcon: React.FC<CopyableIconProps> = ({ name, justCopied, onSelect, theme }) => {
const className = classNames({
copied: justCopied === name,
[theme]: !!theme,
});
return (
<li
className={className}
onClick={() => {
if (onSelect) {
onSelect(theme, name);
}
}}
>
<Tooltip title={name}>
{React.createElement(allIcons[name], { className: styles.anticon })}
</Tooltip>
{/* <span className={styles.anticonClass}>
<Badge dot={isNew}>{name}</Badge>
</span> */}
</li>
);
};

export default CopyableIcon;

+ 0
- 237
react-ui/src/components/IconSelector/IconPicSearcher.tsx View File

@@ -1,237 +0,0 @@
import KFModal from '@/components/KFModal';
import * as AntdIcons from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Popover, Progress, Result, Spin, Tooltip, Upload } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
import './style.less';

const allIcons: { [key: string]: any } = AntdIcons;

const { Dragger } = Upload;
interface AntdIconClassifier {
load: () => void;
predict: (imgEl: HTMLImageElement) => void;
}
declare global {
interface Window {
antdIconClassifier: AntdIconClassifier;
}
}

interface PicSearcherState {
loading: boolean;
modalOpen: boolean;
popoverVisible: boolean;
icons: iconObject[];
fileList: any[];
error: boolean;
modelLoaded: boolean;
}

interface iconObject {
type: string;
score: number;
}

const PicSearcher: React.FC = () => {
const intl = useIntl();
const { formatMessage } = intl;
const [state, setState] = useState<PicSearcherState>({
loading: false,
modalOpen: false,
popoverVisible: false,
icons: [],
fileList: [],
error: false,
modelLoaded: false,
});
const predict = (imgEl: HTMLImageElement) => {
try {
let icons: any[] = window.antdIconClassifier.predict(imgEl);
if (gtag && icons.length) {
gtag('event', 'icon', {
event_category: 'search-by-image',
event_label: icons[0].className,
});
}
icons = icons.map((i) => ({ score: i.score, type: i.className.replace(/\s/g, '-') }));
setState((prev) => ({ ...prev, loading: false, error: false, icons }));
} catch {
setState((prev) => ({ ...prev, loading: false, error: true }));
}
};
// eslint-disable-next-line class-methods-use-this
const toImage = (url: string) =>
new Promise((resolve) => {
const img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
img.onload = () => {
resolve(img);
};
});

const uploadFile = useCallback((file: File) => {
setState((prev) => ({ ...prev, loading: true }));
const reader = new FileReader();
reader.onload = () => {
toImage(reader.result as string).then(predict);
setState((prev) => ({
...prev,
fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }],
}));
};
reader.readAsDataURL(file);
}, []);

const onPaste = useCallback((event: ClipboardEvent) => {
const items = event.clipboardData && event.clipboardData.items;
let file = null;
if (items && items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.includes('image')) {
file = items[i].getAsFile();
break;
}
}
}
if (file) {
uploadFile(file);
}
}, []);
const toggleModal = useCallback(() => {
setState((prev) => ({
...prev,
modalOpen: !prev.modalOpen,
popoverVisible: false,
fileList: [],
icons: [],
}));
if (!localStorage.getItem('disableIconTip')) {
localStorage.setItem('disableIconTip', 'true');
}
}, []);

useEffect(() => {
const script = document.createElement('script');
script.onload = async () => {
await window.antdIconClassifier.load();
setState((prev) => ({ ...prev, modelLoaded: true }));
document.addEventListener('paste', onPaste);
};
script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js';
document.head.appendChild(script);
setState((prev) => ({ ...prev, popoverVisible: !localStorage.getItem('disableIconTip') }));
return () => {
document.removeEventListener('paste', onPaste);
};
}, []);

return (
<div className="iconPicSearcher">
<Popover
content={formatMessage({ id: 'app.docs.components.icon.pic-searcher.intro' })}
open={state.popoverVisible}
>
<AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} />
</Popover>
<KFModal
title={intl.formatMessage({
id: 'app.docs.components.icon.pic-searcher.title',
defaultMessage: '信息',
})}
open={state.modalOpen}
onCancel={toggleModal}
footer={null}
>
{state.modelLoaded || (
<Spin
spinning={!state.modelLoaded}
tip={formatMessage({
id: 'app.docs.components.icon.pic-searcher.modelloading',
})}
>
<div style={{ height: 100 }} />
</Spin>
)}
{state.modelLoaded && (
<Dragger
accept="image/jpeg, image/png"
listType="picture"
customRequest={(o) => uploadFile(o.file as File)}
fileList={state.fileList}
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
>
<p className="ant-upload-drag-icon">
<AntdIcons.InboxOutlined />
</p>
<p className="ant-upload-text">
{formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-text' })}
</p>
<p className="ant-upload-hint">
{formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-hint' })}
</p>
</Dragger>
)}
<Spin
spinning={state.loading}
tip={formatMessage({ id: 'app.docs.components.icon.pic-searcher.matching' })}
>
<div className="icon-pic-search-result">
{state.icons.length > 0 && (
<div className="result-tip">
{formatMessage({ id: 'app.docs.components.icon.pic-searcher.result-tip' })}
</div>
)}
<table>
{state.icons.length > 0 && (
<thead>
<tr>
<th className="col-icon">
{formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-icon' })}
</th>
<th>
{formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-score' })}
</th>
</tr>
</thead>
)}
<tbody>
{state.icons.map((icon) => {
const { type } = icon;
const iconName = `${type
.split('-')
.map((str) => `${str[0].toUpperCase()}${str.slice(1)}`)
.join('')}Outlined`;
return (
<tr key={iconName}>
<td className="col-icon">
<Tooltip title={icon.type} placement="right">
{React.createElement(allIcons[iconName])}
</Tooltip>
</td>
<td>
<Progress percent={Math.ceil(icon.score * 100)} />
</td>
</tr>
);
})}
</tbody>
</table>
{state.error && (
<Result
status="500"
title="503"
subTitle={formatMessage({
id: 'app.docs.components.icon.pic-searcher.server-error',
})}
/>
)}
</div>
</Spin>
</KFModal>
</div>
);
};

export default PicSearcher;

+ 0
- 223
react-ui/src/components/IconSelector/fields.ts View File

@@ -1,223 +0,0 @@
import * as AntdIcons from '@ant-design/icons/lib/icons';

const all = Object.keys(AntdIcons)
.map((n) => n.replace(/(Outlined|Filled|TwoTone)$/, ''))
.filter((n, i, arr) => arr.indexOf(n) === i);

const direction = [
'StepBackward',
'StepForward',
'FastBackward',
'FastForward',
'Shrink',
'ArrowsAlt',
'Down',
'Up',
'Left',
'Right',
'CaretUp',
'CaretDown',
'CaretLeft',
'CaretRight',
'UpCircle',
'DownCircle',
'LeftCircle',
'RightCircle',
'DoubleRight',
'DoubleLeft',
'VerticalLeft',
'VerticalRight',
'VerticalAlignTop',
'VerticalAlignMiddle',
'VerticalAlignBottom',
'Forward',
'Backward',
'Rollback',
'Enter',
'Retweet',
'Swap',
'SwapLeft',
'SwapRight',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'PlayCircle',
'UpSquare',
'DownSquare',
'LeftSquare',
'RightSquare',
'Login',
'Logout',
'MenuFold',
'MenuUnfold',
'BorderBottom',
'BorderHorizontal',
'BorderInner',
'BorderOuter',
'BorderLeft',
'BorderRight',
'BorderTop',
'BorderVerticle',
'PicCenter',
'PicLeft',
'PicRight',
'RadiusBottomleft',
'RadiusBottomright',
'RadiusUpleft',
'RadiusUpright',
'Fullscreen',
'FullscreenExit',
];

const suggestion = [
'Question',
'QuestionCircle',
'Plus',
'PlusCircle',
'Pause',
'PauseCircle',
'Minus',
'MinusCircle',
'PlusSquare',
'MinusSquare',
'Info',
'InfoCircle',
'Exclamation',
'ExclamationCircle',
'Close',
'CloseCircle',
'CloseSquare',
'Check',
'CheckCircle',
'CheckSquare',
'ClockCircle',
'Warning',
'IssuesClose',
'Stop',
];

const editor = [
'Edit',
'Form',
'Copy',
'Scissor',
'Delete',
'Snippets',
'Diff',
'Highlight',
'AlignCenter',
'AlignLeft',
'AlignRight',
'BgColors',
'Bold',
'Italic',
'Underline',
'Strikethrough',
'Redo',
'Undo',
'ZoomIn',
'ZoomOut',
'FontColors',
'FontSize',
'LineHeight',
'Dash',
'SmallDash',
'SortAscending',
'SortDescending',
'Drag',
'OrderedList',
'UnorderedList',
'RadiusSetting',
'ColumnWidth',
'ColumnHeight',
];

const data = [
'AreaChart',
'PieChart',
'BarChart',
'DotChart',
'LineChart',
'RadarChart',
'HeatMap',
'Fall',
'Rise',
'Stock',
'BoxPlot',
'Fund',
'Sliders',
];

const logo = [
'Android',
'Apple',
'Windows',
'Ie',
'Chrome',
'Github',
'Aliwangwang',
'Dingding',
'WeiboSquare',
'WeiboCircle',
'TaobaoCircle',
'Html5',
'Weibo',
'Twitter',
'Wechat',
'Youtube',
'AlipayCircle',
'Taobao',
'Skype',
'Qq',
'MediumWorkmark',
'Gitlab',
'Medium',
'Linkedin',
'GooglePlus',
'Dropbox',
'Facebook',
'Codepen',
'CodeSandbox',
'CodeSandboxCircle',
'Amazon',
'Google',
'CodepenCircle',
'Alipay',
'AntDesign',
'AntCloud',
'Aliyun',
'Zhihu',
'Slack',
'SlackSquare',
'Behance',
'BehanceSquare',
'Dribbble',
'DribbbleSquare',
'Instagram',
'Yuque',
'Alibaba',
'Yahoo',
'Reddit',
'Sketch',
'WhatsApp',
'Dingtalk',
];

const datum = [...direction, ...suggestion, ...editor, ...data, ...logo];

const other = all.filter((n) => !datum.includes(n));

export const categories = {
direction,
suggestion,
editor,
data,
logo,
other,
};

export default categories;

export type Categories = typeof categories;
export type CategoriesKeys = keyof Categories;

+ 0
- 146
react-ui/src/components/IconSelector/index.tsx View File

@@ -1,146 +0,0 @@
import Icon, * as AntdIcons from '@ant-design/icons';
import { Empty, Input, Radio } from 'antd';
import type { RadioChangeEvent } from 'antd/es/radio/interface';
import debounce from 'lodash/debounce';
import * as React from 'react';
import Category from './Category';
import IconPicSearcher from './IconPicSearcher';
import type { CategoriesKeys } from './fields';
import { categories } from './fields';
import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons';
// import { useIntl } from '@umijs/max';

export enum ThemeType {
Filled = 'Filled',
Outlined = 'Outlined',
TwoTone = 'TwoTone',
}

const allIcons: { [key: string]: any } = AntdIcons;

interface IconSelectorProps {
//intl: any;
onSelect: any;
}

interface IconSelectorState {
theme: ThemeType;
searchKey: string;
}

const IconSelector: React.FC<IconSelectorProps> = (props) => {
// const intl = useIntl();
// const { messages } = intl;
const { onSelect } = props;
const [displayState, setDisplayState] = React.useState<IconSelectorState>({
theme: ThemeType.Outlined,
searchKey: '',
});

const newIconNames: string[] = [];

const handleSearchIcon = React.useCallback(
debounce((searchKey: string) => {
setDisplayState((prevState) => ({ ...prevState, searchKey }));
}),
[],
);

const handleChangeTheme = React.useCallback((e: RadioChangeEvent) => {
setDisplayState((prevState) => ({ ...prevState, theme: e.target.value as ThemeType }));
}, []);

const renderCategories = React.useMemo<React.ReactNode | React.ReactNode[]>(() => {
const { searchKey = '', theme } = displayState;

const categoriesResult = Object.keys(categories)
.map((key: CategoriesKeys) => {
let iconList = categories[key];
if (searchKey) {
const matchKey = searchKey
// eslint-disable-next-line prefer-regex-literals
.replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name)
.replace(/(Filled|Outlined|TwoTone)$/, '')
.toLowerCase();
iconList = iconList.filter((iconName: string) =>
iconName.toLowerCase().includes(matchKey),
);
}

// CopyrightCircle is same as Copyright, don't show it
iconList = iconList.filter((icon: string) => icon !== 'CopyrightCircle');

return {
category: key,
icons: iconList
.map((iconName: string) => iconName + theme)
.filter((iconName: string) => allIcons[iconName]),
};
})
.filter(({ icons }) => !!icons.length)
.map(({ category, icons }) => (
<Category
key={category}
title={category as CategoriesKeys}
theme={theme}
icons={icons}
newIcons={newIconNames}
onSelect={(type, name) => {
if (onSelect) {
onSelect(name, allIcons[name]);
}
}}
/>
));
return categoriesResult.length === 0 ? <Empty style={{ margin: '2em 0' }} /> : categoriesResult;
}, [displayState.searchKey, displayState.theme]);
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Radio.Group
value={displayState.theme}
onChange={handleChangeTheme}
size="large"
optionType="button"
buttonStyle="solid"
options={[
{
label: <Icon component={OutlinedIcon} />,
value: ThemeType.Outlined,
},
{
label: <Icon component={FilledIcon} />,
value: ThemeType.Filled,
},
{
label: <Icon component={TwoToneIcon} />,
value: ThemeType.TwoTone,
},
]}
>
{/* <Radio.Button value={ThemeType.Outlined}>
<Icon component={OutlinedIcon} /> {messages['app.docs.components.icon.outlined']}
</Radio.Button>
<Radio.Button value={ThemeType.Filled}>
<Icon component={FilledIcon} /> {messages['app.docs.components.icon.filled']}
</Radio.Button>
<Radio.Button value={ThemeType.TwoTone}>
<Icon component={TwoToneIcon} /> {messages['app.docs.components.icon.two-tone']}
</Radio.Button> */}
</Radio.Group>
<Input.Search
// placeholder={messages['app.docs.components.icon.search.placeholder']}
style={{ margin: '0 10px', flex: 1 }}
allowClear
onChange={(e) => handleSearchIcon(e.currentTarget.value)}
size="large"
autoFocus
suffix={<IconPicSearcher />}
/>
</div>
{renderCategories}
</>
);
};

export default IconSelector;

+ 0
- 137
react-ui/src/components/IconSelector/style.less View File

@@ -1,137 +0,0 @@
.iconPicSearcher {
display: inline-block;
margin: 0 8px;

.icon-pic-btn {
color: @text-color-secondary;
cursor: pointer;
transition: all 0.3s;

&:hover {
color: @input-icon-hover-color;
}
}
}

.icon-pic-preview {
width: 30px;
height: 30px;
margin-top: 10px;
padding: 8px;
text-align: center;
border: 1px solid @border-color-base;
border-radius: 4px;

> img {
max-width: 50px;
max-height: 50px;
}
}

.icon-pic-search-result {
min-height: 50px;
padding: 0 10px;

> .result-tip {
padding: 10px 0;
color: @text-color-secondary;
}

> table {
width: 100%;

.col-icon {
width: 80px;
padding: 10px 0;

> .anticon {
font-size: 30px;

:hover {
color: @link-hover-color;
}
}
}
}
}

ul.anticonsList {
margin: 2px 0;
overflow: hidden;
direction: ltr;
list-style: none;

li {
position: relative;
float: left;
width: 48px;
height: 48px;
margin: 3px 0;
padding: 2px 0 0;
overflow: hidden;
color: #555;
text-align: center;
list-style: none;
background-color: inherit;
border-radius: 4px;
cursor: pointer;
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;

.rtl & {
margin: 3px 0;
padding: 2px 0 0;
}

.anticon {
margin: 4px 0 2px;
font-size: 24px;
transition: transform 0.3s ease-in-out;
will-change: transform;
}

.anticonClass {
display: block;
font-family: 'Lucida Console', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
white-space: nowrap;
text-align: center;
transform: scale(0.83);

.ant-badge {
transition: color 0.3s ease-in-out;
}
}

&:hover {
color: #fff;
background-color: @primary-color;

.anticon {
transform: scale(1.4);
}

.ant-badge {
color: #fff;
}
}

&.TwoTone:hover {
background-color: #8ecafe;
}

&.copied:hover {
color: rgba(255, 255, 255, 0.2);
}

&.copied::after {
top: -2px;
opacity: 1;
}
}
}

.copied-code {
padding: 2px 4px;
font-size: 12px;
background: #f5f5f5;
border-radius: 2px;
}

+ 0
- 40
react-ui/src/components/IconSelector/themeIcons.tsx View File

@@ -1,40 +0,0 @@
import * as React from 'react';

export const FilledIcon: React.FC = (props) => {
const path =
'M864 64H160C107 64 64 107 64 160v' +
'704c0 53 43 96 96 96h704c53 0 96-43 96-96V16' +
'0c0-53-43-96-96-96z';
return (
<svg {...props} viewBox="0 0 1024 1024">
<path d={path} />
</svg>
);
};

export const OutlinedIcon: React.FC = (props) => {
const path =
'M864 64H160C107 64 64 107 64 160v7' +
'04c0 53 43 96 96 96h704c53 0 96-43 96-96V160c' +
'0-53-43-96-96-96z m-12 800H172c-6.6 0-12-5.4-' +
'12-12V172c0-6.6 5.4-12 12-12h680c6.6 0 12 5.4' +
' 12 12v680c0 6.6-5.4 12-12 12z';
return (
<svg {...props} viewBox="0 0 1024 1024">
<path d={path} />
</svg>
);
};

export const TwoToneIcon: React.FC = (props) => {
const path =
'M16 512c0 273.932 222.066 496 496 49' +
'6s496-222.068 496-496S785.932 16 512 16 16 238.' +
'066 16 512z m496 368V144c203.41 0 368 164.622 3' +
'68 368 0 203.41-164.622 368-368 368z';
return (
<svg {...props} viewBox="0 0 1024 1024">
<path d={path} />
</svg>
);
};

+ 11
- 0
react-ui/src/components/InfoGroup/index.less View File

@@ -0,0 +1,11 @@
.kf-info-group {
width: 100%;

&__content {
padding: 20px @content-padding;
background-color: white;
border: 1px solid @border-color-base;
border-top: none;
border-radius: 0 0 4px 4px;
}
}

+ 34
- 0
react-ui/src/components/InfoGroup/index.tsx View File

@@ -0,0 +1,34 @@
import classNames from 'classnames';
import InfoGroupTitle from '../InfoGroupTitle';
import './index.less';

type InfoGroupProps = {
title: string;
height?: string | number; // 如果要纵向滚动,需要设置高度
width?: string | number; // 如果要横向滚动,需要设置宽度
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
};

function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) {
const contentStyle: React.CSSProperties = {};
if (height) {
contentStyle.height = height;
contentStyle.overflowY = 'auto';
}
if (width) {
contentStyle.width = width;
contentStyle.overflowX = 'auto';
}
return (
<div className={classNames('kf-info-group', className)} style={style}>
<InfoGroupTitle title={title} />
<div style={contentStyle} className={'kf-info-group__content'}>
{children}
</div>
</div>
);
}

export default InfoGroup;

+ 39
- 0
react-ui/src/components/InfoGroupTitle/index.less View File

@@ -0,0 +1,39 @@
.kf-info-group-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;

&__image {
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: '';
}
}
}

+ 25
- 0
react-ui/src/components/InfoGroupTitle/index.tsx View File

@@ -0,0 +1,25 @@
import { Flex } from 'antd';
import classNames from 'classnames';
import './index.less';

type InfoGroupTitleProps = {
title: string;
className?: string;
style?: React.CSSProperties;
};

function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) {
return (
<Flex align="center" className={classNames('kf-info-group-title', className)} style={style}>
<img
src={require('@/assets/img/code-name-icon.png')}
className="kf-info-group-title__image"
alt=""
draggable={false}
/>
<span className="kf-info-group-title__text">{title}</span>
</Flex>
);
}

export default InfoGroupTitle;

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


+ 0
- 7
react-ui/src/components/KFConfirmModal/index.less View File

@@ -1,7 +0,0 @@
.kf-confirm-modal {
&__content {
width: 100%;
font-size: 18px;
text-align: center;
}
}

+ 0
- 37
react-ui/src/components/KFConfirmModal/index.tsx View File

@@ -1,37 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-10-10 10:54:25
* @Description: 自定义 Confirm Modal
*/

import classNames from 'classnames';
import KFModal, { KFModalProps } from '../KFModal';
import './index.less';

export interface KFConfirmModalProps extends KFModalProps {
content: string;
}
function KFConfirmModal({
title,
image,
className = '',
centered,
maskClosable,
content,
...rest
}: KFConfirmModalProps) {
return (
<KFModal
className={classNames(['kf-confirm-modal', className])}
{...rest}
centered={centered ?? true}
maskClosable={maskClosable ?? false}
title={title}
image={image ?? require('@/assets/img/edit-experiment.png')}
>
<div className="kf-confirm-modal__content">{content}</div>
</KFModal>
);
}

export default KFConfirmModal;

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

+ 22
- 0
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -1,7 +1,10 @@
import { ServiceData } from '@/pages/ModelDeployment/types';
import { getDatasetList, getModelList } from '@/services/dataset/index.js';
import { getServiceListReq } from '@/services/modelDeployment';
import { getComputingResourceReq } from '@/services/pipeline';
import { ComputingResource } from '@/types';
import { type SelectProps } from 'antd';
import { pick } from 'lodash';

// 过滤资源规格
const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = (
@@ -62,6 +65,25 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
},
optionFilterProp: 'name',
},
service: {
getOptions: async () => {
const res = await getServiceListReq({
page: 0,
size: 1000,
});
return (
res?.data?.content?.map((item: ServiceData) => ({
label: item.service_name,
value: JSON.stringify(pick(item, ['id', 'service_name'])),
})) ?? []
);
},
fieldNames: {
label: 'label',
value: 'value',
},
optionFilterProp: 'label',
},
resource: {
getOptions: async () => {
const res = await getComputingResourceReq({


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


+ 15
- 1
react-ui/src/hooks/resource.ts View File

@@ -1,15 +1,28 @@
/*
* @Author: 赵伟
* @Date: 2024-10-10 08:51:41
* @Description: 资源规格 hook
*/

import { getComputingResourceReq } from '@/services/pipeline';
import computingResourceState, { setComputingResource } from '@/state/computingResourceStore';
import { ComputingResource } from '@/types';
import { to } from '@/utils/promise';
import { type SelectProps } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import { useSnapshot } from 'umi';

// 获取资源规格
export function useComputingResource() {
const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]);
const computingResourceSnap = useSnapshot(computingResourceState);

useEffect(() => {
getComputingResource();
if (computingResourceSnap.computingResource.length > 0) {
setResourceStandardList(computingResourceSnap.computingResource as ComputingResource[]);
} else {
getComputingResource();
}
}, []);

// 获取资源规格列表数据
@@ -22,6 +35,7 @@ export function useComputingResource() {
const [res] = await to(getComputingResourceReq(params));
if (res && res.data && res.data.content) {
setResourceStandardList(res.data.content);
setComputingResource(res.data.content);
}
}, []);



+ 6
- 0
react-ui/src/hooks/sessionStorage.ts View File

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-11-06 14:53:37
* @Description: SessionStorage hook
*/

import SessionStorage from '@/utils/sessionStorage';
import { useEffect, useState } from 'react';



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

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

@@ -0,0 +1,214 @@
/*
* @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 { useLocation, 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);
const { pathname } = useLocation();
const isCopy = pathname.includes('copy');

useEffect(() => {
// 编辑,复制
if (id && !Number.isNaN(id)) {
getAutoMLInfo(id);
}
}, [id]);

// 获取服务详情
const getAutoMLInfo = async (id: number) => {
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 && !isCopy
? {
id: id,
...object,
}
: object;

const request = id && !isCopy ? 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) {
if (isCopy) {
title = '复制实验';
buttonText = '确定';
} else {
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;
}
}

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

@@ -0,0 +1,215 @@
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(false);
}
return () => {
closeSSE();
};
}, []);

// 获取实验实例详情
const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
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);
}

// 这个接口返回的状态有延时,SSE 返回的状态是最新的
// SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE
if (isStatusDetermined) {
setInstanceInfo((prev) => ({
...info,
nodeStatus: prev!.nodeStatus,
}));
return;
}

// 进行节点状态
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!,
nodeStatus: statusData,
}));

// 实验结束,关闭 SSE
if (
statusData.phase !== ExperimentStatus.Pending &&
statusData.phase !== ExperimentStatus.Running
) {
closeSSE();
getExperimentInsInfo(true);
}
}
}
}
};
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}
modelPath={instanceInfo?.model_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;
}
}
}

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

@@ -0,0 +1,411 @@
/*
* @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 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) {
navigate(`/pipeline/autoML/copy/${record.id}`);
} else {
navigate(`/pipeline/autoML/edit/${record.id}`);
}
} else {
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;
}
}
}

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

@@ -0,0 +1,308 @@
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}
style={{ marginBottom: '20px' }}
/>
)}
{!isInstance && (
<ConfigInfo
title="基本信息"
data={basicDatas}
labelWidth={70}
style={{ marginBottom: '20px' }}
/>
)}
<ConfigInfo
title="配置信息"
data={configDatas}
labelWidth={150}
style={{ marginBottom: '20px' }}
/>
<ConfigInfo title="优化指标" data={metricsData} labelWidth={70} />
</div>
);
}

export default AutoMLBasic;

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

@@ -0,0 +1,20 @@
.config-info {
:global {
.kf-basic-info {
width: 100%;

&__item {
width: calc((100% - 80px) / 3);
&__label {
font-size: @font-size;
text-align: left;
text-align-last: left;
}
&__value {
min-width: 0;
font-size: @font-size;
}
}
}
}
}

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

@@ -0,0 +1,26 @@
import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo';
import InfoGroup from '@/components/InfoGroup';
import classNames from 'classnames';
import styles from './index.less';
export * from '@/components/BasicInfo/format';
export type { BasicInfoData };

type ConfigInfoProps = {
title: string;
data: BasicInfoData[];
labelWidth: number;
className?: string;
style?: React.CSSProperties;
};

function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) {
return (
<InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}>
<div className={styles['config-info__content']}>
<BasicInfo datas={data} labelWidth={labelWidth} />
</div>
</InfoGroup>
);
}

export default ConfigInfo;

+ 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


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

@@ -0,0 +1,52 @@
.experiment-result {
height: calc(100% - 10px);
margin-top: 10px;
padding: 20px @content-padding;
overflow-y: auto;
background-color: white;
border-radius: 10px;

&__download {
padding-top: 16px;
padding-bottom: 16px;

padding-left: @content-padding;
color: @text-color;
font-size: 13px;
background-color: #f8f8f9;
border-radius: 4px;

&__btn {
display: block;
height: 36px;
margin-top: 15px;
font-size: 14px;
}
}

&__text {
white-space: pre-wrap;
}

&__images {
display: flex;
align-items: flex-start;
width: 100%;
overflow-x: auto;

:global {
.ant-image {
margin-right: 20px;

&:last-child {
margin-right: 0;
}
}
}

&__item {
height: 248px;
border: 1px solid rgba(96, 107, 122, 0.3);
}
}
}

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

@@ -0,0 +1,83 @@
import InfoGroup from '@/components/InfoGroup';
import { getFileReq } from '@/services/file';
import { to } from '@/utils/promise';
import { Button, Image } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import styles from './index.less';

type ExperimentResultProps = {
fileUrl?: string;
imageUrl?: string;
modelPath?: string;
};

function ExperimentResult({ fileUrl, imageUrl, modelPath }: 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']}>
<InfoGroup title="实验结果" height={420} width="100%">
<div className={styles['experiment-result__text']}>{result}</div>
</InfoGroup>
<InfoGroup title="可视化结果" style={{ margin: '16px 0' }}>
<div className={styles['experiment-result__images']}>
<Image.PreviewGroup
preview={{
onChange: (current, prev) =>
console.log(`current index: ${current}, prev index: ${prev}`),
}}
>
{images.map((item) => (
<Image
key={item}
className={styles['experiment-result__images__item']}
src={item}
height={248}
draggable={false}
alt=""
/>
))}
</Image.PreviewGroup>
</div>
</InfoGroup>
{modelPath && (
<div className={styles['experiment-result__download']}>
<span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span>
<span>save_model.joblib</span>
<Button
type="primary"
className={styles['experiment-result__download__btn']}
onClick={() => {
window.location.href = modelPath;
}}
>
模型下载
</Button>
</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;
};

+ 3
- 0
react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx View File

@@ -23,6 +23,7 @@ interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> {
resourceId: number;
identifier: string;
resoureName: string;
is_public: boolean;
onOk: () => void;
}

@@ -31,6 +32,7 @@ function AddVersionModal({
resourceId,
resoureName,
identifier,
is_public,
onOk,
...rest
}: AddVersionModalProps) {
@@ -71,6 +73,7 @@ function AddVersionModal({
const params = {
id: resourceId,
identifier,
is_public,
[config.filePropKey]: version_vos,
...omit(formData, 'fileList'),
[config.sourceParamKey]: DataSource.Create,


+ 4
- 0
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -47,6 +47,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
const name = searchParams.get('name') || '';
const owner = searchParams.get('owner') || '';
const identifier = searchParams.get('identifier') || '';
const is_public = searchParams.get('is_public') || '';
const [versionList, setVersionList] = useState<ResourceVersionData[]>([]);
const [version, setVersion] = useState<string | undefined>(undefined);
const [activeTab, setActiveTab] = useState<string>(defaultTab);
@@ -66,6 +67,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
name,
identifier,
version,
is_public: is_public === 'true',
});
}
}, [version]);
@@ -77,6 +79,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
id: number;
identifier: string;
version?: string;
is_public: boolean;
}) => {
const request = config.getInfo;
const [res] = await to(request(params));
@@ -117,6 +120,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
resourceId: resourceId,
resoureName: info.name,
identifier: info.identifier,
is_public: info.is_public,
onOk: () => {
getVersionList();
close();


+ 2
- 2
react-ui/src/pages/Dataset/components/ResourceList/index.tsx View File

@@ -122,7 +122,7 @@ function ResourceList(
modalConfirm({
title: config.deleteModalTitle,
onOk: () => {
deleteRecord(pick(record, ['owner', 'identifier', 'id']));
deleteRecord(pick(record, ['owner', 'identifier', 'id', 'is_public']));
},
});
};
@@ -138,7 +138,7 @@ function ResourceList(
});
const prefix = config.prefix;
navigate(
`/dataset/${prefix}/info/${record.id}?name=${record.name}&owner=${record.owner}&identifier=${record.identifier}`,
`/dataset/${prefix}/info/${record.id}?name=${record.name}&owner=${record.owner}&identifier=${record.identifier}&is_public=${record.is_public}`,
);
};



+ 1
- 0
react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx View File

@@ -28,6 +28,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) {
id: info.id,
version: info.version,
identifier: info.identifier,
is_public: info.is_public,
});
};



+ 3
- 23
react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx View File

@@ -167,7 +167,7 @@ function EditorCreate() {
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="镜  像"
label="镜像"
name="image"
rules={[
{
@@ -188,17 +188,7 @@ function EditorCreate() {
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="模  型"
name="model"
rules={[
{
validator: requiredValidator,
message: '请选择模型',
},
]}
required
>
<Form.Item label="模型" name="model">
<ResourceSelect
type={ResourceSelectorType.Model}
placeholder="请选择模型"
@@ -210,17 +200,7 @@ function EditorCreate() {
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="数据集"
name="dataset"
rules={[
{
validator: requiredValidator,
message: '请选择数据集',
},
]}
required
>
<Form.Item label="数据集" name="dataset">
<ResourceSelect
type={ResourceSelectorType.Dataset}
placeholder="请选择数据集"


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

@@ -3,6 +3,12 @@
.ant-drawer-body {
overflow-y: hidden;
}

.ant-drawer-close {
position: absolute;
top: 16px;
right: 16px;
}
}

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

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

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

@@ -2,7 +2,7 @@ import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs } from 'antd';
import { useMemo } from 'react';
import ExperimentParameter from '../ExperimentParameter';
@@ -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 />,
},
@@ -93,10 +95,11 @@ const ExperimentDrawer = ({

return (
<Drawer
rootStyle={{ marginTop: '55px' }}
title="任务执行详情"
placement="right"
getContainer={false}
closeIcon={false}
closeIcon={<CloseOutlined className={styles['experiment-drawer__close']} />}
onClose={onClose}
open={open}
width={520}


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

@@ -52,7 +52,7 @@ function LogGroup({
const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const socketRef = useRef<WebSocket | undefined>(undefined);
const retryRef = useRef(2); // 等待 2 秒,重试 2
const retryRef = useRef(2); // 等待 2 秒,重试 3

useEffect(() => {
scrollToBottom(false);
@@ -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}"}`,
@@ -147,7 +147,7 @@ function LogGroup({

socket.addEventListener('close', (event) => {
console.log('WebSocket is closed:', event);
// 有时候会出现连接失败,重试 2
// 有时候会出现连接失败,重试 3
if (event.code !== 1000 && retryRef.current > 0) {
retryRef.current -= 1;
setTimeout(() => {


+ 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


+ 35
- 28
react-ui/src/pages/ModelDeployment/CreateService/index.tsx View File

@@ -7,14 +7,12 @@
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys, serviceTypeOptions } from '@/enums';
import { createServiceReq, updateServiceReq } from '@/services/modelDeployment';
import { createServiceReq, getServiceInfoReq, updateServiceReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { useNavigate } from '@umijs/max';
import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Col, Form, Input, Row, Select } from 'antd';
import { pick } from 'lodash';
import { useEffect, useState } from 'react';
import { ServiceData, ServiceOperationType, createServiceVersionMessage } from '../types';
import { ServiceData, createServiceVersionMessage } from '../types';
import styles from './index.less';

// 表单数据
@@ -27,40 +25,49 @@ export type FormData = {
function CreateService() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [operationType, setOperationType] = useState(ServiceOperationType.Create);
const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
const { message } = App.useApp();
const params = useParams();
const serviceId = params.serviceId;

useEffect(() => {
const res = SessionStorage.getItem(SessionStorage.serviceInfoKey, true);
if (res) {
setOperationType(res.operationType);
setServiceInfo(res);
form.setFieldsValue(pick(res, ['service_name', 'service_type', 'description']));
if (serviceId) {
getServiceInfo();
}
return () => {
SessionStorage.removeItem(SessionStorage.serviceInfoKey);
};
}, []);

// 获取服务详情
const getServiceInfo = async () => {
const [res] = await to(getServiceInfoReq(serviceId));
if (res && res.data) {
setServiceInfo(res.data);
const { service_type, service_name, description } = res.data;
form.setFieldsValue({
service_type,
service_name,
description,
});
}
};

// 创建、更新服务
const createService = async (formData: FormData) => {
const request =
operationType === ServiceOperationType.Create ? createServiceReq : updateServiceReq;
const params =
operationType === ServiceOperationType.Create
? formData
: {
id: serviceInfo?.id,
...formData,
};
const request = serviceId ? updateServiceReq : createServiceReq;
const params = serviceId
? {
id: serviceId,
...formData,
}
: formData;
const [res] = await to(request(params));
if (res && res.data) {
message.success('操作成功');
navigate(-1);
setTimeout(() => {
window.postMessage({ type: createServiceVersionMessage, payload: res.data.id });
}, 500);
if (!serviceId) {
setTimeout(() => {
window.postMessage({ type: createServiceVersionMessage, payload: res.data.id });
}, 500);
}
}
};

@@ -74,8 +81,8 @@ function CreateService() {
navigate(-1);
};

const disabled = operationType !== ServiceOperationType.Create;
const title = operationType === ServiceOperationType.Create ? '创建推理服务' : '更新推理服务';
const disabled = !!serviceId;
const title = serviceId ? '编辑推理服务' : '创建推理服务';

return (
<div className={styles['model-deployment-create']}>


+ 35
- 37
react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx View File

@@ -24,15 +24,10 @@ import SessionStorage from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui';
import { PlusOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Col, Flex, Form, Input, Row, Select } from 'antd';
import { App, Button, Col, Flex, Form, Input, InputNumber, Row, Select } from 'antd';
import { omit, pick } from 'lodash';
import { useEffect, useState } from 'react';
import {
CreateServiceVersionFrom,
ServiceData,
ServiceOperationType,
ServiceVersionData,
} from '../types';
import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less';

// 表单数据
@@ -49,6 +44,11 @@ export type FormData = {
env_variables: { key: string; value: string }[]; // 环境变量
};

type ServiceVersionCache = ServiceVersionData & {
operationType: ServiceOperationType;
lastPage: CreateServiceVersionFrom;
};

function CreateServiceVersion() {
const navigate = useNavigate();
const [form] = Form.useForm();
@@ -56,18 +56,16 @@ function CreateServiceVersion() {
const [operationType, setOperationType] = useState(ServiceOperationType.Create);
const [lastPage, setLastPage] = useState(CreateServiceVersionFrom.ServiceInfo);
const { message } = App.useApp();
const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
// const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined);
const params = useParams();
const id = params.id;
const serviceId = params.serviceId;

useEffect(() => {
const res:
| (ServiceVersionData & {
operationType: ServiceOperationType;
lastPage: CreateServiceVersionFrom;
})
| undefined = SessionStorage.getItem(SessionStorage.serviceVersionInfoKey, true);
const res: ServiceVersionCache | undefined = SessionStorage.getItem(
SessionStorage.serviceVersionInfoKey,
true,
);
if (res) {
setOperationType(res.operationType);
setLastPage(res.lastPage);
@@ -109,9 +107,9 @@ function CreateServiceVersion() {

// 获取服务详情
const getServiceInfo = async () => {
const [res] = await to(getServiceInfoReq(id));
const [res] = await to(getServiceInfoReq(serviceId));
if (res && res.data) {
setServiceInfo(res.data);
// setServiceInfo(res.data);
form.setFieldsValue({
service_name: res.data.service_name,
});
@@ -120,11 +118,11 @@ function CreateServiceVersion() {

// 创建版本
const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables'] ?? [];
const envList = formData['env_variables'];
const image = formData['image'];
const model = formData['model'];
const codeConfig = formData['code_config'];
const envVariables = envList.reduce((acc, cur) => {
const envVariables = envList?.reduce((acc, cur) => {
acc[cur.key] = cur.value;
return acc;
}, {} as Record<string, string>);
@@ -139,15 +137,20 @@ function CreateServiceVersion() {
pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']),
{ showValue: 'show_value' },
),
code_config: changePropertyName(pick(codeConfig, ['code_path', 'branch', 'showValue']), {
showValue: 'show_value',
}),
service_id: serviceInfo?.id,
code_config: codeConfig
? changePropertyName(pick(codeConfig, ['code_path', 'branch', 'showValue']), {
showValue: 'show_value',
})
: undefined,
service_id: serviceId,
};

const params =
operationType === ServiceOperationType.Create
? object
? {
...object,
deploy_type: 'web',
}
: {
id: versionInfo?.id,
rerun: operationType === ServiceOperationType.Restart ? true : false,
@@ -166,7 +169,7 @@ function CreateServiceVersion() {
if (lastPage === CreateServiceVersionFrom.ServiceInfo) {
navigate(-1);
} else {
navigate(`/modelDeployment/serviceInfo/${serviceInfo?.id}`, { replace: true });
navigate(`/modelDeployment/serviceInfo/${serviceId}`, { replace: true });
}
}
};
@@ -334,17 +337,7 @@ function CreateServiceVersion() {
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="代码配置"
name="code_config"
rules={[
{
validator: requiredValidator,
message: '请选择代码配置',
},
]}
required
>
<Form.Item label="代码配置" name="code_config">
<CodeSelect
placeholder="请选择代码配置"
canInput={false}
@@ -395,7 +388,12 @@ function CreateServiceVersion() {
},
]}
>
<Input placeholder="请输入副本数量" allowClear />
<InputNumber
style={{ width: '100%' }}
placeholder="请输入副本数量"
min={1}
precision={0}
/>
</Form.Item>
</Col>
</Row>


+ 10
- 15
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -116,23 +116,18 @@ function ModelDeployment() {
};

// 创建、更新服务
const createService = (type: ServiceOperationType, record?: ServiceData) => {
SessionStorage.setItem(
SessionStorage.serviceInfoKey,
{
...record,
operationType: type,
},
true,
);

const createService = (record?: ServiceData) => {
setCacheState({
pagination,
searchText,
serviceType: serviceType,
});

navigate(`/modelDeployment/createService`);
if (record) {
navigate(`editService/${record.id}`);
} else {
navigate('createService');
}
};

// 查看详情
@@ -143,7 +138,7 @@ function ModelDeployment() {
serviceType: serviceType,
});

navigate(`/modelDeployment/serviceInfo/${record.id}`);
navigate(`serviceInfo/${record.id}`);
};

const handleMessage = (e: MessageEvent) => {
@@ -172,7 +167,7 @@ function ModelDeployment() {
true,
);

navigate(`/modelDeployment/addVersion/${serviceId}`);
navigate(`serviceInfo/${serviceId}/createVersion`);
};

// 分页切换
@@ -248,7 +243,7 @@ function ModelDeployment() {
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => createService(ServiceOperationType.Update, record)}
onClick={() => createService(record)}
>
编辑
</Button>
@@ -307,7 +302,7 @@ function ModelDeployment() {
<Button
style={{ marginLeft: 'auto', marginRight: '20px' }}
type="default"
onClick={() => createService(ServiceOperationType.Create)}
onClick={() => createService()}
icon={<KFIcon type="icon-xinjian2" />}
>
创建推理服务


+ 16
- 5
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -66,7 +66,7 @@ function ServiceInfo() {
},
);
const params = useParams();
const id = params.id;
const serviceId = params.serviceId;
const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
const basicInfo = [
{
@@ -99,7 +99,7 @@ function ServiceInfo() {

// 获取服务详情
const getServiceInfo = async () => {
const [res] = await to(getServiceInfoReq(id));
const [res] = await to(getServiceInfoReq(serviceId));
if (res && res.data) {
setServiceInfo(res.data);
}
@@ -112,11 +112,16 @@ function ServiceInfo() {
size: pagination.pageSize,
version: searchText,
run_state: serviceStatus,
service_id: id,
service_id: serviceId,
};
const [res] = await to(getServiceVersionsReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
content.forEach((item: ServiceVersionData) => {
if (item.model && !item.model.show_value) {
item.model.show_value = `${item.model.name}:${item.model.version}`;
}
});
setTableData(content);
setTotal(totalElements);
}
@@ -196,7 +201,13 @@ function ServiceInfo() {
serviceStatus: serviceStatus,
});

navigate(`/modelDeployment/addVersion/${id}`);
if (type === ServiceOperationType.Update) {
navigate('updateVersion');
} else if (type === ServiceOperationType.Restart) {
navigate('restartVersion');
} else {
navigate('createVersion');
}
};

// 查看详情
@@ -207,7 +218,7 @@ function ServiceInfo() {
serviceStatus: serviceStatus,
});

navigate(`/modelDeployment/versionInfo/${record.id}`);
navigate(`versionInfo/${record.id}`);
};

// 分页切换


+ 2
- 2
react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx View File

@@ -27,7 +27,7 @@ function BasicInfo({ info }: BasicInfoProps) {
};

const formatCodeConfig = () => {
if (info && info.code_config) {
if (info && info.code_config && info.code_config.code_path) {
const { code_path, branch } = info.code_config;
const url = getGitUrl(code_path, branch);
return (
@@ -36,7 +36,7 @@ function BasicInfo({ info }: BasicInfoProps) {
</a>
);
}
return undefined;
return '--';
};

const formatResource = () => {


+ 1
- 1
react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx View File

@@ -49,7 +49,7 @@ const GlobalParamsDrawer = forwardRef(

return (
<Drawer
rootStyle={{ marginTop: '45px' }}
rootStyle={{ marginTop: '55px' }}
title="全局参数"
placement="right"
closeIcon={false}


+ 82
- 60
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -43,22 +43,34 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
if (!open) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_values, error] = await to(form.validateFields());
// 不管是否验证成功,都需要获取表单数据
const fields = form.getFieldsValue();
const control_strategy = JSON.stringify(fields.control_strategy);
const in_parameters = JSON.stringify(fields.in_parameters);
const out_parameters = JSON.stringify(fields.out_parameters);

// 保存字段顺序
const control_strategy = {
...stagingItem.control_strategy,
...fields.control_strategy,
};
const in_parameters = {
...stagingItem.in_parameters,
...fields.in_parameters,
};
const out_parameters = {
...stagingItem.out_parameters,
...fields.out_parameters,
};

// console.log('getFieldsValue', fields);

const res = {
...stagingItem,
...fields,
control_strategy: control_strategy,
in_parameters: in_parameters,
out_parameters: out_parameters,
control_strategy: JSON.stringify(control_strategy),
in_parameters: JSON.stringify(in_parameters),
out_parameters: JSON.stringify(out_parameters),
formError: !!error,
};

console.log('res', res);
// console.log('res', res);
onFormChange(res);
}
};
@@ -323,7 +335,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
<Drawer
title="编辑任务"
placement="right"
rootStyle={{ marginTop: '52px' }}
rootStyle={{ marginTop: '55px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
@@ -491,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>
);


+ 5
- 0
react-ui/src/pages/System/User/edit.tsx View File

@@ -199,6 +199,11 @@ const UserForm: React.FC<UserFormProps> = (props) => {
{
required: true,
},
{
pattern: /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/,
message:
'只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以数字或字母开头与结尾',
},
]}
/>
<ProFormText.Password


+ 1
- 1
react-ui/src/pages/System/User/index.tsx View File

@@ -235,7 +235,7 @@ const UserTableList: React.FC = () => {
const columns: ProColumns<API.System.User>[] = [
{
title: <FormattedMessage id="system.user.user_id" defaultMessage="用户编号" />,
dataIndex: 'deptId',
dataIndex: 'userId',
valueType: 'text',
},
{


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


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save