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',
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',
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 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) => (
+7 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1
View File
@@ -9,6 +9,7 @@
width: max-content;
height: 24px;
font-size: var(--font-size-small);
font-weight: 400;
overflow: hidden;
.txt {
+51 -8
View File
@@ -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>
);
};
+11 -2
View File
@@ -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;
}
+3 -1
View File
@@ -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'
};
+2 -1
View File
@@ -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'
};
+2 -1
View File
@@ -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'
};
+7 -2
View File
@@ -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 ==========
+4 -2
View File
@@ -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 ==========
+4 -2
View File
@@ -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 ==========
+6 -2
View File
@@ -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 ==========
+4 -2
View File
@@ -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 ==========
+4 -2
View File
@@ -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 ==========
+3 -1
View File
@@ -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': '添加存储卷'
};
+2 -1
View File
@@ -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': '添加项'
};
+2 -1
View File
@@ -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>
);
};
+3 -4
View File
@@ -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 }
}
}
+55 -14
View File
@@ -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',
+12 -10
View File
@@ -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;
+29 -22
View File
@@ -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">