refactor: create cluster ux

This commit is contained in:
jialin
2026-01-06 17:21:32 +08:00
parent 66e8f83395
commit c821a2e463
10 changed files with 200 additions and 67 deletions
+73 -45
View File
@@ -1,13 +1,12 @@
import { clusterSessionAtom } from '@/atoms/clusters'; import { clusterSessionAtom } from '@/atoms/clusters';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import PageBreadcrumb from '@/pages/_components/page-breadcrumb'; import ColumnWrapper from '@/pages/_components/column-wrapper';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { PageContainerInner } from '../_components/page-box';
import { import {
createCluster, createCluster,
queryClusterList, queryClusterList,
@@ -38,10 +37,28 @@ const Container = styled.div`
`; `;
const Content = styled.div` const Content = styled.div`
width: 600px; width: 100%;
`; `;
const ClusterCreate = () => { const StepsWrapper = styled.div`
width: 230px;
padding-inline: 24px;
padding-block: 16px;
// background-color: var(--ant-color-fill-quaternary);
border-right: 1px solid var(--ant-color-split);
.ant-steps.ant-steps-vertical {
height: 600px;
}
`;
const MainWrapper = styled.div`
display: flex;
height: 100%;
`;
const ClusterCreate: React.FC<{
onClose?: () => void;
}> = ({ onClose }) => {
const intl = useIntl(); const intl = useIntl();
const startStep = 0; const startStep = 0;
const stepList = useStepList(); const stepList = useStepList();
@@ -297,10 +314,12 @@ const ClusterCreate = () => {
firstAddCluster: false, firstAddCluster: false,
firstAddWorker: false firstAddWorker: false
}); });
navigate(`/cluster-management/clusters/list`); // navigate(`/cluster-management/clusters/list`);
onClose?.();
return; return;
} }
navigate(-1); onClose?.();
// navigate(-1);
}; };
const breadcrumbItems = [ const breadcrumbItems = [
@@ -314,46 +333,55 @@ const ClusterCreate = () => {
]; ];
return ( return (
<PageContainerInner <MainWrapper>
footer={[ <StepsWrapper>
<FooterButtons <ClusterSteps steps={steps} currentStep={currentStep}></ClusterSteps>
key="buttons" </StepsWrapper>
onPrevious={onPrevious} <ColumnWrapper
onNext={onNext} styles={{
handleCancel={handleCancel} container: { paddingTop: 16, paddingBottom: 56 }
handleSubmit={handleSubmit} }}
loading={submitLoading} footer={
showButtons={showButtons} <FooterButtons
/> key="buttons"
]} onPrevious={onPrevious}
header={{ onNext={onNext}
title: <PageBreadcrumb items={breadcrumbItems} /> handleCancel={handleCancel}
}} handleSubmit={handleSubmit}
> showButtons={showButtons}
<ClusterSteps steps={steps} currentStep={currentStep}></ClusterSteps> loading={submitLoading}
<div style={{ width: 800, margin: '0 auto' }}>
{currentStep === startStep && (
<ProviderCatalog
dataList={providerList}
height={70}
onSelect={handleSelectProvider}
clickable={true}
current={extraData.provider}
/> />
)} }
<StepsContext.Provider >
value={{ <div style={{ flex: 1 }}>
formValues: formValues, {currentStep === startStep && (
systemConfig: systemConfig <ProviderCatalog
}} groupIcons={{
> 'clusters.create.provider.self': 'icon-server02',
{renderModules()} 'clusters.create.provider.cloud': 'icon-cloud'
<Container> }}
<Content>{renderForms()}</Content> cols={2}
</Container> dataList={providerList}
</StepsContext.Provider> height={70}
</div> onSelect={handleSelectProvider}
</PageContainerInner> clickable={true}
current={extraData.provider}
/>
)}
<StepsContext.Provider
value={{
formValues: formValues,
systemConfig: systemConfig
}}
>
{renderModules()}
<Container>
<Content>{renderForms()}</Content>
</Container>
</StepsContext.Provider>
</div>
</ColumnWrapper>
</MainWrapper>
); );
}; };
@@ -0,0 +1,37 @@
import GSDrawer from '@/components/scroller-modal/gs-drawer';
import React from 'react';
import ClusterCreate from './cluster-create';
interface ClusterModalProps {
open: boolean;
onClose: () => void;
}
const ClusterModal: React.FC<ClusterModalProps> = ({ open, onClose }) => {
const handleCancel = () => {
onClose();
};
return (
<GSDrawer
title={'Create Cluster'}
open={open}
onClose={handleCancel}
destroyOnHidden={true}
closeIcon={false}
maskClosable={false}
keyboard={false}
width={900}
styles={{
body: {
paddingBlock: 0
}
}}
footer={false}
>
<ClusterCreate onClose={handleCancel}></ClusterCreate>
</GSDrawer>
);
};
export default ClusterModal;
+11 -1
View File
@@ -30,6 +30,7 @@ import {
updateCluster, updateCluster,
WORKER_POOLS_API WORKER_POOLS_API
} from './apis'; } from './apis';
import ClusterModal from './cluster-modal';
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 { import {
@@ -47,6 +48,7 @@ import {
} from './config/types'; } from './config/types';
import useAddWorker from './hooks/use-add-worker'; import useAddWorker from './hooks/use-add-worker';
import useClusterColumns from './hooks/use-cluster-columns'; import useClusterColumns from './hooks/use-cluster-columns';
import useCreateCluster from './hooks/use-create-cluster';
const Clusters: React.FC = () => { const Clusters: React.FC = () => {
const { const {
@@ -77,6 +79,10 @@ const Clusters: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const { handleAddWorker, checkDefaultCluster, AddWorkerModal, setStepList } = const { handleAddWorker, checkDefaultCluster, AddWorkerModal, setStepList } =
useAddWorker({}); useAddWorker({});
const { clusterModalStatus, openClusterModal, closeClusterModal } =
useCreateCluster({
refresh: handleSearch
});
const [openAddModal, setOpenAddModal] = useState<{ const [openAddModal, setOpenAddModal] = useState<{
open: boolean; open: boolean;
@@ -124,7 +130,7 @@ const Clusters: React.FC = () => {
}; };
const handleClickDropdown = () => { const handleClickDropdown = () => {
navigate(`/cluster-management/clusters/create?action=${PageAction.CREATE}`); openClusterModal();
}; };
const handleModalOk = async (data: FormData) => { const handleModalOk = async (data: FormData) => {
@@ -396,6 +402,10 @@ const Clusters: React.FC = () => {
onOk={handleSubmitWorkerPool} onOk={handleSubmitWorkerPool}
></AddPool> ></AddPool>
<DeleteModal ref={modalRef}></DeleteModal> <DeleteModal ref={modalRef}></DeleteModal>
<ClusterModal
open={clusterModalStatus.open}
onClose={closeClusterModal}
></ClusterModal>
{AddWorkerModal} {AddWorkerModal}
</> </>
); );
@@ -7,8 +7,7 @@ const { Step } = Steps;
const Wrapper = styled.div` const Wrapper = styled.div`
width: min(1200px, 100%); width: min(1200px, 100%);
margin: 0 auto; margin: 0 auto;
padding-block: 30px; padding-block: 0px;
background-color: var(--ant-color-bg-container);
.ant-steps-item-description { .ant-steps-item-description {
max-width: 300px !important; max-width: 300px !important;
color: var(--ant-color-text-description) !important; color: var(--ant-color-text-description) !important;
@@ -24,7 +23,12 @@ const ClusterSteps: React.FC<{
return ( return (
<Wrapper> <Wrapper>
<Steps current={currentStep} size="small" onChange={onChange} progressDot> <Steps
current={currentStep}
size="small"
onChange={onChange}
direction="vertical"
>
{steps.map((step) => ( {steps.map((step) => (
<Step <Step
key={step.title} key={step.title}
@@ -37,15 +37,19 @@ const FooterButtons: React.FC<FooterButtonsProps> = (props) => {
onNext(); onNext();
}; };
return ( return (
<Title style={{ height: 64 }}> <Title style={{ padding: '16px 24px 32px' }}>
<div className="flex-center gap-16"> <div className="flex-center gap-20">
{showButtons.previous && ( {showButtons.previous && (
<Button type="link" icon={<LeftOutlined />} onClick={onPrevious}> <Button type="default" icon={<LeftOutlined />} onClick={onPrevious}>
{intl.formatMessage({ id: 'common.button.prev' })} {intl.formatMessage({ id: 'common.button.prev' })}
</Button> </Button>
)} )}
{showButtons.next && ( {showButtons.next && (
<Button type="link" onClick={handleOnNext}> <Button
type="primary"
onClick={handleOnNext}
style={{ minWidth: 100 }}
>
{intl.formatMessage({ id: 'common.button.next' })} {intl.formatMessage({ id: 'common.button.next' })}
<RightOutlined /> <RightOutlined />
</Button> </Button>
@@ -62,7 +66,8 @@ const FooterButtons: React.FC<FooterButtonsProps> = (props) => {
style={{ minWidth: 88 }} style={{ minWidth: 88 }}
loading={loading} loading={loading}
> >
{intl.formatMessage({ id: 'common.button.save' })} {/* {intl.formatMessage({ id: 'common.button.save' })} */}
Create & Continue
</Button> </Button>
)} )}
</div> </div>
@@ -1,3 +1,4 @@
import IconFont from '@/components/icon-font';
import Card from '@/components/templates/card'; import Card from '@/components/templates/card';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
@@ -10,7 +11,7 @@ const Wrapper = styled.div<{ $cols?: number }>`
grid-template-columns: repeat(${(props) => props.$cols || 3}, 1fr); grid-template-columns: repeat(${(props) => props.$cols || 3}, 1fr);
gap: 16px; gap: 16px;
.template-card-wrapper { .template-card-wrapper {
padding: 10px; padding: 10px 16px;
} }
.template-card-inner { .template-card-inner {
justify-content: center; justify-content: center;
@@ -21,16 +22,26 @@ const Container = styled.div`
display: flex; display: flex;
margin: 0 auto; margin: 0 auto;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 24px;
`; `;
const Title = styled.span` const Title = styled.span`
display: flex; display: flex;
gap: 8px;
align-items: center; align-items: center;
justify-content: space-between;
font-weight: 700; font-weight: 700;
font-size: 16px; font-size: 14px;
margin-block: 20px 16px; margin-bottom: 16px;
.icon {
color: var(--ant-color-text-secondary);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: var(--ant-color-fill-tertiary);
}
`; `;
const Header = styled.div` const Header = styled.div`
@@ -38,6 +49,9 @@ const Header = styled.div`
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 4px;
.title {
font-weight: 500;
}
.description { .description {
font-size: 13px; font-size: 13px;
font-weight: 400; font-weight: 400;
@@ -47,6 +61,7 @@ const Header = styled.div`
interface ProviderCatalogProps { interface ProviderCatalogProps {
onSelect?: (provider: string, item: any) => void; onSelect?: (provider: string, item: any) => void;
groupIcons?: Record<string, string>;
cols?: number; cols?: number;
current?: ProviderType | string; current?: ProviderType | string;
clickable?: boolean; clickable?: boolean;
@@ -64,6 +79,7 @@ interface ProviderCatalogProps {
const ProviderCatalog: React.FC<ProviderCatalogProps> = ({ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
onSelect, onSelect,
groupIcons,
cols = 3, cols = 3,
dataList, dataList,
clickable, clickable,
@@ -86,7 +102,7 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
const renderTitle = (item: any) => { const renderTitle = (item: any) => {
return ( return (
<Header> <Header>
<span> <span className="title">
{item.locale ? intl.formatMessage({ id: item.label }) : item.label} {item.locale ? intl.formatMessage({ id: item.label }) : item.label}
</span> </span>
{item.description && ( {item.description && (
@@ -103,7 +119,14 @@ const ProviderCatalog: React.FC<ProviderCatalogProps> = ({
{Object.entries(groupList).map(([groupName, items]) => ( {Object.entries(groupList).map(([groupName, items]) => (
<div key={groupName}> <div key={groupName}>
{groupName !== 'default' && ( {groupName !== 'default' && (
<Title>{intl.formatMessage({ id: groupName })}</Title> <Title>
{groupIcons && groupIcons[groupName] && (
<span className="icon">
<IconFont type={groupIcons[groupName]} />
</span>
)}
<span>{intl.formatMessage({ id: groupName })}</span>
</Title>
)} )}
<Wrapper $cols={cols}> <Wrapper $cols={cols}>
{items?.map((action) => ( {items?.map((action) => (
@@ -141,11 +141,11 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
<Box> <Box>
<ProviderCatalog <ProviderCatalog
onSelect={onSelect} onSelect={onSelect}
height={50} height={60}
current={current} current={current}
dataList={supportedHardPlatforms} dataList={supportedHardPlatforms}
clickable={clickable} clickable={clickable}
cols={4} cols={3}
/> />
</Box> </Box>
); );
@@ -0,0 +1,26 @@
import { useState } from 'react';
const useCreateCluster = (options: { refresh: () => void }) => {
const [clusterModalStatus, setClusterModalStatus] = useState<{
open: boolean;
}>({
open: false
});
const openClusterModal = () => {
setClusterModalStatus({ open: true });
};
const closeClusterModal = () => {
setClusterModalStatus({ open: false });
options.refresh();
};
return {
clusterModalStatus,
openClusterModal,
closeClusterModal
};
};
export default useCreateCluster;
@@ -10,8 +10,8 @@ const Title = styled.span`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-weight: 700; font-weight: 600;
font-size: 16px; font-size: 14px;
.text { .text {
font-size: 20px; font-size: 20px;
} }
@@ -30,8 +30,8 @@ const Title = styled.span`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-weight: 700; font-weight: 600;
font-size: 16px; font-size: 14px;
.text { .text {
font-size: 20px; font-size: 20px;
} }