Files
gpustack-ui/src/pages/cluster-management/cluster-create.tsx
T
2025-09-19 15:46:24 +08:00

347 lines
9.3 KiB
TypeScript

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 React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { createCluster, queryClusterToken, queryCredentialList } from './apis';
import ClusterSteps from './components/cluster-steps';
import FooterButtons from './components/footer-buttons';
import ProviderCatalog from './components/provider-catalog';
import { ProviderType, ProviderValueMap } from './config';
import providerList from './config/providers';
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 Nav = styled.div`
display: flex;
align-items: center;
height: 72px;
font-weight: 400;
font-size: 20px;
color: var(--ant-color-text-tertiary);
.level-2 {
color: var(--ant-color-text);
font-weight: 600;
}
`;
const Content = styled.div`
width: 600px;
`;
const HeaderContainer = styled.div`
position: relative;
display: grid;
grid-template-columns: 1fr;
align-items: center;
padding-inline: var(--layout-content-header-inlinepadding);
.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 startStep = 0;
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, { provider: ProviderType }>[]
>([]);
const [currentStep, setCurrentStep] = useState<number>(startStep);
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: ProviderValueMap.Docker
} 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 availableCredentials = useMemo(
() => credentialList.filter((item) => item.provider === extraData.provider),
[credentialList, extraData.provider]
);
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 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 resetForm = () => {
setFormValues({});
for (const [formKey, formRef] of Object.entries(formRefs)) {
formRefs[formKey] = React.createRef();
}
};
const handleSelectProvider = (value: string) => {
if (value === extraData.provider) {
onNext();
return;
}
setExtraData({
provider: value
} as ClusterFormData);
resetForm();
onNext();
};
useEffect(() => {
const fetchData = async () => {
const res = await queryCredentialList({ page: 1, perPage: 100 });
const list = res.items?.map((item) => ({
label: item.name,
value: item.id,
provider: item.provider
}));
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={availableCredentials}
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}
provider={extraData.provider}
/>
) : 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 : {})
};
const res = await createCluster({ data });
const info = await queryClusterToken({ id: res.id });
setRegistrationInfo({
...info,
cluster_id: res.id
});
return true;
};
const handleSubmit = async () => {
onNext(submit);
};
const handleCancel = () => {
navigate(-1);
};
return (
<PageContainer
ghost
footer={[
<FooterButtons
key="buttons"
onPrevious={onPrevious}
onNext={onNext}
handleCancel={handleCancel}
handleSubmit={handleSubmit}
showButtons={showButtons}
/>
]}
header={{
title: false,
style: {
paddingInline: 'var(--layout-content-header-inlinepadding)'
},
breadcrumb: {}
}}
pageHeaderRender={() => (
<HeaderContainer>
<Nav>
<span className="level-1">
{intl.formatMessage({ id: 'clusters.title' })}
</span>
<span
style={{
marginInline: 20,
color: 'var(--ant-color-split)'
}}
>
/
</span>
<span className="level-2">
{intl.formatMessage({ id: 'common.button.create' })}
</span>
</Nav>
<ClusterSteps
steps={steps}
currentStep={currentStep}
onChange={handleStepChange}
></ClusterSteps>
</HeaderContainer>
)}
>
{currentStep === startStep && (
<ProviderCatalog
dataList={providerList}
height={70}
onSelect={handleSelectProvider}
clickable={true}
current={extraData.provider}
/>
)}
{renderModules()}
<Container>
<Content>{renderForms()}</Content>
</Container>
</PageContainer>
);
};
export default ClusterCreate;