refactor: create cluster by steps
@@ -168,7 +168,7 @@ export default [
|
||||
},
|
||||
{
|
||||
name: 'clusterDetail',
|
||||
path: '/cluster-management/cluster/detail',
|
||||
path: '/cluster-management/clusters/detail',
|
||||
key: 'clusterDetail',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
@@ -176,6 +176,16 @@ export default [
|
||||
hideInMenu: true,
|
||||
component: './cluster-management/cluster-detail'
|
||||
},
|
||||
{
|
||||
name: 'clusterCreate',
|
||||
path: '/cluster-management/clusters/create',
|
||||
key: 'clusterCreate',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
hideInMenu: true,
|
||||
component: './cluster-management/cluster-create'
|
||||
},
|
||||
{
|
||||
name: 'credentials',
|
||||
path: '/cluster-management/credentials',
|
||||
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -5,20 +5,6 @@ import React, { useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import FormWidget from './form-widget';
|
||||
|
||||
interface ListMapProps {
|
||||
dataList: any[];
|
||||
label?: React.ReactNode;
|
||||
btnText?: string;
|
||||
properties: Record<string, any>;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
schemaList: any[];
|
||||
data: Record<string, any>;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
const RowWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -32,6 +18,20 @@ const WidgetBox = styled.div`
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
`;
|
||||
interface ListMapProps {
|
||||
minItems?: number;
|
||||
dataList: any[];
|
||||
label?: React.ReactNode;
|
||||
btnText?: string;
|
||||
properties: Record<string, any>;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
schemaList: any[];
|
||||
data: Record<string, any>;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({ schemaList, data, onChange }) => {
|
||||
const handleValueChange = (name: string, target: any) => {
|
||||
@@ -65,6 +65,7 @@ const ListMap: React.FC<ListMapProps> = ({
|
||||
label,
|
||||
btnText,
|
||||
properties = {},
|
||||
minItems = 0,
|
||||
onChange
|
||||
}) => {
|
||||
const [items, setItems] = React.useState(dataList || []);
|
||||
@@ -99,11 +100,15 @@ const ListMap: React.FC<ListMapProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataList.length) {
|
||||
if (!dataList.length && minItems > 0) {
|
||||
handleOnAdd();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setItems(dataList);
|
||||
}, [dataList]);
|
||||
|
||||
return (
|
||||
<Wrapper label={label} btnText={btnText} onAdd={handleOnAdd}>
|
||||
{items.map((item, index) => (
|
||||
|
||||
@@ -35,7 +35,13 @@ const icons = {
|
||||
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }),
|
||||
Docker: React.createElement(IconFont, { type: 'icon-docker' }),
|
||||
DigitalOcean: React.createElement(IconFont, { type: 'icon-digitalocean' }),
|
||||
DetailInfo: React.createElement(IconFont, { type: 'icon-detail-info' })
|
||||
DetailInfo: React.createElement(IconFont, { type: 'icon-detail-info' }),
|
||||
HuaweiCloud: React.createElement(IconFont, { type: 'icon-huaweicloud' }),
|
||||
AliCloud: React.createElement(IconFont, { type: 'icon-alicloud' }),
|
||||
TencentCloud: React.createElement(IconFont, { type: 'icon-tencentcloud' }),
|
||||
Nvidia: React.createElement(IconFont, { type: 'icon-nvidia' }),
|
||||
Ascend: React.createElement(IconFont, { type: 'icon-ascend' }),
|
||||
AMD: React.createElement(IconFont, { type: 'icon-amd' })
|
||||
};
|
||||
|
||||
export default icons;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
||||
// import './iconfont/iconfont.js';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_wy0l8ooa03c.js'
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_l0y0uhurkz8.js'
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
width: max-content;
|
||||
height: 24px;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
|
||||
.txt {
|
||||
|
||||
@@ -10,20 +10,23 @@ interface CardProps {
|
||||
ghost?: boolean;
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const CardWrapper = styled.div`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 22px;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
cursor: default;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
&.clickable:hover:not(.disabled) {
|
||||
background-color: var(--ant-color-fill-tertiary);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
@@ -35,15 +38,47 @@ const CardWrapper = styled.div`
|
||||
&.active {
|
||||
background-color: var(--ant-color-fill-tertiary);
|
||||
}
|
||||
&.clickable {
|
||||
|
||||
&.clickable:not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
background-color: var(--ant-color-fill-quaternary);
|
||||
}
|
||||
`;
|
||||
|
||||
const CardContent = styled.div`
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
`;
|
||||
|
||||
const Inner = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const Icon = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 16px;
|
||||
font-size: 46px;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const Card: React.FC<CardProps> = (props) => {
|
||||
@@ -55,6 +90,9 @@ const Card: React.FC<CardProps> = (props) => {
|
||||
ghost = false,
|
||||
header,
|
||||
footer,
|
||||
icon,
|
||||
active,
|
||||
disabled,
|
||||
onClick
|
||||
} = props;
|
||||
|
||||
@@ -62,14 +100,19 @@ const Card: React.FC<CardProps> = (props) => {
|
||||
<CardWrapper
|
||||
className={classNames(className, {
|
||||
clickable: clickable,
|
||||
active: active,
|
||||
disabled: disabled,
|
||||
ghost: ghost
|
||||
})}
|
||||
style={{ height: height || '180px' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{header}
|
||||
<CardContent>{children}</CardContent>
|
||||
{footer}
|
||||
{icon && <Icon>{icon}</Icon>}
|
||||
<Inner>
|
||||
{header && <Header>{header}</Header>}
|
||||
<CardContent>{children}</CardContent>
|
||||
{footer}
|
||||
</Inner>
|
||||
</CardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,8 +63,9 @@ html {
|
||||
--ant-color-success: #54cc98;
|
||||
--ant-color-error-bg: #fff2f0;
|
||||
--ant-color-error: #ff4d4f;
|
||||
--ant-box-shadow-secondary: 0 6px 16px 0 rgba(0, 0, 0, 8%),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 12%), 0 9px 28px 8px rgba(0, 0, 0, 5%);
|
||||
--ant-box-shadow-secondary:
|
||||
0 6px 16px 0 rgba(0, 0, 0, 8%), 0 3px 6px -4px rgba(0, 0, 0, 12%),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 5%);
|
||||
--color-chart-red: rgba(255, 77, 79, 80%);
|
||||
--color-chart-green: rgb(84, 204, 152, 80%);
|
||||
--color-chart-glod: rgba(250, 173, 20, 80%);
|
||||
@@ -823,3 +824,11 @@ body {
|
||||
.ant-progress-line {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.ant-pro-page-container-affix .ant-affix .ant-pro-page-container-warp {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ant-page-header .ant-page-header-heading-extra {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
@@ -23,5 +23,7 @@ export default {
|
||||
'clusters.workerpool.volumes': 'Volumes',
|
||||
'clusters.workerpool.format': 'Format',
|
||||
'clusters.workerpool.size': 'Size (GiB)',
|
||||
'clusters.workerpool.title': 'Worker Pools'
|
||||
'clusters.workerpool.title': 'Worker Pools',
|
||||
'clusters.workerpool.cloudOptions': 'Add Cloud Options',
|
||||
'clusters.workerpool.volumes.add': 'Add Volume'
|
||||
};
|
||||
|
||||
@@ -259,5 +259,6 @@ export default {
|
||||
'common.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||
'common.button.edit.item': 'Edit {name}',
|
||||
'common.button.terminal': 'Terminal'
|
||||
'common.button.terminal': 'Terminal',
|
||||
'common.button.addItem': 'Add Item'
|
||||
};
|
||||
|
||||
@@ -30,5 +30,6 @@ export default {
|
||||
'menu.clusterManagement': 'Cluster Management',
|
||||
'menu.clusterManagement.clusters': 'Clusters',
|
||||
'menu.clusterManagement.credentials': 'Credentials',
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
'menu.clusterManagement.clusterCreate': 'Create Cluster'
|
||||
};
|
||||
|
||||
@@ -23,7 +23,9 @@ export default {
|
||||
'clusters.workerpool.volumes': 'Volumes',
|
||||
'clusters.workerpool.format': 'Format',
|
||||
'clusters.workerpool.size': 'Size (GiB)',
|
||||
'clusters.workerpool.title': 'Worker Pools'
|
||||
'clusters.workerpool.title': 'Worker Pools',
|
||||
'clusters.workerpool.cloudOptions': 'Add Cloud Options',
|
||||
'clusters.workerpool.volumes.add': 'Add Volume'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -51,5 +53,8 @@ export default {
|
||||
// 22. 'clusters.workerpool.format': 'Format',
|
||||
// 23. 'clusters.workerpool.size': 'Size (GiB)',
|
||||
// 24. 'clusters.credential.title': 'Credential',
|
||||
// 25. 'clusters.workerpool.title': 'Worker Pools'
|
||||
// 25. 'clusters.workerpool.title': 'Worker Pools',
|
||||
// 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
|
||||
// 27. 'clusters.workerpool.volumes.add': 'Add Volume'
|
||||
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -259,7 +259,8 @@ export default {
|
||||
'common.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||
'common.button.edit.item': 'Edit {name}',
|
||||
'common.button.terminal': 'Terminal'
|
||||
'common.button.terminal': 'Terminal',
|
||||
'common.button.addItem': 'Add Item'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -284,5 +285,6 @@ export default {
|
||||
// 19. 'common.login.password': 'Log in with Password',
|
||||
// 20. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
|
||||
// 21. 'common.button.edit.item': 'Edit {name}',
|
||||
// 22. 'common.button.terminal': 'Terminal'
|
||||
// 22. 'common.button.terminal': 'Terminal',
|
||||
// 23. 'common.button.addItem': 'Add Item'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -30,7 +30,8 @@ export default {
|
||||
'menu.clusterManagement.clusters': 'Clusters',
|
||||
'menu.clusterManagement.credentials': 'Credentials',
|
||||
'menu.models.userModels': 'My Models',
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
'menu.clusterManagement.clusterCreate': 'Create Cluster'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -45,5 +46,6 @@ export default {
|
||||
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
||||
// 10. 'menu.clusterManagement.credentials': 'Credentials',
|
||||
// 11. 'menu.models.userModels': 'My Models'
|
||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -23,7 +23,9 @@ export default {
|
||||
'clusters.workerpool.volumes': 'Volumes',
|
||||
'clusters.workerpool.format': 'Format',
|
||||
'clusters.workerpool.size': 'Size (GiB)',
|
||||
'clusters.workerpool.title': 'Worker Pools'
|
||||
'clusters.workerpool.title': 'Worker Pools',
|
||||
'clusters.workerpool.cloudOptions': 'Add Cloud Options',
|
||||
'clusters.workerpool.volumes.add': 'Add Volume'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -51,5 +53,7 @@ export default {
|
||||
// 22. 'clusters.workerpool.volumes': 'Volumes',
|
||||
// 23. 'clusters.workerpool.format': 'Format',
|
||||
// 24. 'clusters.workerpool.size': 'Size (GiB)',
|
||||
// 25. 'clusters.workerpool.title': 'Worker Pools'
|
||||
// 25. 'clusters.workerpool.title': 'Worker Pools',
|
||||
// 26. 'clusters.workerpool.cloudOptions': 'Add Cloud Options',
|
||||
// 27. 'clusters.workerpool.volumes.add': 'Add Volume'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -258,7 +258,8 @@ export default {
|
||||
'common.sso.noConfig':
|
||||
'Single sign-on is not enabled on this system. Please contact your administrator.',
|
||||
'common.button.edit.item': 'Edit {name}',
|
||||
'common.button.terminal': 'Terminal'
|
||||
'common.button.terminal': 'Terminal',
|
||||
'common.button.addItem': 'Add Item'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -266,5 +267,6 @@ export default {
|
||||
// 2. 'common.login.password': 'Log in with Password'
|
||||
// 3. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
|
||||
// 4. 'common.button.edit.item': 'Edit {name}',
|
||||
// 5. 'common.button.terminal': 'Terminal'
|
||||
// 5. 'common.button.terminal': 'Terminal',
|
||||
// 6. 'common.button.addItem': 'Add Item'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -30,7 +30,8 @@ export default {
|
||||
'menu.clusterManagement.clusters': 'Clusters',
|
||||
'menu.clusterManagement.credentials': 'Credentials',
|
||||
'menu.models.userModels': 'My Models',
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
'menu.clusterManagement.clusterCreate': 'Create Cluster'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -45,5 +46,6 @@ export default {
|
||||
// 9. 'menu.clusterManagement.clusters': 'Clusters',
|
||||
// 10. 'menu.clusterManagement.credentials': 'Credentials',
|
||||
// 11. 'menu.models.userModels': 'My Models',
|
||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail'
|
||||
// 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
|
||||
// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -23,5 +23,7 @@ export default {
|
||||
'clusters.workerpool.volumes': '存储卷',
|
||||
'clusters.workerpool.format': '文件系统格式',
|
||||
'clusters.workerpool.size': '容量(GiB)',
|
||||
'clusters.workerpool.title': '节点池'
|
||||
'clusters.workerpool.title': '节点池',
|
||||
'clusters.workerpool.cloudOptions': '添加云配置',
|
||||
'clusters.workerpool.volumes.add': '添加存储卷'
|
||||
};
|
||||
|
||||
@@ -251,5 +251,6 @@ export default {
|
||||
'common.external.login': '使用 {type} 登录',
|
||||
'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。',
|
||||
'common.button.edit.item': '编辑 {name}',
|
||||
'common.button.terminal': '终端'
|
||||
'common.button.terminal': '终端',
|
||||
'common.button.addItem': '添加项'
|
||||
};
|
||||
|
||||
@@ -30,5 +30,6 @@ export default {
|
||||
'menu.clusterManagement': '集群管理',
|
||||
'menu.clusterManagement.clusters': '集群',
|
||||
'menu.clusterManagement.credentials': '凭证',
|
||||
'menu.clusterManagement.clusterDetail': '集群详情'
|
||||
'menu.clusterManagement.clusterDetail': '集群详情',
|
||||
'menu.clusterManagement.clusterCreate': '创建集群'
|
||||
};
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { queryClusterList } from './apis';
|
||||
import ClusterSteps from './components/cluster-steps';
|
||||
import FooterButtons from './components/footer-buttons';
|
||||
import ProviderCatalog from './components/provider-catalog';
|
||||
import { providerList, ProviderType } from './config';
|
||||
import { ClusterFormData } from './config/types';
|
||||
import { moduleMap, moduleRegistry } from './step-forms/module-registry';
|
||||
import useStepList from './step-forms/use-step-list';
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
width: 600px;
|
||||
`;
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
padding-inline: var(--layout-content-header-inlinepadding);
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
min-height: 72px;
|
||||
.text {
|
||||
margin-right: 16px;
|
||||
padding-right: 16px;
|
||||
border-right: 1px solid var(--ant-color-split);
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
margin-right: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ClusterCreate = () => {
|
||||
const intl = useIntl();
|
||||
const stepList = useStepList();
|
||||
const [searchParams] = useSearchParams();
|
||||
const action =
|
||||
(searchParams.get('action') as PageActionType) || PageAction.CREATE;
|
||||
const navigate = useNavigate();
|
||||
const [credentialList, setCredentialList] = useState<
|
||||
Global.BaseOption<number>[]
|
||||
>([]);
|
||||
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||
const [registrationInfo, setRegistrationInfo] = useState<{
|
||||
token: string;
|
||||
image: string;
|
||||
server_url: string;
|
||||
cluster_id: number;
|
||||
}>({
|
||||
token: '',
|
||||
image: '',
|
||||
server_url: '',
|
||||
cluster_id: 0
|
||||
});
|
||||
const [extraData, setExtraData] = useState<ClusterFormData>({
|
||||
provider: null
|
||||
} as ClusterFormData);
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||
|
||||
const formRefs: Record<string, any> = {
|
||||
[moduleMap.BasicForm]: useRef<any>(null),
|
||||
[moduleMap.WorkerPoolForm]: useRef<any>(null)
|
||||
};
|
||||
|
||||
const steps = useMemo(() => {
|
||||
if (!extraData.provider) {
|
||||
return stepList.filter((step) => step.defaultShow);
|
||||
}
|
||||
|
||||
return stepList
|
||||
.filter((step) =>
|
||||
step.providers?.length
|
||||
? step.providers.includes(extraData.provider as never)
|
||||
: true
|
||||
)
|
||||
.map((step, index) => ({
|
||||
...step,
|
||||
disabled: action === PageAction.CREATE
|
||||
}));
|
||||
}, [action, extraData.provider, stepList]);
|
||||
|
||||
const handleStepChange = (newStep: number) => {
|
||||
setCurrentStep(newStep);
|
||||
console.log('step========', newStep);
|
||||
};
|
||||
const customizer = (objValue: any, srcValue: any) => {
|
||||
if (Array.isArray(objValue)) {
|
||||
return srcValue;
|
||||
}
|
||||
};
|
||||
const getFormFieldsValue = () => {
|
||||
setFormValues((prev) => {
|
||||
const newFormValues = _.cloneDeep(prev);
|
||||
for (const [formKey, formRef] of Object.entries(formRefs)) {
|
||||
const formValues = formRef?.current?.getFieldsValue?.();
|
||||
if (formValues) {
|
||||
newFormValues[formKey] = formValues;
|
||||
}
|
||||
}
|
||||
return newFormValues;
|
||||
});
|
||||
};
|
||||
|
||||
const onPrevious = () => {
|
||||
getFormFieldsValue();
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 0));
|
||||
};
|
||||
|
||||
const validateForms = async () => {
|
||||
const step = steps[currentStep];
|
||||
const formKeys = step?.showForms || [];
|
||||
if (formKeys?.length > 0) {
|
||||
const results = await Promise.allSettled(
|
||||
formKeys.map((key) => formRefs[key].current?.validateFields())
|
||||
);
|
||||
|
||||
console.log('results========', results);
|
||||
|
||||
const resultsMap = new Map<string, any>();
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
const values = result.reason?.values || {};
|
||||
resultsMap.set(formKeys[index], values);
|
||||
} else if (result.status === 'fulfilled') {
|
||||
resultsMap.set(formKeys[index], result.value);
|
||||
}
|
||||
});
|
||||
const newValues = _.merge(formValues, Object.fromEntries(resultsMap));
|
||||
|
||||
setFormValues(newValues);
|
||||
|
||||
const isValid = results.every((result) => result.status === 'fulfilled');
|
||||
if (!isValid) {
|
||||
return false;
|
||||
}
|
||||
return Object.assign({}, ...Object.values(newValues));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const onNext = async (callback?: (values: ClusterFormData) => void) => {
|
||||
try {
|
||||
const result = await validateForms();
|
||||
console.log('results========2', result);
|
||||
if (result) {
|
||||
await callback?.(result as ClusterFormData);
|
||||
const step = steps[currentStep];
|
||||
step.beforeNext?.();
|
||||
setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('next error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectProvider = (value: ProviderType) => {
|
||||
setExtraData({
|
||||
provider: value
|
||||
} as ClusterFormData);
|
||||
onNext();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await queryClusterList({ page: 1, perPage: 100 });
|
||||
const list = res.items?.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}));
|
||||
setCredentialList(list);
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const renderForms = () => {
|
||||
const step = steps[currentStep];
|
||||
const formKeys = step?.showForms || [];
|
||||
if (formKeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return formKeys.map((key) => {
|
||||
console.log('renderForms key========', key, formValues);
|
||||
const FormComponent = moduleRegistry[key];
|
||||
return FormComponent ? (
|
||||
<FormComponent
|
||||
key={key}
|
||||
ref={formRefs[key]}
|
||||
action={action}
|
||||
provider={extraData.provider}
|
||||
credentialList={credentialList}
|
||||
currentData={formValues[key]}
|
||||
/>
|
||||
) : null;
|
||||
});
|
||||
};
|
||||
|
||||
const renderModules = () => {
|
||||
const step = steps[currentStep];
|
||||
const moduleKeys = step?.showModules || [];
|
||||
if (moduleKeys.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return moduleKeys.map((key) => {
|
||||
const ModuleComponent = moduleRegistry[key];
|
||||
return ModuleComponent ? (
|
||||
<ModuleComponent key={key} registrationInfo={registrationInfo} />
|
||||
) : null;
|
||||
});
|
||||
};
|
||||
|
||||
const showButtons = useMemo(() => {
|
||||
const step = steps[currentStep];
|
||||
return step?.showButtons ? step?.showButtons(extraData.provider) : {};
|
||||
}, [currentStep, steps, extraData.provider]);
|
||||
|
||||
const submit = async (values: ClusterFormData) => {
|
||||
const data = {
|
||||
...extraData,
|
||||
...(typeof values === 'object' ? values : {})
|
||||
};
|
||||
console.log('submit values====:', data);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
onNext(submit);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
ghost
|
||||
fixedHeader
|
||||
affixProps={{
|
||||
offsetTop: 0
|
||||
}}
|
||||
footer={[
|
||||
<FooterButtons
|
||||
key="buttons"
|
||||
onPrevious={onPrevious}
|
||||
onNext={onNext}
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
showButtons={showButtons}
|
||||
/>
|
||||
]}
|
||||
pageHeaderRender={() => (
|
||||
<HeaderContainer>
|
||||
<span className="text">
|
||||
<span>Create Cluster</span>
|
||||
</span>
|
||||
<ClusterSteps
|
||||
steps={steps}
|
||||
currentStep={currentStep}
|
||||
onChange={handleStepChange}
|
||||
></ClusterSteps>
|
||||
</HeaderContainer>
|
||||
)}
|
||||
>
|
||||
{currentStep === 0 && (
|
||||
<ProviderCatalog
|
||||
dataList={providerList}
|
||||
onSelect={handleSelectProvider}
|
||||
clickable={true}
|
||||
currentProvider={extraData.provider}
|
||||
/>
|
||||
)}
|
||||
{renderModules()}
|
||||
<Container>
|
||||
<Content>{renderForms()}</Content>
|
||||
</Container>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClusterCreate;
|
||||
@@ -1,8 +1,6 @@
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useSearchParams } from '@umijs/max';
|
||||
import ClusterMetrics from './components/cluster-metrics';
|
||||
import WorkerPools from './components/worker-pools';
|
||||
import { ProviderValueMap } from './config';
|
||||
|
||||
const ClusterDetailModal = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -25,7 +23,7 @@ const ClusterDetailModal = () => {
|
||||
extra={[]}
|
||||
>
|
||||
<ClusterMetrics />
|
||||
{provider === ProviderValueMap.DigitalOcean && <WorkerPools />}
|
||||
{/* {provider === ProviderValueMap.DigitalOcean && <WorkerPools />} */}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import AddCluster from './components/add-cluster';
|
||||
import AddPool from './components/add-pool';
|
||||
import RegisterCluster from './components/register-cluster';
|
||||
import { ProviderLabelMap, ProviderValueMap, addActions } from './config';
|
||||
import { ProviderLabelMap, ProviderValueMap } from './config';
|
||||
import {
|
||||
ClusterFormData as FormData,
|
||||
ClusterListItem as ListItem,
|
||||
@@ -146,7 +146,8 @@ const Credentials: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleClickDropdown = (item: any) => {
|
||||
handleAddCluster(item.key);
|
||||
navigate(`/cluster-management/clusters/create?action=${PageAction.CREATE}`);
|
||||
// handleAddCluster(item.key);
|
||||
};
|
||||
|
||||
const handleModalOk = async (data: FormData) => {
|
||||
@@ -298,8 +299,6 @@ const Credentials: React.FC = () => {
|
||||
marginTop={30}
|
||||
width={{ input: 300 }}
|
||||
buttonText={intl.formatMessage({ id: 'clusters.button.add' })}
|
||||
actionType="dropdown"
|
||||
actionItems={addActions}
|
||||
rowSelection={rowSelection}
|
||||
handleInputChange={handleNameChange}
|
||||
handleSearch={handleSearch}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import ModalFooter from '@/components/modal-footer';
|
||||
import ScrollerModal from '@/components/scroller-modal/index';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import ContainerInstall from '@/pages/resources/components/container-install';
|
||||
import { CheckCircleFilled } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { ProviderValueMap } from '../config';
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
ClusterFormData as FormData,
|
||||
ClusterListItem as ListItem
|
||||
} from '../config/types';
|
||||
import CloudProvider from './cloud-provider-form';
|
||||
import RegisterClusterInner from './register-cluster-inner';
|
||||
import ClusterForm from './cluster-form';
|
||||
|
||||
type AddModalProps = {
|
||||
title: string;
|
||||
@@ -35,15 +28,10 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
onOk,
|
||||
onCancel
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const [submissionStatus, setSubmissionStatus] = React.useState<{
|
||||
success: boolean;
|
||||
data: ListItem;
|
||||
}>({ success: false, data: {} as ListItem });
|
||||
const form = useRef<any>(null);
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.submit();
|
||||
form.current?.submit();
|
||||
};
|
||||
|
||||
const handleOk = async (data: FormData) => {
|
||||
@@ -54,63 +42,13 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
form.current?.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const renderAddWorkerContent = () => {
|
||||
if (provider === ProviderValueMap.Custom) {
|
||||
return <ContainerInstall />;
|
||||
}
|
||||
if (provider === ProviderValueMap.Kubernetes) {
|
||||
return <RegisterClusterInner></RegisterClusterInner>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const modalTitle = useMemo(() => {
|
||||
if (submissionStatus.success) {
|
||||
return (
|
||||
<div className="flex-center">
|
||||
<CheckCircleFilled
|
||||
className="text-success font-size-20"
|
||||
style={{ marginRight: '8px' }}
|
||||
/>
|
||||
<span>Cluster added! Now you can register a worker.</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return title;
|
||||
}, [submissionStatus.success, title]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
form.setFieldsValue(currentData);
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
const renderFooter = () => {
|
||||
if (submissionStatus.success) {
|
||||
return (
|
||||
<ModalFooter
|
||||
onOk={onCancel}
|
||||
onCancel={onCancel}
|
||||
showCancelBtn={false}
|
||||
okText="Skip for Now"
|
||||
okBtnProps={{
|
||||
style: {
|
||||
width: 'auto'
|
||||
}
|
||||
}}
|
||||
></ModalFooter>
|
||||
);
|
||||
}
|
||||
return <ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>;
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollerModal
|
||||
title={modalTitle}
|
||||
title={title}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
destroyOnHidden={true}
|
||||
@@ -118,49 +56,18 @@ const AddCluster: React.FC<AddModalProps> = ({
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
width={680}
|
||||
footer={renderFooter()}
|
||||
footer={
|
||||
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
|
||||
}
|
||||
>
|
||||
{submissionStatus.success ? (
|
||||
renderAddWorkerContent()
|
||||
) : (
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleOk}
|
||||
preserve={false}
|
||||
initialValues={currentData}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{
|
||||
name: intl.formatMessage({ id: 'common.table.name' })
|
||||
}
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({ id: 'common.table.name' })}
|
||||
required
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
{provider === ProviderValueMap.DigitalOcean && (
|
||||
<CloudProvider
|
||||
provider={provider}
|
||||
credentialList={credentialList}
|
||||
></CloudProvider>
|
||||
)}
|
||||
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
|
||||
<SealInput.TextArea
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
></SealInput.TextArea>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
<ClusterForm
|
||||
ref={form}
|
||||
provider={provider}
|
||||
credentialList={credentialList}
|
||||
action={action}
|
||||
currentData={currentData}
|
||||
onFinish={handleOk}
|
||||
/>
|
||||
</ScrollerModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import RegisterClusterInner from './register-cluster-inner';
|
||||
import SupportedHardware from './support-hardware';
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
type AddModalProps = {
|
||||
registrationInfo: {
|
||||
token: string;
|
||||
image: string;
|
||||
server_url: string;
|
||||
cluster_id: number;
|
||||
};
|
||||
};
|
||||
const AddWorkerStep: React.FC<AddModalProps> = ({ registrationInfo }) => {
|
||||
return (
|
||||
<div>
|
||||
<Title>Supported Hardware Platforms</Title>
|
||||
<SupportedHardware />
|
||||
<Title style={{ marginTop: 32 }}>Execute Command</Title>
|
||||
<RegisterClusterInner registrationInfo={registrationInfo} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWorkerStep;
|
||||
@@ -2,6 +2,7 @@ import DropDownActions from '@/components/drop-down-actions';
|
||||
import ListMap from '@/components/dynamic-form/components/list-map';
|
||||
import { FieldSchema } from '@/components/dynamic-form/config/types';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Button, Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
@@ -36,7 +37,7 @@ const ButtonWrapper = styled.span`
|
||||
const CloudOptions: React.FC<{
|
||||
ref?: any;
|
||||
}> = forwardRef((props, ref) => {
|
||||
// form instance
|
||||
const intl = useIntl();
|
||||
const form = Form.useFormInstance();
|
||||
const [selectedOptions, setSelectedOptions] = React.useState<Set<string>>(
|
||||
new Set()
|
||||
@@ -102,7 +103,9 @@ const CloudOptions: React.FC<{
|
||||
<DropDownActions menu={menu}>
|
||||
<Button variant="filled" color="default">
|
||||
<PlusOutlined />
|
||||
<span>Add Cloud Options</span>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'clusters.workerpool.cloudOptions' })}
|
||||
</span>
|
||||
</Button>
|
||||
</DropDownActions>
|
||||
</Title>
|
||||
@@ -113,7 +116,7 @@ const CloudOptions: React.FC<{
|
||||
name={['cloud_options', field.name as string]}
|
||||
>
|
||||
<ListMap
|
||||
btnText={'Add Item'}
|
||||
btnText={intl.formatMessage({ id: 'common.button.addItem' })}
|
||||
label={field.title}
|
||||
dataList={form.getFieldValue(['cloud_options', field.name]) || []}
|
||||
properties={field.properties || {}}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import { ProviderType, ProviderValueMap } from '../config';
|
||||
import {
|
||||
ClusterFormData as FormData,
|
||||
ClusterListItem as ListItem
|
||||
} from '../config/types';
|
||||
import CloudProvider from './cloud-provider-form';
|
||||
|
||||
type AddModalProps = {
|
||||
action: PageActionType;
|
||||
currentData?: ListItem; // Used when action is EDIT
|
||||
provider: ProviderType;
|
||||
credentialList: Global.BaseOption<number>[];
|
||||
onFinish: (values: FormData) => void;
|
||||
ref?: any;
|
||||
};
|
||||
const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
||||
({ action, provider, currentData, credentialList, onFinish }, ref) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
form.setFieldsValue(currentData);
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetFields: () => {
|
||||
form.resetFields();
|
||||
},
|
||||
submit: () => {
|
||||
form.submit();
|
||||
},
|
||||
setFieldsValue: (values: any) => {
|
||||
form.setFieldsValue(values);
|
||||
},
|
||||
getFieldsValue: () => {
|
||||
return form.getFieldsValue();
|
||||
},
|
||||
validateFields: async () => {
|
||||
return await form.validateFields();
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form
|
||||
name="clusterForm"
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
preserve={false}
|
||||
scrollToFirstError={true}
|
||||
initialValues={currentData}
|
||||
>
|
||||
<Form.Item<FormData>
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage(
|
||||
{ id: 'common.form.rule.input' },
|
||||
{
|
||||
name: intl.formatMessage({ id: 'common.table.name' })
|
||||
}
|
||||
)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({ id: 'common.table.name' })}
|
||||
required
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
{provider === ProviderValueMap.DigitalOcean && (
|
||||
<CloudProvider
|
||||
provider={provider}
|
||||
credentialList={credentialList}
|
||||
></CloudProvider>
|
||||
)}
|
||||
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
|
||||
<SealInput.TextArea
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
></SealInput.TextArea>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ClusterForm;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Steps } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const { Step } = Steps;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding-block: 20px;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
.ant-steps-item-description {
|
||||
max-width: 300px !important;
|
||||
color: var(--ant-color-text-description) !important;
|
||||
}
|
||||
.ant-steps-item-content > .ant-steps-item-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
`;
|
||||
|
||||
const ClusterSteps: React.FC<{
|
||||
currentStep: number;
|
||||
onChange: (step: number) => void;
|
||||
steps: any[];
|
||||
}> = (props) => {
|
||||
const { steps, currentStep, onChange } = props;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Steps current={currentStep} size="small" onChange={onChange}>
|
||||
{steps.map((step) => (
|
||||
<Step
|
||||
disabled={step.disabled}
|
||||
key={step.title}
|
||||
title={step.title}
|
||||
description={step.content}
|
||||
/>
|
||||
))}
|
||||
</Steps>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClusterSteps;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Title = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
.text {
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
||||
interface FooterButtonsProps {
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
handleCancel: () => void;
|
||||
handleSubmit: () => void;
|
||||
showButtons: Record<string, boolean>;
|
||||
}
|
||||
|
||||
const FooterButtons: React.FC<FooterButtonsProps> = (props) => {
|
||||
const { onPrevious, onNext, handleCancel, handleSubmit, showButtons } = props;
|
||||
const intl = useIntl();
|
||||
const handleOnNext = () => {
|
||||
onNext();
|
||||
};
|
||||
return (
|
||||
<Title style={{ height: 64 }}>
|
||||
<div className="flex-center gap-16">
|
||||
{showButtons.previous && (
|
||||
<Button type="link" icon={<LeftOutlined />} onClick={onPrevious}>
|
||||
Previous
|
||||
</Button>
|
||||
)}
|
||||
{showButtons.next && (
|
||||
<Button type="link" onClick={handleOnNext}>
|
||||
Next
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
)}
|
||||
{showButtons.skip && (
|
||||
<Button type="primary" onClick={handleCancel}>
|
||||
Skip for now
|
||||
</Button>
|
||||
)}
|
||||
{showButtons.save && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSubmit}
|
||||
style={{ minWidth: 88 }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.save' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Title>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterButtons;
|
||||
@@ -13,21 +13,20 @@ import React, {
|
||||
useImperativeHandle,
|
||||
useRef
|
||||
} from 'react';
|
||||
import {
|
||||
NodePoolFormData as FormData,
|
||||
NodePoolListItem as ListItem
|
||||
} from '../config/types';
|
||||
import CloudOptions from './cloud-options';
|
||||
import { ProviderType } from '../config';
|
||||
import { NodePoolFormData as FormData } from '../config/types';
|
||||
import VolumesConfig from './volumes-config';
|
||||
|
||||
type AddModalProps = {
|
||||
ref: any;
|
||||
name?: string;
|
||||
action: PageActionType;
|
||||
provider: string; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||
currentData?: ListItem | null;
|
||||
provider: ProviderType; // 'kubernetes' | 'custom' | 'digitalocean';
|
||||
currentData?: FormData | null;
|
||||
onFinish: (values: FormData) => void;
|
||||
};
|
||||
const PoolForm: React.FC<AddModalProps> = forwardRef(
|
||||
({ action, onFinish, currentData }, ref) => {
|
||||
({ action, name = 'workerPoolForm', onFinish, currentData }, ref) => {
|
||||
const cloudOptionsRef = useRef<any>(null);
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
@@ -35,21 +34,26 @@ const PoolForm: React.FC<AddModalProps> = forwardRef(
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
console.log('currentData===========1=', currentData);
|
||||
form.setFieldsValue({
|
||||
...currentData,
|
||||
volumes: currentData.cloud_options?.volumes?.[0] || {}
|
||||
...currentData
|
||||
});
|
||||
cloudOptionsRef.current?.initFieldList();
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
resetFields: () => {
|
||||
form.resetFields();
|
||||
},
|
||||
submit: () => {
|
||||
form.submit();
|
||||
},
|
||||
setFieldsValue: (values: any) => {
|
||||
form.setFieldsValue(values);
|
||||
},
|
||||
getFieldsValue: () => {
|
||||
return form.getFieldsValue();
|
||||
},
|
||||
validateFields: async () => {
|
||||
return await form.validateFields();
|
||||
}
|
||||
@@ -57,9 +61,11 @@ const PoolForm: React.FC<AddModalProps> = forwardRef(
|
||||
|
||||
return (
|
||||
<Form
|
||||
name={name}
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
preserve={false}
|
||||
scrollToFirstError={true}
|
||||
initialValues={{
|
||||
replicas: 1,
|
||||
batch_size: 1
|
||||
@@ -158,7 +164,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef(
|
||||
btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
|
||||
></LabelSelector>
|
||||
</Form.Item>
|
||||
<CloudOptions ref={cloudOptionsRef}></CloudOptions>
|
||||
<VolumesConfig ref={cloudOptionsRef}></VolumesConfig>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import Card from '@/components/templates/card';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProviderType } from '../config';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 22px;
|
||||
`;
|
||||
interface ProviderCatalogProps {
|
||||
onSelect?: (provider: ProviderType) => void;
|
||||
currentProvider?: ProviderType;
|
||||
clickable?: boolean;
|
||||
dataList: {
|
||||
label: string;
|
||||
key: string;
|
||||
locale?: boolean;
|
||||
disabled?: boolean;
|
||||
icon: React.ReactNode;
|
||||
description?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
|
||||
onSelect,
|
||||
dataList,
|
||||
clickable,
|
||||
currentProvider
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Wrapper>
|
||||
{dataList?.map((action) => (
|
||||
<Card
|
||||
height="auto"
|
||||
key={action.key}
|
||||
onClick={() => onSelect?.(action.key as ProviderType)}
|
||||
active={currentProvider === action.key}
|
||||
disabled={action.disabled}
|
||||
clickable={clickable}
|
||||
header={
|
||||
action.locale
|
||||
? intl.formatMessage({ id: action.label })
|
||||
: action.label
|
||||
}
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.description || 'This is a description'}
|
||||
</Card>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProviderCatalog;
|
||||
@@ -0,0 +1,94 @@
|
||||
import ascendLogo from '@/assets/logo/ascend.png';
|
||||
import CambriconPNG from '@/assets/logo/cambricon.png';
|
||||
import hyponPNG from '@/assets/logo/hygon.png';
|
||||
import iluvatarWEBP from '@/assets/logo/Iluvatar.png';
|
||||
import metaxLogo from '@/assets/logo/metax.png';
|
||||
import moorePNG from '@/assets/logo/moore _threads.png';
|
||||
import nvidiaLogo from '@/assets/logo/nvidia.png';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import ProviderCatalog from './provider-catalog';
|
||||
|
||||
const ProviderImage = ({ src, showBg }: { src: string; showBg?: boolean }) => {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
style={{
|
||||
width: 46,
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
width={46}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const supportedHardPlatforms = [
|
||||
{
|
||||
label: 'NVIDIA CUDA',
|
||||
value: 'cuda',
|
||||
key: 'nvidia',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={nvidiaLogo} showBg />
|
||||
},
|
||||
{
|
||||
label: 'AMD ROCm',
|
||||
value: 'rocm',
|
||||
key: 'amd',
|
||||
locale: false,
|
||||
icon: (
|
||||
<IconFont
|
||||
type="icon-amd"
|
||||
style={{ fontSize: 46, color: 'var(--ant-color-text)' }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Ascend CANN',
|
||||
value: 'npu',
|
||||
key: 'ascend',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={ascendLogo} showBg />
|
||||
},
|
||||
{
|
||||
label: 'Hygon DTK',
|
||||
value: 'dcu',
|
||||
key: 'hygon',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={hyponPNG} />
|
||||
},
|
||||
{
|
||||
label: 'Moore Threads MUSA',
|
||||
value: 'musa',
|
||||
key: 'musa',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={moorePNG} />
|
||||
},
|
||||
{
|
||||
label: 'Iluvatar Corex',
|
||||
value: 'corex',
|
||||
key: 'corex',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={iluvatarWEBP} showBg />
|
||||
},
|
||||
{
|
||||
label: 'Cambricon',
|
||||
value: 'cambricon',
|
||||
key: 'cambricon',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={CambriconPNG} />
|
||||
},
|
||||
{
|
||||
label: 'Metax',
|
||||
value: 'metax',
|
||||
key: 'metax',
|
||||
locale: false,
|
||||
icon: <ProviderImage src={metaxLogo} showBg />
|
||||
}
|
||||
];
|
||||
|
||||
const SupportedHardware = () => {
|
||||
return (
|
||||
<ProviderCatalog dataList={supportedHardPlatforms} clickable={false} />
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportedHardware;
|
||||
@@ -0,0 +1,34 @@
|
||||
import ListMap from '@/components/dynamic-form/components/list-map';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { fieldConfig } from '../config/cloud-options-config';
|
||||
|
||||
const volumeOptions = fieldConfig.volumes;
|
||||
|
||||
const CloudOptions: React.FC<{
|
||||
ref?: any;
|
||||
}> = forwardRef((props, ref) => {
|
||||
const intl = useIntl();
|
||||
const form = Form.useFormInstance();
|
||||
const volumes = Form.useWatch(['cloud_options', volumeOptions.name], form);
|
||||
|
||||
const handleOnChange = (name: string, value: any) => {
|
||||
console.log('handleOnChange========', name, value);
|
||||
form.setFieldValue(['cloud_options', name], value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item name={['cloud_options', volumeOptions.name as string]}>
|
||||
<ListMap
|
||||
btnText={intl.formatMessage({ id: 'clusters.workerpool.volumes.add' })}
|
||||
label={intl.formatMessage({ id: 'clusters.workerpool.volumes' })}
|
||||
dataList={volumes || []}
|
||||
properties={volumeOptions.properties || {}}
|
||||
onChange={(value) => handleOnChange(volumeOptions.name, value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
});
|
||||
|
||||
export default CloudOptions;
|
||||
@@ -41,7 +41,7 @@ export const fieldConfig: Record<string, FieldSchema> = {
|
||||
type: 'string',
|
||||
title: 'Format',
|
||||
widget: 'Select',
|
||||
enum: ['ext4', 'xfs', 'btrfs'],
|
||||
enum: ['ext4', 'xfs'],
|
||||
style: { width: 150 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import icons from '@/components/icon-font/icons';
|
||||
import { StatusMaps } from '@/config';
|
||||
import { StatusType } from '@/config/types';
|
||||
import React from 'react';
|
||||
|
||||
export const ClusterStatusValueMap = {
|
||||
Provisioning: 0,
|
||||
@@ -23,12 +24,17 @@ export const ClusterStatus: Record<string, StatusType> = {
|
||||
export const ProviderValueMap = {
|
||||
Kubernetes: 'Kubernetes',
|
||||
DigitalOcean: 'DigitalOcean',
|
||||
Custom: 'Custom'
|
||||
Custom: 'Custom',
|
||||
HuaweiCloud: 'HuaweiCloud',
|
||||
AliCloud: 'AliCloud',
|
||||
TencentCloud: 'TencentCloud'
|
||||
};
|
||||
|
||||
export type ProviderType = keyof typeof ProviderValueMap | null | undefined;
|
||||
|
||||
export const ProviderLabelMap = {
|
||||
[ProviderValueMap.Kubernetes]: 'Kubernetes',
|
||||
[ProviderValueMap.DigitalOcean]: 'Digital Ocean',
|
||||
[ProviderValueMap.DigitalOcean]: 'DigitalOcean',
|
||||
[ProviderValueMap.Custom]: 'Custom'
|
||||
};
|
||||
|
||||
@@ -41,27 +47,62 @@ export const generateRegisterCommand = (params: {
|
||||
--header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`;
|
||||
};
|
||||
|
||||
export const addActions = [
|
||||
export const providerList = [
|
||||
{
|
||||
label: 'clusters.provider.custom',
|
||||
locale: true,
|
||||
value: ProviderValueMap.Custom,
|
||||
key: ProviderValueMap.Custom,
|
||||
icon: icons.Docker
|
||||
icon: React.cloneElement(icons.Docker, {
|
||||
style: { color: 'var(--ant-color-primary)' }
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Kubernetes',
|
||||
locale: false,
|
||||
value: ProviderValueMap.Kubernetes,
|
||||
key: ProviderValueMap.Kubernetes,
|
||||
icon: icons.KubernetesOutlined
|
||||
icon: React.cloneElement(icons.KubernetesOutlined, {
|
||||
style: { color: 'var(--ant-color-primary)' }
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Digital Ocean',
|
||||
label: 'DigitalOcean',
|
||||
locale: false,
|
||||
value: ProviderValueMap.DigitalOcean,
|
||||
key: ProviderValueMap.DigitalOcean,
|
||||
icon: icons.DigitalOcean
|
||||
icon: React.cloneElement(icons.DigitalOcean, {
|
||||
style: {
|
||||
color: 'var(--ant-color-primary)'
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Huawei Cloud',
|
||||
locale: false,
|
||||
disabled: true,
|
||||
value: ProviderValueMap.HuaweiCloud,
|
||||
key: ProviderValueMap.HuaweiCloud,
|
||||
icon: icons.HuaweiCloud,
|
||||
description: 'Currently Not Supported'
|
||||
},
|
||||
{
|
||||
label: 'Ali Cloud',
|
||||
locale: false,
|
||||
disabled: true,
|
||||
value: ProviderValueMap.AliCloud,
|
||||
key: ProviderValueMap.AliCloud,
|
||||
icon: icons.AliCloud,
|
||||
description: 'Currently Not Supported'
|
||||
},
|
||||
{
|
||||
label: 'Tencent Cloud',
|
||||
locale: false,
|
||||
disabled: true,
|
||||
value: ProviderValueMap.TencentCloud,
|
||||
key: ProviderValueMap.TencentCloud,
|
||||
icon: icons.TencentCloud,
|
||||
description: 'Currently Not Supported'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -101,13 +142,13 @@ export const clusterActionList = [
|
||||
locale: true,
|
||||
icon: icons.KubernetesOutlined
|
||||
},
|
||||
{
|
||||
key: 'addPool',
|
||||
label: 'clusters.button.addNodePool',
|
||||
provider: ProviderValueMap.DigitalOcean,
|
||||
locale: true,
|
||||
icon: icons.Catalog
|
||||
},
|
||||
// {
|
||||
// key: 'addPool',
|
||||
// label: 'clusters.button.addNodePool',
|
||||
// provider: ProviderValueMap.DigitalOcean,
|
||||
// locale: true,
|
||||
// icon: icons.Catalog
|
||||
// },
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'common.button.delete',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ProviderType } from '.';
|
||||
|
||||
export interface CredentialFormData {
|
||||
name: string;
|
||||
provider: string;
|
||||
@@ -20,15 +22,6 @@ export interface CredentialListItem {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ClusterFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
provider: string;
|
||||
credential_id: number;
|
||||
zone: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
export interface NodePoolListItem {
|
||||
id: number;
|
||||
instance_type: string;
|
||||
@@ -47,7 +40,6 @@ export interface NodePoolFormData {
|
||||
batch_size: number;
|
||||
labels: Record<string, string>;
|
||||
cloud_options: Record<string, any>;
|
||||
volumes?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ClusterListItem {
|
||||
@@ -67,3 +59,13 @@ export interface ClusterListItem {
|
||||
state_message: string;
|
||||
worker_pools: NodePoolListItem[];
|
||||
}
|
||||
|
||||
export interface ClusterFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
provider: ProviderType;
|
||||
credential_id: number;
|
||||
zone: string;
|
||||
region: string;
|
||||
worker_pools?: NodePoolFormData[];
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const useClusterColumns = (
|
||||
render: (text: string, record: ClusterListItem) => (
|
||||
<AutoTooltip ghost>
|
||||
<Link
|
||||
to={`/cluster-management/cluster/detail?id=${record.id}&provider=${record.provider}&name=${record.name}`}
|
||||
to={`/cluster-management/clusters/detail?id=${record.id}&provider=${record.provider}&name=${record.name}`}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import PageTools from '@/components/page-tools';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ClusterForm from '../components/cluster-form';
|
||||
import { ProviderType } from '../config';
|
||||
|
||||
const Title = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
.text {
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
||||
interface BasicFormProps {
|
||||
provider: ProviderType;
|
||||
action: PageActionType;
|
||||
credentialList: Global.BaseOption<number>[];
|
||||
currentData?: any;
|
||||
}
|
||||
|
||||
const BasicForm = forwardRef((props: BasicFormProps, ref) => {
|
||||
const { provider, credentialList, action, currentData } = props;
|
||||
const formRef = useRef<any>(null);
|
||||
|
||||
const handleOnFinish = (values: any) => {
|
||||
console.log(values);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
validateFields: formRef.current?.validateFields,
|
||||
getFieldsValue: formRef.current?.getFieldsValue,
|
||||
submit: formRef.current?.submit
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTools
|
||||
marginBottom={26}
|
||||
left={<Title>Basic Configuration</Title>}
|
||||
marginTop={0}
|
||||
></PageTools>
|
||||
<ClusterForm
|
||||
provider={provider}
|
||||
action={action}
|
||||
ref={formRef}
|
||||
credentialList={credentialList}
|
||||
onFinish={handleOnFinish}
|
||||
currentData={currentData}
|
||||
></ClusterForm>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default BasicForm;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import AddWorkerStep from '../components/add-worker-step';
|
||||
import ProviderCatalog from '../components/provider-catalog';
|
||||
import BasicForm from './basic-form';
|
||||
import WorkerPoolForm from './worker-pools-form';
|
||||
|
||||
export const moduleMap = {
|
||||
BasicForm: 'BasicForm',
|
||||
WorkerPoolForm: 'WorkerPoolForm',
|
||||
ProviderCatalog: 'ProviderCatalog',
|
||||
AddWorkerStep: 'AddWorkerStep'
|
||||
};
|
||||
|
||||
export const moduleRegistry: Record<string, React.ComponentType<any>> = {
|
||||
[moduleMap.BasicForm]: BasicForm,
|
||||
[moduleMap.WorkerPoolForm]: WorkerPoolForm,
|
||||
[moduleMap.ProviderCatalog]: ProviderCatalog,
|
||||
[moduleMap.AddWorkerStep]: AddWorkerStep
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useNavigate } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useMemo } from 'react';
|
||||
import { ProviderType, ProviderValueMap } from '../config';
|
||||
import { moduleMap } from './module-registry';
|
||||
|
||||
export default function useStepList() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleBack = useMemoizedFn(() => {
|
||||
navigate(-1);
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Select Cloud Provider',
|
||||
content: 'Choose the cloud provider for your cluster.',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
previous: false,
|
||||
next: true,
|
||||
save: false,
|
||||
skip: false
|
||||
};
|
||||
},
|
||||
defaultShow: true,
|
||||
showForms: [],
|
||||
showModules: [moduleMap.ProviderCatalog],
|
||||
providers: []
|
||||
},
|
||||
{
|
||||
title: 'Configure Cluster Settings',
|
||||
content: 'Set up the basic configuration for your cluster.',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
previous: true,
|
||||
next: provider === ProviderValueMap.DigitalOcean,
|
||||
save: provider !== ProviderValueMap.DigitalOcean,
|
||||
skip: false
|
||||
};
|
||||
},
|
||||
defaultShow: true,
|
||||
showForms: [moduleMap.BasicForm],
|
||||
showModules: [],
|
||||
providers: []
|
||||
},
|
||||
{
|
||||
title: 'Add Worker Pools',
|
||||
content: 'Define the worker pools for your cluster.',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
previous: true,
|
||||
next: false,
|
||||
save: true,
|
||||
skip: false
|
||||
};
|
||||
},
|
||||
defaultShow: false,
|
||||
showForms: [moduleMap.WorkerPoolForm],
|
||||
showModules: [],
|
||||
providers: [ProviderValueMap.DigitalOcean],
|
||||
beforeNext: handleBack
|
||||
},
|
||||
{
|
||||
title: 'Add Worker',
|
||||
content: 'Add a worker node to your cluster.',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
previous: false,
|
||||
next: false,
|
||||
save: false,
|
||||
skip: true
|
||||
};
|
||||
},
|
||||
defaultShow: false,
|
||||
showForms: [],
|
||||
showModules: [moduleMap.AddWorkerStep],
|
||||
providers: [ProviderValueMap.Custom]
|
||||
},
|
||||
{
|
||||
title: 'Register Cluster',
|
||||
content: 'Register your cluster with the chosen provider.',
|
||||
showButtons: (provider?: ProviderType) => {
|
||||
return {
|
||||
previous: false,
|
||||
next: false,
|
||||
save: false,
|
||||
skip: true
|
||||
};
|
||||
},
|
||||
defaultShow: false,
|
||||
showForms: [],
|
||||
showModules: [moduleMap.AddWorkerStep],
|
||||
providers: [ProviderValueMap.Kubernetes]
|
||||
}
|
||||
],
|
||||
[handleBack]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import PageTools from '@/components/page-tools';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, FormInstance } from 'antd';
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import WorkerPoolForm from '../components/pool-form';
|
||||
import { ProviderType } from '../config';
|
||||
import { NodePoolFormData } from '../config/types';
|
||||
|
||||
const PoolContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const PoolFormWrapper = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
.text {
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
||||
interface WorkerPoolsFormProps {
|
||||
provider: ProviderType;
|
||||
action: PageActionType;
|
||||
currentData?: any;
|
||||
}
|
||||
|
||||
const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => {
|
||||
const { provider, action, currentData } = props;
|
||||
const countRef = useRef(1);
|
||||
const formRefs = useRef<Record<number, FormInstance<any> | null>>({});
|
||||
const [workerPoolList, setWorkerPoolList] = useState<
|
||||
Map<number, NodePoolFormData>
|
||||
>(
|
||||
new Map([
|
||||
[
|
||||
1,
|
||||
{
|
||||
instance_type: 'Pool-1'
|
||||
}
|
||||
]
|
||||
]) as Map<number, NodePoolFormData>
|
||||
);
|
||||
|
||||
const handleOnFinish = (values: any) => {};
|
||||
|
||||
const updateCount = () => {
|
||||
countRef.current += 1;
|
||||
return countRef.current;
|
||||
};
|
||||
|
||||
const handleAddPool = () => {
|
||||
const newId = updateCount();
|
||||
setWorkerPoolList((prev) =>
|
||||
new Map(prev).set(newId, {
|
||||
instance_type: `Pool-${newId}`
|
||||
} as NodePoolFormData)
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemovePool = (id: number) => {
|
||||
if (workerPoolList.size === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkerPoolList((prev) => {
|
||||
const newList = new Map(prev);
|
||||
newList.delete(id);
|
||||
return newList;
|
||||
});
|
||||
formRefs.current[id] = null;
|
||||
};
|
||||
|
||||
const gatherFormValues = (results: PromiseSettledResult<any>[]) => {
|
||||
const resultList = results.map((result: PromiseSettledResult<any>) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return result.value;
|
||||
}
|
||||
if (result.status === 'rejected') {
|
||||
return result.reason?.values || {};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
console.log('gatherFormValues========', resultList);
|
||||
return resultList;
|
||||
};
|
||||
|
||||
const validateFields = async () => {
|
||||
const promises = Array.from(Object.values(formRefs.current)).map((form) =>
|
||||
form?.validateFields()
|
||||
);
|
||||
const results = await Promise.allSettled(promises);
|
||||
console.log('results========0', promises, results);
|
||||
if (results.some((result) => result.status === 'rejected')) {
|
||||
return Promise.reject({
|
||||
values: {
|
||||
worker_pools: gatherFormValues(results)
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
worker_pools: gatherFormValues(results)
|
||||
});
|
||||
};
|
||||
|
||||
const setFieldsValue = (data: any) => {
|
||||
const worker_pools = data.worker_pools || [];
|
||||
const newWorkerPools = worker_pools.map(
|
||||
(poolData: NodePoolFormData, index: number) => [index, poolData]
|
||||
);
|
||||
console.log('newWorkerPools========', newWorkerPools);
|
||||
setWorkerPoolList(new Map(newWorkerPools));
|
||||
};
|
||||
|
||||
const getFieldsValue = () => {
|
||||
const values = Object.values(formRefs.current).map((form) =>
|
||||
form?.getFieldsValue()
|
||||
);
|
||||
console.log('getFieldsValue========', values);
|
||||
return {
|
||||
worker_pools: values
|
||||
};
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submit: () => {},
|
||||
validateFields: validateFields,
|
||||
getFieldsValue: getFieldsValue
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (currentData) {
|
||||
console.log('currentData===========1=', currentData);
|
||||
setFieldsValue(currentData);
|
||||
}
|
||||
}, [currentData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTools
|
||||
marginBottom={22}
|
||||
marginTop={0}
|
||||
left={
|
||||
<span className="flex-center gap-16">
|
||||
<Title>Worker Pools Configuration</Title>
|
||||
</span>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
onClick={handleAddPool}
|
||||
variant="filled"
|
||||
color="default"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Add Pool
|
||||
</Button>
|
||||
}
|
||||
></PageTools>
|
||||
{Array.from(workerPoolList.keys()).map((key, index) => (
|
||||
<div key={key}>
|
||||
<PoolContainer>
|
||||
<PoolFormWrapper>
|
||||
<WorkerPoolForm
|
||||
name={`workerPoolForm_${key}`}
|
||||
action={action}
|
||||
ref={(el: any) => {
|
||||
if (el) {
|
||||
formRefs.current[key] = el;
|
||||
}
|
||||
}}
|
||||
onFinish={handleOnFinish}
|
||||
provider={provider}
|
||||
currentData={workerPoolList.get(key)}
|
||||
></WorkerPoolForm>
|
||||
</PoolFormWrapper>
|
||||
</PoolContainer>
|
||||
{index !== Array.from(workerPoolList.keys()).length - 1 && (
|
||||
<Divider orientation="right">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="default"
|
||||
onClick={() => handleRemovePool(key)}
|
||||
icon={<DeleteOutlined />}
|
||||
></Button>
|
||||
</Divider>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex-end">
|
||||
<Button
|
||||
onClick={handleAddPool}
|
||||
variant="filled"
|
||||
color="default"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Add Pool
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkerPoolsForm;
|
||||
@@ -21,24 +21,6 @@ const ModelItemContent = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 8px;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-middle);
|
||||
font-weight: 500;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -83,6 +65,25 @@ const ModelItemContent = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-middle);
|
||||
font-weight: 600;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
`;
|
||||
|
||||
const sourceIconMap = {
|
||||
[modelSourceMap.local_path_value]: 'icon-hard-disk',
|
||||
[modelSourceMap.huggingface_value]: 'icon-huggingface',
|
||||
@@ -96,9 +97,12 @@ const ModelItem: React.FC<{
|
||||
const { model, onClick } = props;
|
||||
|
||||
return (
|
||||
<Card onClick={() => onClick(model)} clickable={false} ghost>
|
||||
<ModelItemContent>
|
||||
<div className="title">
|
||||
<Card
|
||||
onClick={() => onClick(model)}
|
||||
clickable={false}
|
||||
ghost
|
||||
header={
|
||||
<Header>
|
||||
<span className="text">
|
||||
<IconFont
|
||||
type={sourceIconMap[model.source]}
|
||||
@@ -114,7 +118,10 @@ const ModelItem: React.FC<{
|
||||
message: model.state_message
|
||||
}}
|
||||
></StatusTag>
|
||||
</div>
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
<ModelItemContent>
|
||||
<div className="content">
|
||||
<div className="extra-info">
|
||||
<ThemeTag className="tag-item" color="blue">
|
||||
|
||||