Browse Source

feat: 完成自动学习UI

pull/146/head
cp3hnu 1 year ago
parent
commit
27ff585924
44 changed files with 2763 additions and 7 deletions
  1. +3
    -2
      .gitignore
  2. +45
    -0
      package-lock.json
  3. +5
    -0
      package.json
  4. +21
    -0
      react-ui/config/routes.ts
  5. +1
    -0
      react-ui/src/app.tsx
  6. BIN
      react-ui/src/assets/img/editor-parameter.png
  7. BIN
      react-ui/src/assets/img/mirror-basic.png
  8. BIN
      react-ui/src/assets/img/model-deployment.png
  9. BIN
      react-ui/src/assets/img/search-config-icon.png
  10. BIN
      react-ui/src/assets/img/trial-config-icon.png
  11. +16
    -2
      react-ui/src/components/BasicInfo/index.tsx
  12. +2
    -2
      react-ui/src/components/KFIcon/index.tsx
  13. +1
    -1
      react-ui/src/iconfont/iconfont.js
  14. +47
    -0
      react-ui/src/pages/AutoML/Create/index.less
  15. +220
    -0
      react-ui/src/pages/AutoML/Create/index.tsx
  16. +40
    -0
      react-ui/src/pages/AutoML/Info/index.less
  17. +61
    -0
      react-ui/src/pages/AutoML/Info/index.tsx
  18. +20
    -0
      react-ui/src/pages/AutoML/List/index.less
  19. +311
    -0
      react-ui/src/pages/AutoML/List/index.tsx
  20. +7
    -0
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.less
  21. +81
    -0
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx
  22. +15
    -0
      react-ui/src/pages/AutoML/components/AutoMLTable/index.less
  23. +305
    -0
      react-ui/src/pages/AutoML/components/AutoMLTable/index.tsx
  24. +42
    -0
      react-ui/src/pages/AutoML/components/ConfigInfo/index.less
  25. +47
    -0
      react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx
  26. +38
    -0
      react-ui/src/pages/AutoML/components/ConfigTitle/index.less
  27. +22
    -0
      react-ui/src/pages/AutoML/components/ConfigTitle/index.tsx
  28. +18
    -0
      react-ui/src/pages/AutoML/components/CopyingText/index.less
  29. +33
    -0
      react-ui/src/pages/AutoML/components/CopyingText/index.tsx
  30. +85
    -0
      react-ui/src/pages/AutoML/components/CreateForm/BasicConfig.tsx
  31. +134
    -0
      react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx
  32. +258
    -0
      react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfigDLC.tsx
  33. +82
    -0
      react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfigMC.tsx
  34. +119
    -0
      react-ui/src/pages/AutoML/components/CreateForm/SearchConfig.tsx
  35. +193
    -0
      react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx
  36. +130
    -0
      react-ui/src/pages/AutoML/components/CreateForm/index.less
  37. +30
    -0
      react-ui/src/pages/AutoML/components/ExecuteScheduleCell/index.less
  38. +25
    -0
      react-ui/src/pages/AutoML/components/ExecuteScheduleCell/index.tsx
  39. +19
    -0
      react-ui/src/pages/AutoML/components/RunStatusCell/index.less
  40. +44
    -0
      react-ui/src/pages/AutoML/components/RunStatusCell/index.tsx
  41. +8
    -0
      react-ui/src/pages/AutoML/components/StatusChart/index.less
  42. +217
    -0
      react-ui/src/pages/AutoML/components/StatusChart/index.tsx
  43. +6
    -0
      react-ui/src/pages/AutoML/types.ts
  44. +12
    -0
      react-ui/src/utils/clipboard.js

+ 3
- 2
.gitignore View File

@@ -50,5 +50,6 @@ Thumbs.db
mvnw.cmd
mvnw

# Files or folders need to be retained
# ...
# web
**/node_modules


+ 45
- 0
package-lock.json View File

@@ -0,0 +1,45 @@
{
"name": "ci4sManagement-cloud",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"clipboard": "~2.0.11"
}
},
"node_modules/clipboard": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
}
}
}

+ 5
- 0
package.json View File

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

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

@@ -135,6 +135,27 @@ 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',
},
],
},
],
},
{


+ 1
- 0
react-ui/src/app.tsx View File

@@ -21,6 +21,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 { gotoLoginPage } from './utils/ui';


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

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

@@ -1,6 +1,7 @@
import { Link } from '@umijs/max';
import { Typography } from 'antd';
import classNames from 'classnames';
import React from 'react';
import './index.less';

export type BasicInfoLink = {
@@ -29,9 +30,12 @@ type BasicInfoItemProps = {
classPrefix: string;
};

type BasicInfoItemValueProps = BasicInfoLink & {
type BasicInfoItemValueProps = {
ellipsis?: boolean;
classPrefix: string;
value: string | React.ReactNode;
link?: string;
url?: string;
};

export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) {
@@ -69,6 +73,11 @@ export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemPr
))}
</div>
);
} else if (React.isValidElement(formatValue)) {
// 这个判断必须在下面的判断之前
valueComponent = (
<BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} />
);
} else if (typeof formatValue === 'object' && formatValue) {
valueComponent = (
<BasicInfoItemValue
@@ -115,13 +124,18 @@ export function BasicInfoItemValue({
{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}>
<Typography.Text
ellipsis={ellipsis ? { tooltip: value } : false}
style={{ fontSize: 'inherit' }}
>
{component}
</Typography.Text>
</div>


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

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


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

@@ -0,0 +1,47 @@
.create-service-version {
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.ant-btn-icon-only .anticon {
color: #565658;
font-size: 20px;
}

.anticon-question-circle {
margin-top: -12px;
color: @text-color-tertiary !important;
font-size: 12px !important;
}
}
}
}

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

@@ -0,0 +1,220 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建服务版本
*/
import PageTitle from '@/components/PageTitle';
import { type ParameterInputObject } from '@/components/ResourceSelect';
import { useComputingResource } from '@/hooks/resource';
import {
createServiceVersionReq,
getServiceInfoReq,
updateServiceVersionReq,
} from '@/services/modelDeployment';
import { changePropertyName } from '@/utils';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Form } from 'antd';
import { omit, pick } from 'lodash';
import { useEffect, useState } from 'react';
import BasicConfig from '../components/CreateForm/BasicConfig';
import ExecuteConfig from '../components/CreateForm/ExecuteConfig';
import SearchConfig from '../components/CreateForm/SearchConfig';
import TrialConfig from '../components/CreateForm/TrialConfig';
import { ServiceData, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less';

// 表单数据
export type FormData = {
service_name: string; // 服务名称
version: string; // 服务版本
description: string; // 描述
model: ParameterInputObject; // 模型
image: ParameterInputObject; // 镜像
code_config: ParameterInputObject; // 代码
resource: string; // 资源规格
replicas: string; // 副本数量
mount_path: string; // 模型路径
env_variables: { key: string; value: string }[]; // 环境变量
};

function CreateAutoML() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [resourceStandardList, filterResourceStandard] = useComputingResource();
const [operationType, setOperationType] = useState(ServiceOperationType.Create);

const { message } = App.useApp();
const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined);
const params = useParams();
const id = params.id;

useEffect(() => {
const res:
| (ServiceVersionData & {
operationType: ServiceOperationType;
})
| undefined = SessionStorage.getItem(SessionStorage.serviceVersionInfoKey, true);
if (res) {
setOperationType(res.operationType);

setVersionInfo(res);
let model, codeConfig, envVariables;
if (res.model && typeof res.model === 'object') {
model = changePropertyName(res.model, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value
model.value = model.showValue;
}
if (res.code_config && typeof res.code_config === 'object') {
codeConfig = changePropertyName(res.code_config, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value
codeConfig.value = codeConfig.showValue;
}
if (res.env_variables && typeof res.env_variables === 'object') {
envVariables = Object.entries(res.env_variables).map(([key, value]) => ({
key,
value,
}));
}

const formData = {
...omit(res, 'model', 'code_config', 'env_variables'),
model: model,
code_config: codeConfig,
env_variables: envVariables,
};
form.setFieldsValue(formData);
}
return () => {
SessionStorage.removeItem(SessionStorage.serviceVersionInfoKey);
};
}, []);

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

// 创建版本
const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables'] ?? [];
const image = formData['image'];
const model = formData['model'];
const codeConfig = formData['code_config'];
const envVariables = envList.reduce((acc, cur) => {
acc[cur.key] = cur.value;
return acc;
}, {} as Record<string, string>);

// 根据后台要求,修改表单数据
const object = {
...omit(formData, ['replicas', 'env_variables', 'image', 'model', 'code_config']),
replicas: Number(formData.replicas),
env_variables: envVariables,
image: image.value,
model: changePropertyName(
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,
};

const params =
operationType === ServiceOperationType.Create
? object
: {
id: versionInfo?.id,
rerun: operationType === ServiceOperationType.Restart ? true : false,
deployment_name: versionInfo?.deployment_name,
...object,
};

const request =
operationType === ServiceOperationType.Create
? createServiceVersionReq
: updateServiceVersionReq;

const [res] = await to(request(params));
if (res) {
message.success('操作成功');
navigate(-1);
}
};

// 提交
const handleSubmit = (values: FormData) => {
console.log('values', values);
};

// 取消
const cancel = () => {
navigate(-1);
};

const disabled = operationType !== ServiceOperationType.Create;
let buttonText = '新建';
let title = '新增服务版本';
if (operationType === ServiceOperationType.Update) {
title = '更新服务版本';
buttonText = '更新';
} else if (operationType === ServiceOperationType.Restart) {
title = '重启服务版本';
buttonText = '重启';
}

return (
<div className={styles['create-service-version']}>
<PageTitle title={title}></PageTitle>
<div className={styles['create-service-version__content']}>
<div>
<Form
name="create-service-version"
labelCol={{ flex: '140px' }}
labelAlign="left"
form={form}
onFinish={handleSubmit}
size="large"
autoComplete="off"
initialValues={{
execute_type: 'DLC',
image_type: 3,
advanced_config: [{ key: '', value: '' }],
}}
>
<BasicConfig />
<ExecuteConfig disabled={disabled} />
<TrialConfig />
<SearchConfig />

<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 { CommonTabKeys } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import themes from '@/styles/theme.less';
import { useNavigate } from '@umijs/max';
import { Tabs } from 'antd';
import { useState } from 'react';
import AutoMLBasic from '../components/AutoMLBasic';
import AutoMLTable from '../components/AutoMLTable';
import styles from './index.less';

export type MirrorData = {
id: number;
name: string;
description: string;
create_time: string;
};

function AutoMLInfo() {
const navigate = useNavigate();
const [cacheState, setCacheState] = useCacheState();
const [activeTab, setActiveTab] = useState<string>(cacheState?.activeTab ?? CommonTabKeys.Public);

const tabItems = [
{
key: CommonTabKeys.Public,
label: '基本信息',
icon: <KFIcon type="icon-jibenxinxi" />,
},
{
key: CommonTabKeys.Private,
label: 'Trial列表',
icon: <KFIcon type="icon-Trialliebiao" />,
},
];

return (
<div className={styles['auto-ml-info']}>
<div className={styles['auto-ml-info__tabs']}>
<Tabs items={tabItems} activeKey={activeTab} onChange={setActiveTab} />
</div>
<div className={styles['auto-ml-info__content']}>
{activeTab === CommonTabKeys.Public && <AutoMLBasic />}
{activeTab === CommonTabKeys.Private && <AutoMLTable />}
</div>
{activeTab === CommonTabKeys.Private && (
<div className={styles['auto-ml-info__tips']}>
<KFIcon type="icon-tishi" color={themes['warningColor']} font={17} />
<span style={{ marginLeft: '4px' }}>Trial是一次独立的尝试,他会使用某组超参来运行</span>
</div>
)}
</div>
);
}

export default AutoMLInfo;

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

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

@@ -0,0 +1,311 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 自主机器学习
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { useCacheState } from '@/hooks/pageCacheState';
import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Table,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import ExecuteScheduleCell from '../components/ExecuteScheduleCell';
import RunStatusCell from '../components/RunStatusCell';
import { ServiceData, ServiceOperationType } 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<ServiceData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);

useEffect(() => {
getServiceList();
}, [pagination, searchText]);

// 获取模型部署服务列表
const getServiceList = async () => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
service_name: searchText,
};
const [res] = await to(getServiceListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

// 删除模型部署
const deleteService = async (record: ServiceData) => {
const [res] = await to(deleteServiceReq(record.id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getServiceList();
}
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 处理删除
const handleServiceDelete = (record: ServiceData) => {
modalConfirm({
title: '删除后,该服务将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteService(record);
},
});
};

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

setCacheState({
pagination,
searchText,
});

navigate(`/pipeline/autoML/create`);
};

// 查看详情
const toDetail = (record: ServiceData) => {
setCacheState({
pagination,
searchText,
});

navigate(`/pipeline/autoML/info/${record.id}`);
};

// 分页切换
const handleTableChange: TableProps<ServiceData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};

const columns: TableProps<ServiceData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: '20%',
render: tableCellRender(false, TableCellValueType.Index, {
page: pagination.current! - 1,
pageSize: pagination.pageSize!,
}),
},
{
title: '实验名称',
dataIndex: 'service_name',
key: 'service_name',
width: '20%',
render: tableCellRender(false, TableCellValueType.Link, {
onClick: toDetail,
}),
},
{
title: '实验描述',
dataIndex: 'service_type_name',
key: 'service_type_name',
width: '20%',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '状态',
dataIndex: 'run_status',
key: 'run_status',
width: '20%',
render: RunStatusCell,
},
{
title: '实验实例执行进度',
dataIndex: 'description',
key: 'description',
render: ExecuteScheduleCell,
width: 180,
},
{
title: '创建时间',
dataIndex: 'update_time',
key: 'update_time',
width: '20%',
render: tableCellRender(true, TableCellValueType.Date),
ellipsis: { showTitle: false },
},
{
title: '修改时间',
dataIndex: 'update_time',
key: 'update_time',
width: '20%',
render: tableCellRender(true, TableCellValueType.Date),
ellipsis: { showTitle: false },
},
{
title: '操作',
dataIndex: 'operation',
width: 400,
key: 'operation',
render: (_: any, record: ServiceData) => (
<div>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-jingxiang" />}
onClick={() => createService(ServiceOperationType.Update, record)}
>
镜像
</Button>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => createService(ServiceOperationType.Update, record)}
>
编辑
</Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-fuzhi" />}
onClick={() => toDetail(record)}
>
复制
</Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => toDetail(record)}
>
停止
</Button>
<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleServiceDelete(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={() => createService(ServiceOperationType.Create)}
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}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default AutoMLList;

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

@@ -0,0 +1,7 @@
.auto-ml-basic {
height: 100%;
padding: 20px @content-padding;
overflow-y: auto;
background-color: white;
border-radius: 10px;
}

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

@@ -0,0 +1,81 @@
import { Flex } from 'antd';
import { useEffect } from 'react';
import ConfigInfo, { type BasicInfoData } from '../ConfigInfo';
import CopyingText from '../CopyingText';
import StatusChart from '../StatusChart';
import styles from './index.less';

function AutoMLBasic() {
useEffect(() => {}, []);

const datas: BasicInfoData[] = [
{
label: '项目名称',
value: '测试项目名称',
ellipsis: true,
},
{
label: '项目名称',
value: '测试项目名称',
ellipsis: true,
},
{
label: '项目名称',
value: '测试项目名称',
ellipsis: true,
},
{
label: '项目名称',
value: '测试项目名称',
ellipsis: true,
},
{
label: '项目名称',
value: '测试项目名称',
ellipsis: true,
},
{
label: '项目名称',
value: '测试项目名称',
ellipsis: true,
},
{
label: '项目名称',
value: '测试项目名称',
ellipsis: true,
},
{
label: '项目名称',
value: <CopyingText text="测试项目名称测试项目名称测试项目名称"></CopyingText>,
ellipsis: false,
},
];

return (
<div className={styles['auto-ml-basic']}>
<Flex gap={15} align="stretch">
<ConfigInfo title="基本信息" data={datas} labelWidth={70} />
<StatusChart
chartData={{ Failed: 10, Pending: 20, Running: 30, Succeeded: 40, Terminated: 50 }}
/>
<ConfigInfo title="Trial 配置" data={datas} labelWidth={70} />
</Flex>
<ConfigInfo
title="搜索配置"
data={datas}
style={{ marginTop: '16px' }}
labelWidth={70}
threeColumn
/>
<ConfigInfo
title="执行配置"
data={datas}
style={{ marginTop: '16px' }}
labelWidth={70}
threeColumn
/>
</div>
);
}

export default AutoMLBasic;

+ 15
- 0
react-ui/src/pages/AutoML/components/AutoMLTable/index.less View File

@@ -0,0 +1,15 @@
.auto-ml-table {
height: 100%;
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;
}
}

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

@@ -0,0 +1,305 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 自主机器学习
*/
import KFIcon from '@/components/KFIcon';
import { useCacheState } from '@/hooks/pageCacheState';
import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Table,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
// import ExecuteScheduleCell from '../components/ExecuteScheduleCell';
import { ServiceData, ServiceOperationType } from '@/pages/AutoML/types';
import RunStatusCell from '../RunStatusCell';
import styles from './index.less';

function AutoMLTable() {
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<ServiceData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);

useEffect(() => {
getServiceList();
}, [pagination, searchText]);

// 获取模型部署服务列表
const getServiceList = async () => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
service_name: searchText,
};
const [res] = await to(getServiceListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

// 删除模型部署
const deleteService = async (record: ServiceData) => {
const [res] = await to(deleteServiceReq(record.id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getServiceList();
}
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 处理删除
const handleServiceDelete = (record: ServiceData) => {
modalConfirm({
title: '删除后,该服务将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteService(record);
},
});
};

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

setCacheState({
pagination,
searchText,
});

navigate(`/modelDeployment/createService`);
};

// 查看详情
const toDetail = (record: ServiceData) => {
setCacheState({
pagination,
searchText,
});

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

// 分页切换
const handleTableChange: TableProps<ServiceData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};

const columns: TableProps<ServiceData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: '20%',
render: tableCellRender(false, TableCellValueType.Index, {
page: pagination.current! - 1,
pageSize: pagination.pageSize!,
}),
},
{
title: 'Trial ID',
dataIndex: 'service_name',
key: 'service_name',
width: '20%',
render: tableCellRender(false, TableCellValueType.Link, {
onClick: toDetail,
}),
},
{
title: '状态',
dataIndex: 'run_status',
key: 'run_status',
width: '20%',
render: RunStatusCell,
},
{
title: '最终指标',
dataIndex: 'service_type_name',
key: 'service_type_name',
width: '20%',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '当前指标',
dataIndex: 'description',
key: 'description',
width: '20%',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: 'lr',
dataIndex: 'description',
key: 'description',
width: '20%',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: 'batch_size',
dataIndex: 'description',
key: 'description',
width: '20%',
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: 'description',
key: 'description',
width: '20%',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '操作',
dataIndex: 'operation',
width: 400,
key: 'operation',
render: (_: any, record: ServiceData) => (
<div>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-rizhi" />}
onClick={() => createService(ServiceOperationType.Update, record)}
>
日志
</Button>

<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => toDetail(record)}
>
停止
</Button>
<ConfigProvider
theme={{
token: {
colorLink: themes['textColorTertiary'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-zhongpao" />}
onClick={() => handleServiceDelete(record)}
>
重跑
</Button>
</ConfigProvider>
</div>
),
},
];

return (
<div className={styles['auto-ml-table']}>
<div className={styles['auto-ml-table__filter']}>
<Input.Search
placeholder="按服务名称筛选"
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
value={inputText}
allowClear
/>
<Button
style={{ marginRight: 0, marginLeft: 'auto' }}
type="default"
onClick={() => {}}
icon={<KFIcon type="icon-shuaxin" />}
>
刷新
</Button>
</div>
<div className={classNames('vertical-scroll-table', styles['auto-ml-table__table'])}>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
);
}

export default AutoMLTable;

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

@@ -0,0 +1,42 @@
.config-info {
width: 100%;
border: 1px solid @border-color-base;
border-radius: 4px;

&__content {
padding: 20px;
padding: 20px @content-padding;
background-color: white;
}

:global {
.kf-basic-info {
gap: 15px 40px;
width: 100%;

&__item {
&__label {
font-size: @font-size;
text-align: left;
text-align-last: left;
&::after {
display: none;
}
}
&__value {
font-size: @font-size;
}
}
}
}

&--three-column {
:global {
.kf-basic-info {
&__item {
width: calc((100% - 80px) / 3);
}
}
}
}
}

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

@@ -0,0 +1,47 @@
import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo';
import classNames from 'classnames';
import { useEffect } from 'react';
import ConfigTitle from '../ConfigTitle';
import styles from './index.less';
export { type BasicInfoData };

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

function ConfigInfo({
title,
data,
className,
style,
children,
labelWidth,
threeColumn = false,
}: ConfigInfoProps) {
useEffect(() => {}, []);

return (
<div
className={classNames(
styles['config-info'],
{ [styles['config-info--three-column']]: threeColumn },
className,
)}
style={style}
>
<ConfigTitle title={title} />
<div className={styles['config-info__content']}>
<BasicInfo datas={data} labelWidth={labelWidth} />
{children}
</div>
</div>
);
}

export default ConfigInfo;

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

@@ -0,0 +1,38 @@
.config-title {
width: 100%;
height: 56px;
padding-left: @content-padding;
background: linear-gradient(
179.03deg,
rgba(199, 223, 255, 0.12) 0%,
rgba(22, 100, 255, 0.04) 100%
);
border: 1px solid #e8effb;

&__img {
width: 16px;
height: 16px;
margin-right: 10px;
}

&__text {
position: relative;
color: @text-color;
font-weight: 500;
font-size: @font-size-title;

&::after {
position: absolute;
bottom: 6px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(
to right,
.addAlpha(@primary-color, 0.4) [] 0,
.addAlpha(@primary-color, 0) [] 100%
);
content: '';
}
}
}

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

@@ -0,0 +1,22 @@
import { Flex } from 'antd';
import styles from './index.less';

type ConfigTitleProps = {
title: string;
};

function ConfigTitle({ title }: ConfigTitleProps) {
return (
<Flex align="center" className={styles['config-title']}>
<img
src={require('@/assets/img/code-name-icon.png')}
className={styles['config-title__img']}
alt=""
draggable={false}
/>
<span className={styles['config-title__text']}>{title}</span>
</Flex>
);
}

export default ConfigTitle;

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

@@ -0,0 +1,18 @@
.copying-text {
display: flex;
flex: 1;
align-items: center;
min-width: 0;
margin-left: 16px;

&__text {
color: @text-color;
font-size: 15px;
}

&__icon {
margin-left: 6px;
font-size: 14px;
cursor: pointer;
}
}

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

@@ -0,0 +1,33 @@
import KFIcon from '@/components/KFIcon';
import { Typography } from 'antd';

import styles from './index.less';

export type CopyingTextProps = {
text: string;
onCopySuccess?: () => void;
onCopyFailed?: () => void;
};

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;

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

@@ -0,0 +1,85 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { Col, Form, Input, Row, Select } 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="service_name"
rules={[
{
required: true,
message: '请输入服务名称',
},
{
pattern: /^(?!\d)[\u4e00-\u9fa5\w-]+$/,
message: '不能以数字开头,不能存在除了-_以外的特殊字符',
},
]}
>
<Input
placeholder="不能以数字开头,不能存在除了-_以外的特殊字符"
maxLength={256}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={20}>
<Form.Item
label="描述"
name="description"
rules={[
{
required: true,
message: '请输入描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入描述"
maxLength={256}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="可见范围"
name="version"
rules={[
{
required: true,
message: '请选择可见范围',
},
]}
>
<Select
allowClear
placeholder="请选择可见范围"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
</Col>
</Row>
</>
);
}

export default BasicConfig;

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

@@ -0,0 +1,134 @@
import KFIcon from '@/components/KFIcon';
import SubAreaTitle from '@/components/SubAreaTitle';
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { Button, Col, Flex, Form, Input, Radio, Row } from 'antd';
import ExecuteConfigDLC from './ExecuteConfigDLC';
import ExecuteConfigMC from './ExecuteConfigMC';
import styles from './index.less';

type ExecuteConfigProps = {
disabled?: boolean;
};

function ExecuteConfig({ disabled = false }: ExecuteConfigProps) {
const form = Form.useFormInstance();
const image_type = Form.useWatch('image_type', form);
console.log(image_type);

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="execute_type"
rules={[{ required: true, message: '请选择任务类型' }]}
>
<Radio.Group>
<Radio value={'DLC'}>DLC</Radio>
<Radio value={'MaxCompute'}>MaxCompute</Radio>
</Radio.Group>
</Form.Item>
</Col>
</Row>
<Form.Item dependencies={['execute_type']} noStyle>
{({ getFieldValue }) => {
return getFieldValue('execute_type') === 'DLC' ? (
<ExecuteConfigDLC disabled={disabled}></ExecuteConfigDLC>
) : (
<ExecuteConfigMC disabled={disabled}></ExecuteConfigMC>
);
}}
</Form.Item>

<Form.List name="hyper-parameter">
{(fields, { add, remove }) => (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="超参数"
style={{ marginBottom: 0, marginTop: '-14px' }}
tooltip="超参数"
></Form.Item>
</Col>
</Row>
<div className={styles['hyper-parameter']}>
<Flex align="center" className={styles['hyper-parameter__header']}>
<div className={styles['hyper-parameter__header__name']}>参数名称</div>
<div className={styles['hyper-parameter__header__type']}>约束类型</div>
<div className={styles['hyper-parameter__header__space']}>搜索空间</div>
<div className={styles['hyper-parameter__header__operation']}>操作</div>
</Flex>

{fields.map(({ key, name, ...restField }, index) => (
<Flex key={key} align="center" className={styles['hyper-parameter__body']}>
<Form.Item
className={styles['hyper-parameter__body__name']}
{...restField}
name={[name, 'name']}
rules={[{ required: true, message: 'Missing first name' }]}
>
<Input placeholder="Key" />
</Form.Item>
<Form.Item
className={styles['hyper-parameter__body__name']}
{...restField}
name={[name, 'type']}
rules={[{ required: true, message: 'Missing last name' }]}
>
<Input placeholder="Value" />
</Form.Item>
<Form.Item
className={styles['hyper-parameter__body__name']}
{...restField}
name={[name, 'space']}
rules={[{ required: true, message: 'Missing last name' }]}
>
<Input placeholder="Value" />
</Form.Item>
<div className={styles['hyper-parameter__body__operation']}>
<Button
style={{
marginRight: '3px',
}}
shape="circle"
disabled={fields.length === 1}
type="text"
size="middle"
onClick={() => remove(name)}
icon={<MinusCircleOutlined />}
></Button>
{index === fields.length - 1 && (
<Button
shape="circle"
size="middle"
type="text"
onClick={() => add()}
icon={<PlusCircleOutlined />}
></Button>
)}
</div>
</Flex>
))}
{fields.length === 0 && (
<div className={styles['hyper-parameter__add']}>
<Button type="link" onClick={() => add()} icon={<KFIcon type="icon-xinjian2" />}>
添加一行
</Button>
</div>
)}
</div>
</>
)}
</Form.List>
</>
);
}

export default ExecuteConfig;

+ 258
- 0
react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfigDLC.tsx View File

@@ -0,0 +1,258 @@
import CodeSelect from '@/components/CodeSelect';
import ResourceSelect, { ResourceSelectorType } from '@/components/ResourceSelect';
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd';
import styles from './index.less';

type ExecuteConfigDLCProps = {
disabled?: boolean;
};

function ExecuteConfigDLC({ disabled = false }: ExecuteConfigDLCProps) {
return (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="资源组"
name="version"
rules={[
{
required: false,
message: '请选择资源组',
},
]}
>
<Select
allowClear
placeholder="请选择资源组"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="框架"
name="version"
rules={[
{
required: false,
message: '请选择框架',
},
]}
>
<Select
allowClear
placeholder="请选择框架"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="数据集"
name="model"
// 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="code_config"
// rules={[
// {
// validator: requiredValidator,
// message: '请选择代码配置',
// },
// ]}
required
>
<CodeSelect
placeholder="请选择代码配置"
canInput={false}
size="large"
disabled={disabled}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="节点镜像"
name="image_type"
rules={[{ required: true, message: '请选择节点镜像' }]}
tooltip="节点镜像"
>
<Radio.Group>
<Radio value={1}>官方镜像</Radio>
<Radio value={2}>自定义镜像</Radio>
<Radio value={3}>镜像地址</Radio>
</Radio.Group>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item dependencies={['image_type']} noStyle>
{({ getFieldValue }) => {
const imageType = getFieldValue('image_type');
if (imageType === 1 || imageType === 2) {
return (
<Form.Item name="image_url" label=" " className="image-url">
<Select
allowClear
placeholder="请选择框架"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
);
} else {
return (
<Form.Item name="image_url" label=" " className="image-url">
<Input placeholder="请输入" disabled={disabled} />
</Form.Item>
);
}
}}
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="节点数量"
name="replicas"
rules={[
{
required: true,
message: '请输入节点数量',
},
{
pattern: /^[1-9]\d*$/,
message: '节点数量必须是正整数',
},
]}
>
<InputNumber placeholder="请输入节点数量" min={1} precision={0} />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="高级配置" tooltip="高级配置">
<Form.List name="advanced_config">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Flex key={key} align="center" className={styles['advanced-config']}>
<Form.Item
style={{ flex: 1, marginBottom: 0 }}
{...restField}
name={[name, 'key']}
rules={[{ required: true, message: 'Missing first name' }]}
>
<Input placeholder="Key" />
</Form.Item>
<span style={{ margin: '0 8px' }}>:</span>
<Form.Item
style={{ flex: 1, marginBottom: 0 }}
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: 'Missing last name' }]}
>
<Input placeholder="Value" />
</Form.Item>
<div style={{ width: '76px', marginLeft: '18px' }}>
<Button
style={{
marginRight: '3px',
}}
size="middle"
shape="circle"
disabled={fields.length === 1}
type="text"
onClick={() => remove(name)}
icon={<MinusCircleOutlined />}
></Button>
{index === fields.length - 1 && (
<Button
size="middle"
shape="circle"
type="text"
onClick={() => add()}
icon={<PlusCircleOutlined />}
></Button>
)}
</div>
</Flex>
))}
{fields.length === 0 && (
<Form.Item style={{ marginBottom: 0 }}>
<Button
style={{ background: 'white' }}
type="dashed"
onClick={() => add()}
block
icon={<PlusCircleOutlined />}
>
添加高级配置
</Button>
</Form.Item>
)}
</>
)}
</Form.List>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="节点启动命令"
name="mount_path"
// rules={[
// {
// required: true,
// message: '请输入节点启动命令',
// },
// ]}
tooltip="节点启动命令"
>
<Input placeholder="请输入节点启动命令" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>
</>
);
}

export default ExecuteConfigDLC;

+ 82
- 0
react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfigMC.tsx View File

@@ -0,0 +1,82 @@
import KFIcon from '@/components/KFIcon';
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { Button, Col, Flex, Form, Input, Row } from 'antd';
import styles from './index.less';

type ExecuteConfigMCProps = {
disabled?: boolean;
};

function ExecuteConfigMC({ disabled = false }: ExecuteConfigMCProps) {
return (
<>
<Form.List name="command">
{(fields, { add, remove }) => (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="命令"
style={{ marginBottom: 0, marginTop: '-14px' }}
tooltip="命令"
></Form.Item>
</Col>
</Row>
<div className={styles['command']}>
<Flex align="center" className={styles['command__header']}>
<div className={styles['command__header__name']}>Key</div>
<div className={styles['command__header__command']}>命令</div>
<div className={styles['command__header__operation']}>操作</div>
</Flex>

{fields.map(({ key, name, ...restField }, index) => (
<Flex key={key} align="center" className={styles['command__body']}>
<span className={styles['command__body__name']}>cmd{index + 1}</span>
<Form.Item
className={styles['command__body__command']}
{...restField}
name={[name, 'command']}
rules={[{ required: true, message: 'Missing last name' }]}
>
<Input.TextArea placeholder="Value" autoSize={{ minRows: 2, maxRows: 3 }} />
</Form.Item>
<div className={styles['command__body__operation']}>
<Button
style={{
marginRight: '3px',
}}
shape="circle"
disabled={fields.length === 1}
type="text"
size="middle"
onClick={() => remove(name)}
icon={<MinusCircleOutlined />}
></Button>
{index === fields.length - 1 && (
<Button
shape="circle"
size="middle"
type="text"
onClick={() => add()}
icon={<PlusCircleOutlined />}
></Button>
)}
</div>
</Flex>
))}
{fields.length === 0 && (
<div className={styles['hyper-parameter__add']}>
<Button type="link" onClick={() => add()} icon={<KFIcon type="icon-xinjian2" />}>
添加一行
</Button>
</div>
)}
</div>
</>
)}
</Form.List>
</>
);
}

export default ExecuteConfigMC;

+ 119
- 0
react-ui/src/pages/AutoML/components/CreateForm/SearchConfig.tsx View File

@@ -0,0 +1,119 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { Col, Form, InputNumber, Row, Select, Switch } from 'antd';
function SearchConfig() {
return (
<>
<SubAreaTitle
title="搜索配置"
image={require('@/assets/img/search-config-icon.png')}
style={{ marginTop: '20px', marginBottom: '24px' }}
></SubAreaTitle>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="搜索算法"
name="version"
rules={[
{
required: true,
message: '请选择搜索算法',
},
]}
tooltip="搜索算法"
>
<Select
allowClear
placeholder="请选择搜索算法"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="最大搜素次数"
name="replicas"
rules={[
{
required: true,
message: '请输入最大搜素次数',
},
{
pattern: /^[1-9]\d*$/,
message: '最大搜素次数必须是正整数',
},
]}
tooltip="最大搜素次数"
>
<InputNumber placeholder="请输入最大搜素次数" min={1} precision={0} />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="最大并发量"
name="replicas"
rules={[
{
required: true,
message: '请输入最大并发量',
},
{
pattern: /^[1-9]\d*$/,
message: '最大并发量必须是正整数',
},
]}
tooltip="最大并发量"
>
<InputNumber placeholder="请输入最大并发量" min={1} precision={0} />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="开启EarlyStop"
name="replicas"
rules={[
{
required: true,
message: '请选择是否开启EarlyStop',
},
]}
tooltip="开启EarlyStop"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭" defaultChecked />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="Start step"
name="replicas"
rules={[
{
required: true,
message: '请输入Start step',
},
{
pattern: /^[1-9]\d*$/,
message: 'Start step必须是正整数',
},
]}
tooltip="Start step"
>
<InputNumber placeholder="请输入Start step" min={1} precision={0} />
</Form.Item>
</Col>
</Row>
</>
);
}

export default SearchConfig;

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

@@ -0,0 +1,193 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { Button, Col, Flex, Form, Input, Radio, Row, Select } from 'antd';
import styles from './index.less';

function TrialConfig() {
return (
<>
<SubAreaTitle
title="Trial配置"
image={require('@/assets/img/trial-config-icon.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['trial-metrics']}>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="优化指标"></Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={24}>
<Form.Item
label="指标类型"
name="version"
rules={[
{
required: true,
message: '请选择指标类型',
},
]}
labelCol={{ flex: '120px' }}
>
<Select
allowClear
placeholder="请选择指标类型"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={24}>
<Form.Item
label="计算方式"
name="version"
rules={[
{
required: true,
message: '请选择计算方式',
},
]}
labelCol={{ flex: '120px' }}
>
<Select
allowClear
placeholder="请选择计算方式"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={24}>
<Form.Item label="指标权重" tooltip="指标权重" labelCol={{ flex: '120px' }}>
<Form.List name="metrics_weight">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Flex key={key} align="center" className={styles['advanced-config']}>
<Form.Item
style={{ flex: 1, marginBottom: 0 }}
{...restField}
name={[name, 'key']}
rules={[{ required: true, message: 'Missing first name' }]}
>
<Input placeholder="Key" />
</Form.Item>
<span style={{ margin: '0 8px' }}>:</span>
<Form.Item
style={{ flex: 1, marginBottom: 0 }}
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: 'Missing last name' }]}
>
<Input placeholder="Value" />
</Form.Item>
<div style={{ width: '76px', marginLeft: '18px' }}>
<Button
style={{
marginRight: '3px',
}}
shape="circle"
size="middle"
disabled={fields.length === 1}
type="text"
onClick={() => remove(name)}
icon={<MinusCircleOutlined />}
></Button>
{index === fields.length - 1 && (
<Button
shape="circle"
size="middle"
type="text"
onClick={() => add()}
icon={<PlusCircleOutlined />}
></Button>
)}
</div>
</Flex>
))}
{fields.length === 0 && (
<Form.Item style={{ marginBottom: 0 }}>
<Button
style={{ background: 'white' }}
type="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="service_name"
rules={[
{
required: true,
message: '请输入指标来源',
},
]}
tooltip="指标来源"
labelCol={{ flex: '120px' }}
>
<Input placeholder="请输入指标来源" maxLength={256} showCount allowClear />
</Form.Item>
</Col>
</Row>

<Row gutter={0}>
<Col span={24}>
<Form.Item
label="优化方向"
name="execute_type"
rules={[{ required: true, message: '请选择优化方向' }]}
labelCol={{ flex: '120px' }}
>
<Radio.Group>
<Radio value={1}>越大越好</Radio>
<Radio value={2}>越小越好</Radio>
</Radio.Group>
</Form.Item>
</Col>
</Row>
</div>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="模型名称"
name="service_name"
rules={[
{
required: true,
message: '请输入模型名称',
},
]}
tooltip="模型名称"
>
<Input placeholder="请输入模型名称" maxLength={256} showCount allowClear />
</Form.Item>
</Col>
</Row>
</>
);
}

export default TrialConfig;

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

@@ -0,0 +1,130 @@
.advanced-config {
margin-bottom: 20px;

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

.command {
width: 83.33%;
margin-bottom: 20px;
border: 1px solid rgba(234, 234, 234, 0.8);
border-radius: 4px;
&__header {
height: 50px;
padding-left: 8px;
color: @text-color;
font-size: @font-size;
background: #f8f8f9;
border-radius: 4px 4px 0px 0px;

&__name {
flex: none;
width: 100px;
}
&__command {
flex: 1;
margin-right: 15px;
}

&__operation {
flex: none;
width: 100px;
}
}
&__body {
padding: 8px;
border-bottom: 1px solid rgba(234, 234, 234, 0.8);

&:last-child {
border-bottom: none;
}

&__name {
flex: none;
width: 100px;
}

&__command {
flex: 1;
margin-right: 15px;
margin-bottom: 0 !important;
}

&__operation {
flex: none;
width: 100px;
}
}

&__add {
display: flex;
align-items: center;
justify-content: center;
padding: 15px 0;
}
}

.hyper-parameter {
width: 83.33%;
margin-bottom: 20px;
border: 1px solid rgba(234, 234, 234, 0.8);
border-radius: 4px;
&__header {
height: 50px;
padding-left: 8px;
color: @text-color;
font-size: @font-size;
background: #f8f8f9;
border-radius: 4px 4px 0px 0px;

&__name,
&__type,
&__space {
flex: 1;
margin-right: 15px;
}

&__operation {
flex: none;
width: 100px;
}
}
&__body {
padding: 8px;
border-bottom: 1px solid rgba(234, 234, 234, 0.8);

&:last-child {
border-bottom: none;
}

&__name,
&__type,
&__space {
flex: 1;
margin-right: 15px;
margin-bottom: 0 !important;
}

&__operation {
flex: none;
width: 100px;
}
}

&__add {
display: flex;
align-items: center;
justify-content: center;
padding: 15px 0;
}
}

.trial-metrics {
width: calc(41.67% - 6px);
margin-bottom: 20px;
padding: 0 20px;
border: 1px dashed #e0e0e0;
border-radius: 8px;
}

+ 30
- 0
react-ui/src/pages/AutoML/components/ExecuteScheduleCell/index.less View File

@@ -0,0 +1,30 @@
.execute-schedule-cell {
display: flex;
flex-direction: row;
align-items: center;

&__progress {
width: 112px;
height: 16px;
padding: 4px 5px;
background: rgba(96, 107, 122, 0.15);
border-radius: 8px;

&__bar {
height: 7px;
background: linear-gradient(270deg, #1664ff 0%, rgba(22, 100, 255, 0.1) 100%);
border-radius: 9px;
}
}

&__text {
margin-left: 6px;
color: @text-color-tertiary;
font-size: 12px;
letter-spacing: 2px;

strong {
color: @text-color;
}
}
}

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

@@ -0,0 +1,25 @@
/*
* @Author: 赵伟
* @Date: 2024-10-29 18:35:41
* @Description: 实验实例执行进度
*/

import styles from './index.less';

function ExecuteScheduleCell(status?: any) {
return (
<div className={styles['execute-schedule-cell']}>
<div className={styles['execute-schedule-cell__progress']}>
<div
className={styles['execute-schedule-cell__progress__bar']}
style={{ width: '80%' }}
></div>
</div>
<span className={styles['execute-schedule-cell__text']}>
1/<strong>2</strong>
</span>
</div>
);
}

export default ExecuteScheduleCell;

+ 19
- 0
react-ui/src/pages/AutoML/components/RunStatusCell/index.less View File

@@ -0,0 +1,19 @@
.run-status-cell {
color: @text-color;

&--running {
color: @primary-color;
}

&--stopped {
color: @abort-color;
}

&--error {
color: @error-color;
}

&--pending {
color: @warning-color;
}
}

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

@@ -0,0 +1,44 @@
/*
* @Author: 赵伟
* @Date: 2024-04-18 18:35:41
* @Description: 实验运行状态
*/
import { ServiceRunStatus } from '@/enums';
import styles from './index.less';

export type ServiceRunStatusInfo = {
text: string;
classname: string;
};

export const statusInfo: Record<ServiceRunStatus, ServiceRunStatusInfo> = {
[ServiceRunStatus.Init]: {
text: '启动中',
classname: styles['run-status-cell'],
},
[ServiceRunStatus.Running]: {
classname: styles['run-status-cell--running'],
text: '运行中',
},
[ServiceRunStatus.Stopped]: {
classname: styles['run-status-cell--stopped'],
text: '已停止',
},
[ServiceRunStatus.Failed]: {
classname: styles['run-status-cell--error'],
text: '失败',
},
[ServiceRunStatus.Pending]: {
classname: styles['run-status-cell--pending'],
text: '挂起中',
},
};

function RunStatusCell(status?: ServiceRunStatus | null) {
if (status === null || status === undefined || !statusInfo[status]) {
return <span>--</span>;
}
return <span className={statusInfo[status].classname}>{statusInfo[status].text}</span>;
}

export default RunStatusCell;

+ 8
- 0
react-ui/src/pages/AutoML/components/StatusChart/index.less View File

@@ -0,0 +1,8 @@
.status-chart {
flex: none;
width: 380px;
min-width: 380px;
background: #ffffff;
border: 1px solid @border-color-base;
border-radius: 4px;
}

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

@@ -0,0 +1,217 @@
import themes from '@/styles/theme.less';
import * as echarts from 'echarts';
import React, { useEffect, useRef } from 'react';
import ConfigTitle from '../ConfigTitle';
import styles from './index.less';

const color1 = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: '#c73131' },
{ offset: 1, color: '#ff7e96' },
],
false,
);

const color2 = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: '#6ac21d' },
{ offset: 1, color: '#96e850' },
],
false,
);
const color3 = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: '#8c8c8c' },
{ offset: 1, color: '#c8c6c6' },
],
false,
);
const color4 = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: '#ecb934' },
{ offset: 1, color: '#f0864d' },
],
false,
);

const color5 = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: '#7ea9fe' },
{ offset: 1, color: '#1664ff' },
],
false,
);

const color6 = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: 'rgba(255, 255, 255, 0.62)' },
{ offset: 1, color: '#ebf2ff ' },
],
false,
);

export type ExperimentStatistics = {
Failed: number;
Pending: number;
Running: number;
Succeeded: number;
Terminated: number;
};

type ExperimentChartProps = {
style?: React.CSSProperties;
chartData: ExperimentStatistics;
};

function StatusChart({ chartData, style }: ExperimentChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
const total =
chartData.Failed +
chartData.Pending +
chartData.Running +
chartData.Succeeded +
chartData.Terminated;
const options: echarts.EChartsOption = {
title: {
show: true,
left: '29%',
top: 'center',
textAlign: 'center',
text: [`{a|${total}}`, '{b|Trials}'].join('\n'),
textStyle: {
rich: {
a: {
color: themes['textColor'],
fontSize: 20,
fontWeight: 700,
lineHeight: 28,
},
b: {
color: themes['textColorSecondary'],
fontSize: 10,
fontWeight: 'normal',
},
},
},
},
tooltip: {
trigger: 'item',
},
legend: {
top: 'center',
right: '5%',
orient: 'vertical',
icon: 'circle',
itemWidth: 6,
itemGap: 20,
height: 100,
},
color: [color1, color2, color3, color4, color5],
series: [
{
type: 'pie',
radius: ['70%', '80%'],
center: ['30%', '50%'],
avoidLabelOverlap: false,
padAngle: 3,
itemStyle: {
borderRadius: 3,
},
minAngle: 5,
label: {
show: false,
},
emphasis: {
label: {
show: false,
},
},
labelLine: {
show: false,
},
data: [
{ value: chartData.Failed > 0 ? chartData.Failed : undefined, name: '失败' },
{ value: chartData.Succeeded > 0 ? chartData.Succeeded : undefined, name: '成功' },
{ value: chartData.Terminated > 0 ? chartData.Terminated : undefined, name: '中止' },
{ value: chartData.Pending > 0 ? chartData.Pending : undefined, name: '等待' },
{ value: chartData.Running > 0 ? chartData.Running : undefined, name: '运行中' },
],
},
{
type: 'pie',
radius: '60%',
center: ['30%', '50%'],
avoidLabelOverlap: false,
label: {
show: false,
},
tooltip: {
show: false,
},
emphasis: {
label: {
show: false,
},
disabled: true,
},
animation: false,
labelLine: {
show: false,
},
data: [
{
value: 100,
itemStyle: { color: color6, borderColor: 'rgba(22, 100, 255, 0.08)', borderWidth: 1 },
},
],
},
],
};

useEffect(() => {
// 创建一个echarts实例,返回echarts实例
const chart = echarts.init(chartRef.current);

// 设置图表实例的配置项和数据
chart.setOption(options);

// 组件卸载
return () => {
// myChart.dispose() 销毁实例
chart.dispose();
};
}, []);

return (
<div className={styles['status-chart']} style={style}>
<ConfigTitle title="Trial 状态统计" />
<div style={{ width: '100%', height: '185px' }} ref={chartRef}></div>
</div>
);
}

export default StatusChart;

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

@@ -0,0 +1,6 @@
// 操作类型
export enum ServiceOperationType {
Create = 'Create', // 创建
Update = 'Update', // 更新
Restart = 'Restart', // 重启
}

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

@@ -0,0 +1,12 @@
import ClipboardJS from 'clipboard';
import { message } from "antd";

const clipboard = new ClipboardJS('#copying');

clipboard.on('success', () => {
message.success('复制成功');
});

clipboard.on('error', () => {
message.error('复制失败');
});

Loading…
Cancel
Save