refactor: create cluster by steps

This commit is contained in:
jialin
2025-09-16 11:26:17 +08:00
parent 6e5a7035c8
commit 8de272239e
48 changed files with 1392 additions and 227 deletions
+11 -1
View File
@@ -168,7 +168,7 @@ export default [
}, },
{ {
name: 'clusterDetail', name: 'clusterDetail',
path: '/cluster-management/cluster/detail', path: '/cluster-management/clusters/detail',
key: 'clusterDetail', key: 'clusterDetail',
icon: 'icon-cluster2-outline', icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled', selectedIcon: 'icon-cluster2-filled',
@@ -176,6 +176,16 @@ export default [
hideInMenu: true, hideInMenu: true,
component: './cluster-management/cluster-detail' 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', name: 'credentials',
path: '/cluster-management/credentials', path: '/cluster-management/credentials',
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -5,20 +5,6 @@ import React, { useEffect, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import FormWidget from './form-widget'; 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` const RowWrapper = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@@ -32,6 +18,20 @@ const WidgetBox = styled.div`
gap: 8px; gap: 8px;
width: 100%; 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 ListItem: React.FC<ListItemProps> = ({ schemaList, data, onChange }) => {
const handleValueChange = (name: string, target: any) => { const handleValueChange = (name: string, target: any) => {
@@ -65,6 +65,7 @@ const ListMap: React.FC<ListMapProps> = ({
label, label,
btnText, btnText,
properties = {}, properties = {},
minItems = 0,
onChange onChange
}) => { }) => {
const [items, setItems] = React.useState(dataList || []); const [items, setItems] = React.useState(dataList || []);
@@ -99,11 +100,15 @@ const ListMap: React.FC<ListMapProps> = ({
}; };
useEffect(() => { useEffect(() => {
if (!dataList.length) { if (!dataList.length && minItems > 0) {
handleOnAdd(); handleOnAdd();
} }
}, []); }, []);
useEffect(() => {
setItems(dataList);
}, [dataList]);
return ( return (
<Wrapper label={label} btnText={btnText} onAdd={handleOnAdd}> <Wrapper label={label} btnText={btnText} onAdd={handleOnAdd}>
{items.map((item, index) => ( {items.map((item, index) => (
+7 -1
View File
@@ -35,7 +35,13 @@ const icons = {
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }), Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }),
Docker: React.createElement(IconFont, { type: 'icon-docker' }), Docker: React.createElement(IconFont, { type: 'icon-docker' }),
DigitalOcean: React.createElement(IconFont, { type: 'icon-digitalocean' }), 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; export default icons;
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js'; // import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({ 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; export default IconFont;
+1
View File
@@ -9,6 +9,7 @@
width: max-content; width: max-content;
height: 24px; height: 24px;
font-size: var(--font-size-small); font-size: var(--font-size-small);
font-weight: 400;
overflow: hidden; overflow: hidden;
.txt { .txt {
+51 -8
View File
@@ -10,20 +10,23 @@ interface CardProps {
ghost?: boolean; ghost?: boolean;
header?: React.ReactNode; header?: React.ReactNode;
footer?: React.ReactNode; footer?: React.ReactNode;
icon?: React.ReactNode;
active?: boolean;
disabled?: boolean;
onClick?: () => void; onClick?: () => void;
} }
const CardWrapper = styled.div` const CardWrapper = styled.div`
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; padding: 22px;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: center;
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
cursor: default; cursor: default;
width: 100%; width: 100%;
&:hover { &.clickable:hover:not(.disabled) {
background-color: var(--ant-color-fill-tertiary); background-color: var(--ant-color-fill-tertiary);
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
@@ -35,15 +38,47 @@ const CardWrapper = styled.div`
&.active { &.active {
background-color: var(--ant-color-fill-tertiary); background-color: var(--ant-color-fill-tertiary);
} }
&.clickable {
&.clickable:not(.disabled) {
cursor: pointer; cursor: pointer;
} }
&.disabled {
cursor: not-allowed;
opacity: 0.6;
pointer-events: none;
background-color: var(--ant-color-fill-quaternary);
}
`; `;
const CardContent = styled.div` const CardContent = styled.div`
padding: 16px 20px;
width: 100%; width: 100%;
flex: 1; 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) => { const Card: React.FC<CardProps> = (props) => {
@@ -55,6 +90,9 @@ const Card: React.FC<CardProps> = (props) => {
ghost = false, ghost = false,
header, header,
footer, footer,
icon,
active,
disabled,
onClick onClick
} = props; } = props;
@@ -62,14 +100,19 @@ const Card: React.FC<CardProps> = (props) => {
<CardWrapper <CardWrapper
className={classNames(className, { className={classNames(className, {
clickable: clickable, clickable: clickable,
active: active,
disabled: disabled,
ghost: ghost ghost: ghost
})} })}
style={{ height: height || '180px' }} style={{ height: height || '180px' }}
onClick={onClick} onClick={onClick}
> >
{header} {icon && <Icon>{icon}</Icon>}
<CardContent>{children}</CardContent> <Inner>
{footer} {header && <Header>{header}</Header>}
<CardContent>{children}</CardContent>
{footer}
</Inner>
</CardWrapper> </CardWrapper>
); );
}; };
+11 -2
View File
@@ -63,8 +63,9 @@ html {
--ant-color-success: #54cc98; --ant-color-success: #54cc98;
--ant-color-error-bg: #fff2f0; --ant-color-error-bg: #fff2f0;
--ant-color-error: #ff4d4f; --ant-color-error: #ff4d4f;
--ant-box-shadow-secondary: 0 6px 16px 0 rgba(0, 0, 0, 8%), --ant-box-shadow-secondary:
0 3px 6px -4px rgba(0, 0, 0, 12%), 0 9px 28px 8px rgba(0, 0, 0, 5%); 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-red: rgba(255, 77, 79, 80%);
--color-chart-green: rgb(84, 204, 152, 80%); --color-chart-green: rgb(84, 204, 152, 80%);
--color-chart-glod: rgba(250, 173, 20, 80%); --color-chart-glod: rgba(250, 173, 20, 80%);
@@ -823,3 +824,11 @@ body {
.ant-progress-line { .ant-progress-line {
font-size: var(--font-size-small); 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;
}
+3 -1
View File
@@ -23,5 +23,7 @@ export default {
'clusters.workerpool.volumes': 'Volumes', 'clusters.workerpool.volumes': 'Volumes',
'clusters.workerpool.format': 'Format', 'clusters.workerpool.format': 'Format',
'clusters.workerpool.size': 'Size (GiB)', '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'
}; };
+2 -1
View File
@@ -259,5 +259,6 @@ export default {
'common.sso.noConfig': 'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.', 'Single sign-on is not enabled on this system. Please contact your administrator.',
'common.button.edit.item': 'Edit {name}', 'common.button.edit.item': 'Edit {name}',
'common.button.terminal': 'Terminal' 'common.button.terminal': 'Terminal',
'common.button.addItem': 'Add Item'
}; };
+2 -1
View File
@@ -30,5 +30,6 @@ export default {
'menu.clusterManagement': 'Cluster Management', 'menu.clusterManagement': 'Cluster Management',
'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.clusters': 'Clusters',
'menu.clusterManagement.credentials': 'Credentials', 'menu.clusterManagement.credentials': 'Credentials',
'menu.clusterManagement.clusterDetail': 'Cluster Detail' 'menu.clusterManagement.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterCreate': 'Create Cluster'
}; };
+7 -2
View File
@@ -23,7 +23,9 @@ export default {
'clusters.workerpool.volumes': 'Volumes', 'clusters.workerpool.volumes': 'Volumes',
'clusters.workerpool.format': 'Format', 'clusters.workerpool.format': 'Format',
'clusters.workerpool.size': 'Size (GiB)', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -51,5 +53,8 @@ export default {
// 22. 'clusters.workerpool.format': 'Format', // 22. 'clusters.workerpool.format': 'Format',
// 23. 'clusters.workerpool.size': 'Size (GiB)', // 23. 'clusters.workerpool.size': 'Size (GiB)',
// 24. 'clusters.credential.title': 'Credential', // 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 ========== // ========== End of To-Do List ==========
+4 -2
View File
@@ -259,7 +259,8 @@ export default {
'common.sso.noConfig': 'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.', 'Single sign-on is not enabled on this system. Please contact your administrator.',
'common.button.edit.item': 'Edit {name}', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -284,5 +285,6 @@ export default {
// 19. 'common.login.password': 'Log in with Password', // 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.' // 20. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
// 21. 'common.button.edit.item': 'Edit {name}', // 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 ========== // ========== End of To-Do List ==========
+4 -2
View File
@@ -30,7 +30,8 @@ export default {
'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.clusters': 'Clusters',
'menu.clusterManagement.credentials': 'Credentials', 'menu.clusterManagement.credentials': 'Credentials',
'menu.models.userModels': 'My Models', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -45,5 +46,6 @@ export default {
// 9. 'menu.clusterManagement.clusters': 'Clusters', // 9. 'menu.clusterManagement.clusters': 'Clusters',
// 10. 'menu.clusterManagement.credentials': 'Credentials', // 10. 'menu.clusterManagement.credentials': 'Credentials',
// 11. 'menu.models.userModels': 'My Models' // 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 ========== // ========== End of To-Do List ==========
+6 -2
View File
@@ -23,7 +23,9 @@ export default {
'clusters.workerpool.volumes': 'Volumes', 'clusters.workerpool.volumes': 'Volumes',
'clusters.workerpool.format': 'Format', 'clusters.workerpool.format': 'Format',
'clusters.workerpool.size': 'Size (GiB)', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -51,5 +53,7 @@ export default {
// 22. 'clusters.workerpool.volumes': 'Volumes', // 22. 'clusters.workerpool.volumes': 'Volumes',
// 23. 'clusters.workerpool.format': 'Format', // 23. 'clusters.workerpool.format': 'Format',
// 24. 'clusters.workerpool.size': 'Size (GiB)', // 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 ========== // ========== End of To-Do List ==========
+4 -2
View File
@@ -258,7 +258,8 @@ export default {
'common.sso.noConfig': 'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.', 'Single sign-on is not enabled on this system. Please contact your administrator.',
'common.button.edit.item': 'Edit {name}', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -266,5 +267,6 @@ export default {
// 2. 'common.login.password': 'Log in with Password' // 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.' // 3. 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.'
// 4. 'common.button.edit.item': 'Edit {name}', // 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 ========== // ========== End of To-Do List ==========
+4 -2
View File
@@ -30,7 +30,8 @@ export default {
'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.clusters': 'Clusters',
'menu.clusterManagement.credentials': 'Credentials', 'menu.clusterManagement.credentials': 'Credentials',
'menu.models.userModels': 'My Models', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -45,5 +46,6 @@ export default {
// 9. 'menu.clusterManagement.clusters': 'Clusters', // 9. 'menu.clusterManagement.clusters': 'Clusters',
// 10. 'menu.clusterManagement.credentials': 'Credentials', // 10. 'menu.clusterManagement.credentials': 'Credentials',
// 11. 'menu.models.userModels': 'My Models', // 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 ========== // ========== End of To-Do List ==========
+3 -1
View File
@@ -23,5 +23,7 @@ export default {
'clusters.workerpool.volumes': '存储卷', 'clusters.workerpool.volumes': '存储卷',
'clusters.workerpool.format': '文件系统格式', 'clusters.workerpool.format': '文件系统格式',
'clusters.workerpool.size': '容量(GiB', 'clusters.workerpool.size': '容量(GiB',
'clusters.workerpool.title': '节点池' 'clusters.workerpool.title': '节点池',
'clusters.workerpool.cloudOptions': '添加云配置',
'clusters.workerpool.volumes.add': '添加存储卷'
}; };
+2 -1
View File
@@ -251,5 +251,6 @@ export default {
'common.external.login': '使用 {type} 登录', 'common.external.login': '使用 {type} 登录',
'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。', 'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。',
'common.button.edit.item': '编辑 {name}', 'common.button.edit.item': '编辑 {name}',
'common.button.terminal': '终端' 'common.button.terminal': '终端',
'common.button.addItem': '添加项'
}; };
+2 -1
View File
@@ -30,5 +30,6 @@ export default {
'menu.clusterManagement': '集群管理', 'menu.clusterManagement': '集群管理',
'menu.clusterManagement.clusters': '集群', 'menu.clusterManagement.clusters': '集群',
'menu.clusterManagement.credentials': '凭证', '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 { PageContainer } from '@ant-design/pro-components';
import { useSearchParams } from '@umijs/max'; import { useSearchParams } from '@umijs/max';
import ClusterMetrics from './components/cluster-metrics'; import ClusterMetrics from './components/cluster-metrics';
import WorkerPools from './components/worker-pools';
import { ProviderValueMap } from './config';
const ClusterDetailModal = () => { const ClusterDetailModal = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -25,7 +23,7 @@ const ClusterDetailModal = () => {
extra={[]} extra={[]}
> >
<ClusterMetrics /> <ClusterMetrics />
{provider === ProviderValueMap.DigitalOcean && <WorkerPools />} {/* {provider === ProviderValueMap.DigitalOcean && <WorkerPools />} */}
</PageContainer> </PageContainer>
); );
}; };
+3 -4
View File
@@ -21,7 +21,7 @@ import {
import AddCluster from './components/add-cluster'; import AddCluster from './components/add-cluster';
import AddPool from './components/add-pool'; import AddPool from './components/add-pool';
import RegisterCluster from './components/register-cluster'; import RegisterCluster from './components/register-cluster';
import { ProviderLabelMap, ProviderValueMap, addActions } from './config'; import { ProviderLabelMap, ProviderValueMap } from './config';
import { import {
ClusterFormData as FormData, ClusterFormData as FormData,
ClusterListItem as ListItem, ClusterListItem as ListItem,
@@ -146,7 +146,8 @@ const Credentials: React.FC = () => {
}; };
const handleClickDropdown = (item: any) => { const handleClickDropdown = (item: any) => {
handleAddCluster(item.key); navigate(`/cluster-management/clusters/create?action=${PageAction.CREATE}`);
// handleAddCluster(item.key);
}; };
const handleModalOk = async (data: FormData) => { const handleModalOk = async (data: FormData) => {
@@ -298,8 +299,6 @@ const Credentials: React.FC = () => {
marginTop={30} marginTop={30}
width={{ input: 300 }} width={{ input: 300 }}
buttonText={intl.formatMessage({ id: 'clusters.button.add' })} buttonText={intl.formatMessage({ id: 'clusters.button.add' })}
actionType="dropdown"
actionItems={addActions}
rowSelection={rowSelection} rowSelection={rowSelection}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
handleSearch={handleSearch} handleSearch={handleSearch}
@@ -1,19 +1,12 @@
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal/index'; import ScrollerModal from '@/components/scroller-modal/index';
import SealInput from '@/components/seal-form/seal-input';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import ContainerInstall from '@/pages/resources/components/container-install'; import React, { useRef } from 'react';
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 { import {
ClusterFormData as FormData, ClusterFormData as FormData,
ClusterListItem as ListItem ClusterListItem as ListItem
} from '../config/types'; } from '../config/types';
import CloudProvider from './cloud-provider-form'; import ClusterForm from './cluster-form';
import RegisterClusterInner from './register-cluster-inner';
type AddModalProps = { type AddModalProps = {
title: string; title: string;
@@ -35,15 +28,10 @@ const AddCluster: React.FC<AddModalProps> = ({
onOk, onOk,
onCancel onCancel
}) => { }) => {
const [form] = Form.useForm(); const form = useRef<any>(null);
const intl = useIntl();
const [submissionStatus, setSubmissionStatus] = React.useState<{
success: boolean;
data: ListItem;
}>({ success: false, data: {} as ListItem });
const handleSubmit = () => { const handleSubmit = () => {
form.submit(); form.current?.submit();
}; };
const handleOk = async (data: FormData) => { const handleOk = async (data: FormData) => {
@@ -54,63 +42,13 @@ const AddCluster: React.FC<AddModalProps> = ({
}; };
const handleCancel = () => { const handleCancel = () => {
form.resetFields(); form.current?.resetFields();
onCancel(); 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 ( return (
<ScrollerModal <ScrollerModal
title={modalTitle} title={title}
open={open} open={open}
onCancel={handleCancel} onCancel={handleCancel}
destroyOnHidden={true} destroyOnHidden={true}
@@ -118,49 +56,18 @@ const AddCluster: React.FC<AddModalProps> = ({
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
width={680} width={680}
footer={renderFooter()} footer={
<ModalFooter onOk={handleSubmit} onCancel={onCancel}></ModalFooter>
}
> >
{submissionStatus.success ? ( <ClusterForm
renderAddWorkerContent() ref={form}
) : ( provider={provider}
<Form credentialList={credentialList}
form={form} action={action}
onFinish={handleOk} currentData={currentData}
preserve={false} onFinish={handleOk}
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>
)}
</ScrollerModal> </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 ListMap from '@/components/dynamic-form/components/list-map';
import { FieldSchema } from '@/components/dynamic-form/config/types'; import { FieldSchema } from '@/components/dynamic-form/config/types';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, Form } from 'antd'; import { Button, Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
@@ -36,7 +37,7 @@ const ButtonWrapper = styled.span`
const CloudOptions: React.FC<{ const CloudOptions: React.FC<{
ref?: any; ref?: any;
}> = forwardRef((props, ref) => { }> = forwardRef((props, ref) => {
// form instance const intl = useIntl();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const [selectedOptions, setSelectedOptions] = React.useState<Set<string>>( const [selectedOptions, setSelectedOptions] = React.useState<Set<string>>(
new Set() new Set()
@@ -102,7 +103,9 @@ const CloudOptions: React.FC<{
<DropDownActions menu={menu}> <DropDownActions menu={menu}>
<Button variant="filled" color="default"> <Button variant="filled" color="default">
<PlusOutlined /> <PlusOutlined />
<span>Add Cloud Options</span> <span>
{intl.formatMessage({ id: 'clusters.workerpool.cloudOptions' })}
</span>
</Button> </Button>
</DropDownActions> </DropDownActions>
</Title> </Title>
@@ -113,7 +116,7 @@ const CloudOptions: React.FC<{
name={['cloud_options', field.name as string]} name={['cloud_options', field.name as string]}
> >
<ListMap <ListMap
btnText={'Add Item'} btnText={intl.formatMessage({ id: 'common.button.addItem' })}
label={field.title} label={field.title}
dataList={form.getFieldValue(['cloud_options', field.name]) || []} dataList={form.getFieldValue(['cloud_options', field.name]) || []}
properties={field.properties || {}} 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, useImperativeHandle,
useRef useRef
} from 'react'; } from 'react';
import { import { ProviderType } from '../config';
NodePoolFormData as FormData, import { NodePoolFormData as FormData } from '../config/types';
NodePoolListItem as ListItem import VolumesConfig from './volumes-config';
} from '../config/types';
import CloudOptions from './cloud-options';
type AddModalProps = { type AddModalProps = {
ref: any; ref: any;
name?: string;
action: PageActionType; action: PageActionType;
provider: string; // 'kubernetes' | 'custom' | 'digitalocean'; provider: ProviderType; // 'kubernetes' | 'custom' | 'digitalocean';
currentData?: ListItem | null; currentData?: FormData | null;
onFinish: (values: FormData) => void; onFinish: (values: FormData) => void;
}; };
const PoolForm: React.FC<AddModalProps> = forwardRef( const PoolForm: React.FC<AddModalProps> = forwardRef(
({ action, onFinish, currentData }, ref) => { ({ action, name = 'workerPoolForm', onFinish, currentData }, ref) => {
const cloudOptionsRef = useRef<any>(null); const cloudOptionsRef = useRef<any>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
@@ -35,21 +34,26 @@ const PoolForm: React.FC<AddModalProps> = forwardRef(
useEffect(() => { useEffect(() => {
if (currentData) { if (currentData) {
console.log('currentData===========1=', currentData);
form.setFieldsValue({ form.setFieldsValue({
...currentData, ...currentData
volumes: currentData.cloud_options?.volumes?.[0] || {}
}); });
cloudOptionsRef.current?.initFieldList();
} }
}, [currentData]); }, [currentData]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
reset: () => { resetFields: () => {
form.resetFields(); form.resetFields();
}, },
submit: () => { submit: () => {
form.submit(); form.submit();
}, },
setFieldsValue: (values: any) => {
form.setFieldsValue(values);
},
getFieldsValue: () => {
return form.getFieldsValue();
},
validateFields: async () => { validateFields: async () => {
return await form.validateFields(); return await form.validateFields();
} }
@@ -57,9 +61,11 @@ const PoolForm: React.FC<AddModalProps> = forwardRef(
return ( return (
<Form <Form
name={name}
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
preserve={false} preserve={false}
scrollToFirstError={true}
initialValues={{ initialValues={{
replicas: 1, replicas: 1,
batch_size: 1 batch_size: 1
@@ -158,7 +164,7 @@ const PoolForm: React.FC<AddModalProps> = forwardRef(
btnText={intl.formatMessage({ id: 'common.button.addLabel' })} btnText={intl.formatMessage({ id: 'common.button.addLabel' })}
></LabelSelector> ></LabelSelector>
</Form.Item> </Form.Item>
<CloudOptions ref={cloudOptionsRef}></CloudOptions> <VolumesConfig ref={cloudOptionsRef}></VolumesConfig>
</Form> </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', type: 'string',
title: 'Format', title: 'Format',
widget: 'Select', widget: 'Select',
enum: ['ext4', 'xfs', 'btrfs'], enum: ['ext4', 'xfs'],
style: { width: 150 } style: { width: 150 }
} }
} }
+55 -14
View File
@@ -1,6 +1,7 @@
import icons from '@/components/icon-font/icons'; import icons from '@/components/icon-font/icons';
import { StatusMaps } from '@/config'; import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types'; import { StatusType } from '@/config/types';
import React from 'react';
export const ClusterStatusValueMap = { export const ClusterStatusValueMap = {
Provisioning: 0, Provisioning: 0,
@@ -23,12 +24,17 @@ export const ClusterStatus: Record<string, StatusType> = {
export const ProviderValueMap = { export const ProviderValueMap = {
Kubernetes: 'Kubernetes', Kubernetes: 'Kubernetes',
DigitalOcean: 'DigitalOcean', DigitalOcean: 'DigitalOcean',
Custom: 'Custom' Custom: 'Custom',
HuaweiCloud: 'HuaweiCloud',
AliCloud: 'AliCloud',
TencentCloud: 'TencentCloud'
}; };
export type ProviderType = keyof typeof ProviderValueMap | null | undefined;
export const ProviderLabelMap = { export const ProviderLabelMap = {
[ProviderValueMap.Kubernetes]: 'Kubernetes', [ProviderValueMap.Kubernetes]: 'Kubernetes',
[ProviderValueMap.DigitalOcean]: 'Digital Ocean', [ProviderValueMap.DigitalOcean]: 'DigitalOcean',
[ProviderValueMap.Custom]: 'Custom' [ProviderValueMap.Custom]: 'Custom'
}; };
@@ -41,27 +47,62 @@ export const generateRegisterCommand = (params: {
--header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`; --header 'Authorization: Bearer ${params.registrationToken}' | kubectl apply -f -`;
}; };
export const addActions = [ export const providerList = [
{ {
label: 'clusters.provider.custom', label: 'clusters.provider.custom',
locale: true, locale: true,
value: ProviderValueMap.Custom, value: ProviderValueMap.Custom,
key: ProviderValueMap.Custom, key: ProviderValueMap.Custom,
icon: icons.Docker icon: React.cloneElement(icons.Docker, {
style: { color: 'var(--ant-color-primary)' }
})
}, },
{ {
label: 'Kubernetes', label: 'Kubernetes',
locale: false, locale: false,
value: ProviderValueMap.Kubernetes, value: ProviderValueMap.Kubernetes,
key: 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, locale: false,
value: ProviderValueMap.DigitalOcean, value: ProviderValueMap.DigitalOcean,
key: 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, locale: true,
icon: icons.KubernetesOutlined icon: icons.KubernetesOutlined
}, },
{ // {
key: 'addPool', // key: 'addPool',
label: 'clusters.button.addNodePool', // label: 'clusters.button.addNodePool',
provider: ProviderValueMap.DigitalOcean, // provider: ProviderValueMap.DigitalOcean,
locale: true, // locale: true,
icon: icons.Catalog // icon: icons.Catalog
}, // },
{ {
key: 'delete', key: 'delete',
label: 'common.button.delete', label: 'common.button.delete',
+12 -10
View File
@@ -1,3 +1,5 @@
import { ProviderType } from '.';
export interface CredentialFormData { export interface CredentialFormData {
name: string; name: string;
provider: string; provider: string;
@@ -20,15 +22,6 @@ export interface CredentialListItem {
updated_at: string; updated_at: string;
} }
export interface ClusterFormData {
name: string;
description: string;
provider: string;
credential_id: number;
zone: string;
region: string;
}
export interface NodePoolListItem { export interface NodePoolListItem {
id: number; id: number;
instance_type: string; instance_type: string;
@@ -47,7 +40,6 @@ export interface NodePoolFormData {
batch_size: number; batch_size: number;
labels: Record<string, string>; labels: Record<string, string>;
cloud_options: Record<string, any>; cloud_options: Record<string, any>;
volumes?: Record<string, any>;
} }
export interface ClusterListItem { export interface ClusterListItem {
@@ -67,3 +59,13 @@ export interface ClusterListItem {
state_message: string; state_message: string;
worker_pools: NodePoolListItem[]; 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) => ( render: (text: string, record: ClusterListItem) => (
<AutoTooltip ghost> <AutoTooltip ghost>
<Link <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} {text}
</Link> </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;
+29 -22
View File
@@ -21,24 +21,6 @@ const ModelItemContent = styled.div`
height: 100%; height: 100%;
width: 100%; width: 100%;
cursor: default; 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 { .content {
display: flex; display: flex;
flex-direction: column; 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 = { const sourceIconMap = {
[modelSourceMap.local_path_value]: 'icon-hard-disk', [modelSourceMap.local_path_value]: 'icon-hard-disk',
[modelSourceMap.huggingface_value]: 'icon-huggingface', [modelSourceMap.huggingface_value]: 'icon-huggingface',
@@ -96,9 +97,12 @@ const ModelItem: React.FC<{
const { model, onClick } = props; const { model, onClick } = props;
return ( return (
<Card onClick={() => onClick(model)} clickable={false} ghost> <Card
<ModelItemContent> onClick={() => onClick(model)}
<div className="title"> clickable={false}
ghost
header={
<Header>
<span className="text"> <span className="text">
<IconFont <IconFont
type={sourceIconMap[model.source]} type={sourceIconMap[model.source]}
@@ -114,7 +118,10 @@ const ModelItem: React.FC<{
message: model.state_message message: model.state_message
}} }}
></StatusTag> ></StatusTag>
</div> </Header>
}
>
<ModelItemContent>
<div className="content"> <div className="content">
<div className="extra-info"> <div className="extra-info">
<ThemeTag className="tag-item" color="blue"> <ThemeTag className="tag-item" color="blue">