From 07ade24ea1808f303d30dfb5dbc03b94e40e5c2d Mon Sep 17 00:00:00 2001 From: jialin Date: Sun, 7 Sep 2025 19:21:03 +0800 Subject: [PATCH] refactor: cluster list --- src/atoms/clusters.ts | 4 + src/components/collapse-container/index.tsx | 167 +++++++++ src/pages/cluster-management/apis/index.ts | 6 +- .../cluster-management/cluster-create.tsx | 27 +- src/pages/cluster-management/clusters.tsx | 101 +++++- .../components/add-pool.tsx | 3 +- .../components/pool-form.tsx | 327 ++++++++++-------- .../components/pool-rows.tsx | 151 ++++++++ .../components/provider-catalog.tsx | 76 ++-- .../components/worker-pools.tsx | 2 +- src/pages/cluster-management/config/index.ts | 32 +- src/pages/cluster-management/config/types.ts | 2 + .../hooks/use-cluster-columns.tsx | 9 +- .../hooks/use-pools-columns.tsx | 22 +- .../step-forms/worker-pools-form.tsx | 61 ++-- 15 files changed, 770 insertions(+), 220 deletions(-) create mode 100644 src/atoms/clusters.ts create mode 100644 src/components/collapse-container/index.tsx create mode 100644 src/pages/cluster-management/components/pool-rows.tsx diff --git a/src/atoms/clusters.ts b/src/atoms/clusters.ts new file mode 100644 index 00000000..dec7b324 --- /dev/null +++ b/src/atoms/clusters.ts @@ -0,0 +1,4 @@ +import { atom } from 'jotai'; + +// models expand keys: create, update , delete, +export const expandKeysAtom = atom([]); diff --git a/src/components/collapse-container/index.tsx b/src/components/collapse-container/index.tsx new file mode 100644 index 00000000..5a5b2807 --- /dev/null +++ b/src/components/collapse-container/index.tsx @@ -0,0 +1,167 @@ +import IconFont from '@/components/icon-font'; +import { Card } from 'antd'; +import { createStyles } from 'antd-style'; +import classNames from 'classnames'; +import React, { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +const CardStyled = styled(Card)` + box-shadow: none !important; + &.isOpen { + .ant-card-head { + border-bottom: 1px solid var(--ant-color-border-secondary); + border-radius: var(--ant-border-radius) var(--ant-border-radius) 0 0; + } + } + .ant-card-head { + background-color: var(--ant-color-fill-quaternary); + border-bottom: none; + border-radius: var(--ant-border-radius); + &:hover { + background-color: var(--ant-color-fill-secondary); + } + } +`; + +const useStyles = createStyles(({ css, token }) => { + return { + title: css` + font-weight: 400; + height: 56px; + font-size: ${token.fontSizeLG}; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + `, + subtitle: css` + font-size: 12px; + color: ${token.colorTextSecondary}; + `, + content: css` + padding-top: 8px; + `, + left: css` + flex: 1; + `, + right: css` + display: flex; + align-items: center; + gap: 8px; + ` + }; +}); + +export interface CollapsibleContainerProps { + title?: React.ReactNode; + subtitle?: React.ReactNode; + right?: React.ReactNode; + defaultOpen?: boolean; + open?: boolean; + collapsible?: boolean; + onToggle?: (open: boolean) => void; + disabled?: boolean; + variant?: 'outlined' | 'borderless' | undefined; + className?: string; + children?: React.ReactNode; +} + +export default function CollapsibleContainer({ + title, + subtitle, + right, + defaultOpen = true, + open, + onToggle, + disabled = false, + variant = 'borderless', + className = '', + collapsible, + children +}: CollapsibleContainerProps) { + const { styles } = useStyles(); + const isControlled = typeof open === 'boolean'; + const [internalOpen, setInternalOpen] = useState(defaultOpen); + const isOpen = collapsible + ? isControlled + ? (open as boolean) + : internalOpen + : true; + + const toggle = () => { + if (disabled || !collapsible) return; + const next = !isOpen; + if (!isControlled) setInternalOpen(next); + onToggle?.(next); + }; + + const contentRef = useRef(null); + const [height, setHeight] = useState(isOpen ? 'auto' : '0px'); + + const renderTitle = () => { + if (!collapsible) { + return null; + } + return ( +
+
+ {title &&
{title}
} + {subtitle &&
{subtitle}
} +
+
+ {right} + +
+
+ ); + }; + + useEffect(() => { + if (!collapsible) { + setHeight('auto'); + return; + } + if (isOpen) { + const scrollHeight = contentRef.current?.scrollHeight || 0; + setHeight(scrollHeight + 'px'); + const timer = setTimeout(() => setHeight('auto'), 200); + return () => clearTimeout(timer); + } else { + const scrollHeight = contentRef.current?.scrollHeight || 0; + setHeight(scrollHeight + 'px'); + requestAnimationFrame(() => setHeight('0px')); + return () => {}; + } + }, [isOpen, collapsible]); + + return ( + +
+
{children}
+
+
+ ); +} diff --git a/src/pages/cluster-management/apis/index.ts b/src/pages/cluster-management/apis/index.ts index fe59caa3..aefbe552 100644 --- a/src/pages/cluster-management/apis/index.ts +++ b/src/pages/cluster-management/apis/index.ts @@ -102,13 +102,15 @@ export async function queryClusterToken(params: { id: number }) { // ===================== Worker Pools ===================== export async function queryWorkerPools( - params?: Global.SearchParams & { cluster_id: string | number } + params?: Global.SearchParams & { cluster_id: string | number }, + options?: any ) { return request>( `${WORKER_POOLS_API}?`, { method: 'GET', - params + params, + cancelToken: options?.token } ); } diff --git a/src/pages/cluster-management/cluster-create.tsx b/src/pages/cluster-management/cluster-create.tsx index 4e7d9a35..4177753d 100644 --- a/src/pages/cluster-management/cluster-create.tsx +++ b/src/pages/cluster-management/cluster-create.tsx @@ -3,9 +3,9 @@ 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 React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; -import { queryClusterList } from './apis'; +import { createCluster, queryCredentialList } from './apis'; import ClusterSteps from './components/cluster-steps'; import FooterButtons from './components/footer-buttons'; import ProviderCatalog from './components/provider-catalog'; @@ -45,6 +45,7 @@ const HeaderContainer = styled.div` const ClusterCreate = () => { const intl = useIntl(); + const startStep = 0; const stepList = useStepList(); const [searchParams] = useSearchParams(); const action = @@ -53,7 +54,7 @@ const ClusterCreate = () => { const [credentialList, setCredentialList] = useState< Global.BaseOption[] >([]); - const [currentStep, setCurrentStep] = useState(0); + const [currentStep, setCurrentStep] = useState(startStep); const [registrationInfo, setRegistrationInfo] = useState<{ token: string; image: string; @@ -96,11 +97,7 @@ const ClusterCreate = () => { 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); @@ -166,16 +163,24 @@ const ClusterCreate = () => { } }; + const resetForm = () => { + setFormValues({}); + for (const [formKey, formRef] of Object.entries(formRefs)) { + formRefs[formKey] = React.createRef(); + } + }; + const handleSelectProvider = (value: ProviderType) => { setExtraData({ provider: value } as ClusterFormData); + resetForm(); onNext(); }; useEffect(() => { const fetchData = async () => { - const res = await queryClusterList({ page: 1, perPage: 100 }); + const res = await queryCredentialList({ page: 1, perPage: 100 }); const list = res.items?.map((item) => ({ label: item.name, value: item.id @@ -232,7 +237,7 @@ const ClusterCreate = () => { ...extraData, ...(typeof values === 'object' ? values : {}) }; - console.log('submit values====:', data); + await createCluster({ data }); return true; }; @@ -274,7 +279,7 @@ const ClusterCreate = () => { )} > - {currentStep === 0 && ( + {currentStep === startStep && ( { deleteAPI: deleteCluster, contentForDelete: 'menu.clusterManagement.clusters' }); - + const [expandAtom, setExpandAtom] = useAtom(expandKeysAtom); + const { + handleExpandChange, + handleExpandAll, + updateExpandedRowKeys, + removeExpandedRowKey, + expandedRowKeys + } = useExpandedRowKeys(expandAtom); const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const intl = useIntl(); @@ -106,13 +120,13 @@ const Credentials: React.FC = () => { open: boolean; action: PageActionType; title: string; - provider: string; + provider: ProviderType; clusterId: number; }>({ open: false, action: PageAction.CREATE, title: '', - provider: ProviderValueMap.DigitalOcean, + provider: ProviderValueMap.DigitalOcean as ProviderType, clusterId: 0 }); @@ -246,6 +260,16 @@ const Credentials: React.FC = () => { } }); + const handleOnToggleExpandAll = () => {}; + + const handleToggleExpandAll = useMemoizedFn((expanded: boolean) => { + const keys = dataSource.dataList?.map((item) => item.id); + handleExpandAll(expanded, keys); + if (expanded) { + handleOnToggleExpandAll(); + } + }); + const handleSubmitWorkerPool = async (formdata: NodePoolFormData) => { try { await createWorkerPool({ @@ -262,6 +286,20 @@ const Credentials: React.FC = () => { } }; + const getWorkerPoolList = useMemoizedFn( + async (row: ClusterListItem, options?: any) => { + const params = { + cluster_id: row.id, + page: 1, + perPage: 100 + }; + const data = await queryWorkerPools(params, { + token: options?.token + }); + return data?.items || []; + } + ); + useEffect(() => { const fetchCredentialList = async () => { const data = await queryCredentialList({ page: 1, perPage: 100 }); @@ -274,6 +312,19 @@ const Credentials: React.FC = () => { fetchCredentialList(); }, []); + const renderChildren = ( + list: any, + options: { parent?: any; [key: string]: any } + ) => { + return ( + + ); + }; + const columns = useClusterColumns(handleSelect); return ( @@ -305,15 +356,23 @@ const Credentials: React.FC = () => { handleDeleteByBatch={handleDeleteBatch} handleClickPrimary={handleClickDropdown} > - { hideOnSinglePage: queryParams.perPage === 10, onChange: handlePageChange }} - >
+ > + {/* */} void; onCancel: () => void; diff --git a/src/pages/cluster-management/components/pool-form.tsx b/src/pages/cluster-management/components/pool-form.tsx index d0d04f70..4c218105 100644 --- a/src/pages/cluster-management/components/pool-form.tsx +++ b/src/pages/cluster-management/components/pool-form.tsx @@ -1,22 +1,36 @@ +import CollapsibleContainer, { + CollapsibleContainerProps +} from '@/components/collapse-container'; import LabelSelector from '@/components/label-selector'; import SealInputNumber from '@/components/seal-form/input-number'; import SealInput from '@/components/seal-form/seal-input'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import useAppUtils from '@/hooks/use-app-utils'; +import { DeleteOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; +import { Button, Form } from 'antd'; import _ from 'lodash'; -import React, { - forwardRef, - useEffect, - useImperativeHandle, - useRef -} from 'react'; +import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; +import styled from 'styled-components'; import { ProviderType } from '../config'; import { NodePoolFormData as FormData } from '../config/types'; import VolumesConfig from './volumes-config'; +const Container = styled.div` + pointer-events: auto; + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 0 16px; + .ant-form-item:nth-child(5) { + grid-column: 1 / 3; + } + + .ant-form-item:nth-child(6) { + grid-column: 1 / 3; + } +`; + type AddModalProps = { ref: any; name?: string; @@ -24,42 +38,74 @@ type AddModalProps = { provider: ProviderType; // 'kubernetes' | 'custom' | 'digitalocean'; currentData?: FormData | null; onFinish: (values: FormData) => void; + onDelete?: () => void; + showDelete?: boolean; + collapseProps?: CollapsibleContainerProps; }; -const PoolForm: React.FC = forwardRef( - ({ action, name = 'workerPoolForm', onFinish, currentData }, ref) => { - const cloudOptionsRef = useRef(null); - const [form] = Form.useForm(); - const intl = useIntl(); - const { getRuleMessage } = useAppUtils(); +const PoolForm: React.FC = forwardRef((props, ref) => { + const { + action, + name = 'workerPoolForm', + onFinish, + onDelete, + showDelete, + currentData, + collapseProps + } = props; + const { collapsible, onToggle, ...restCollapseProps } = collapseProps || {}; + const [form] = Form.useForm(); + const intl = useIntl(); + const { getRuleMessage } = useAppUtils(); + const labels = Form.useWatch('labels', form); + const instance_type = Form.useWatch('instance_type', form); - useEffect(() => { - if (currentData) { - console.log('currentData===========1=', currentData); - form.setFieldsValue({ - ...currentData - }); - } - }, [currentData]); + useEffect(() => { + if (currentData) { + console.log('currentData===========1=', 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(); - } - })); + 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 ( + return ( + + {showDelete && ( +
+ +
+ )}
= forwardRef( batch_size: 1 }} > - - name="instance_type" - rules={[ - { - required: true, - message: getRuleMessage( - 'input', - 'clusters.workerpool.instanceType' - ) - } - ]} - > - - - - name="replicas" - rules={[ - { - required: true, - message: getRuleMessage('input', 'clusters.workerpool.replicas') - } - ]} - > - - - - name="batch_size" - rules={[ - { - required: true, - message: getRuleMessage('input', 'clusters.workerpool.batchSize') - } - ]} - > - - - - name="os_image" - rules={[ - { - required: true, - message: getRuleMessage('input', 'clusters.workerpool.osImage') - } - ]} - > - - - - - name="labels" - rules={[ - ({ getFieldValue }) => ({ - validator(rule, value) { - if (_.keys(value).length > 0) { - if (_.some(_.keys(value), (k: string) => !value[k])) { - return Promise.reject( - intl.formatMessage( - { - id: 'common.validate.value' - }, - { - name: 'labels' - } - ) - ); - } - } - return Promise.resolve(); + + + name="instance_type" + rules={[ + { + required: true, + message: getRuleMessage( + 'input', + 'clusters.workerpool.instanceType' + ) } - }) - ]} - > - - - + ]} + > + + + + name="replicas" + rules={[ + { + required: true, + message: getRuleMessage('input', 'clusters.workerpool.replicas') + } + ]} + > + + + + name="batch_size" + rules={[ + { + required: true, + message: getRuleMessage( + 'input', + 'clusters.workerpool.batchSize' + ) + } + ]} + > + + + + name="os_image" + rules={[ + { + required: true, + message: getRuleMessage('input', 'clusters.workerpool.osImage') + } + ]} + > + + + + + name="labels" + rules={[ + ({ getFieldValue }) => ({ + validator(rule, value) { + if (_.keys(value).length > 0) { + if (_.some(_.keys(value), (k: string) => !value[k])) { + return Promise.reject( + intl.formatMessage( + { + id: 'common.validate.value' + }, + { + name: 'labels' + } + ) + ); + } + } + return Promise.resolve(); + } + }) + ]} + > + + + + - ); - } -); +
+ ); +}); export default PoolForm; diff --git a/src/pages/cluster-management/components/pool-rows.tsx b/src/pages/cluster-management/components/pool-rows.tsx new file mode 100644 index 00000000..b9906c71 --- /dev/null +++ b/src/pages/cluster-management/components/pool-rows.tsx @@ -0,0 +1,151 @@ +import DeleteModal from '@/components/delete-modal'; +import RowChildren from '@/components/seal-table/components/row-children'; +import { PageAction } from '@/config'; +import { PageActionType } from '@/config/types'; +import { useIntl } from '@umijs/max'; +import { useMemoizedFn } from 'ahooks'; +import { Col, message, Row } from 'antd'; +import React, { useRef, useState } from 'react'; +import { deleteWorkerPool, updateWorkerPool } from '../apis'; +import { ProviderType } from '../config'; +import { NodePoolFormData, NodePoolListItem } from '../config/types'; +import usePoolsColumns from '../hooks/use-pools-columns'; +import AddPool from './add-pool'; + +interface PoolRowsProps { + dataList: NodePoolListItem[]; + provider: ProviderType; + clusterId: number | string; +} + +const PoolRows: React.FC = ({ + dataList, + provider, + clusterId +}) => { + const intl = useIntl(); + const modalRef = useRef(null); + const [addPoolStatus, setAddPoolStatus] = useState<{ + open: boolean; + action: PageActionType; + title: string; + provider: ProviderType; + currentData: NodePoolListItem | null; + clusterId: number | string; + }>({ + open: false, + action: PageAction.EDIT, + title: '', + provider: provider, + currentData: null as NodePoolListItem | null, + clusterId: clusterId + }); + + const handleSubmitWorkerPool = async (formdata: NodePoolFormData) => { + try { + await updateWorkerPool({ + data: formdata, + id: addPoolStatus.currentData!.id + }); + setAddPoolStatus({ + ...addPoolStatus, + open: false + }); + message.success(intl.formatMessage({ id: 'common.message.success' })); + // handleSearch(); + } catch (error) { + // error + } + }; + + const handleEdit = (action: string, record: NodePoolListItem) => { + if (action === 'edit') { + setAddPoolStatus({ + open: true, + action: PageAction.EDIT, + title: intl.formatMessage( + { id: 'common.button.edit.item' }, + { name: record.instance_type } + ), + provider: provider, + currentData: record, + clusterId: record.cluster_id + }); + } + }; + + const handleDelete = (row: { name: string; id: number }, options?: any) => { + modalRef.current?.show({ + content: 'worker pool', + operation: 'common.delete.single.confirm', + name: row.name, + ...options, + async onOk() { + console.log('OK'); + await deleteWorkerPool(row.id); + } + }); + }; + + const onSelect = useMemoizedFn((key: string, record: NodePoolListItem) => { + if (key === 'delete') { + handleDelete({ ...record, name: record.instance_type }); + } + if (key === 'edit') { + handleEdit(key, record); + } + }); + + const columns = usePoolsColumns(onSelect); + return ( + <> + {dataList?.map((data: NodePoolListItem) => { + return ( +
+ + + {columns.map((col: Record) => { + return ( + + {col.render + ? col.render(data[col.dataIndex as string], data) + : data[col.dataIndex as string]} + + ); + })} + + +
+ ); + })} + { + setAddPoolStatus({ + open: false, + action: PageAction.CREATE, + title: '', + provider: provider, + currentData: null, + clusterId: 0 + }); + }} + onOk={handleSubmitWorkerPool} + > + + + ); +}; + +export default PoolRows; diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx index 7635dc9d..980af83a 100644 --- a/src/pages/cluster-management/components/provider-catalog.tsx +++ b/src/pages/cluster-management/components/provider-catalog.tsx @@ -1,6 +1,6 @@ import Card from '@/components/templates/card'; import { useIntl } from '@umijs/max'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { ProviderType } from '../config'; @@ -9,6 +9,22 @@ const Wrapper = styled.div` grid-template-columns: 1fr 1fr 1fr; gap: 22px; `; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const Title = styled.span` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 700; + font-size: 18px; + margin-block: 16px 24px; +`; + interface ProviderCatalogProps { onSelect?: (provider: ProviderType) => void; currentProvider?: ProviderType; @@ -20,6 +36,7 @@ interface ProviderCatalogProps { disabled?: boolean; icon: React.ReactNode; description?: string; + group?: string; }[]; } @@ -30,27 +47,46 @@ const ProviderCatalog: React.FC = ({ currentProvider }) => { const intl = useIntl(); + + const groupList = useMemo(() => { + if (!dataList) return {}; + return dataList?.reduce( + (acc, item) => { + (acc[item.group || 'default'] ??= []).push(item); + return acc; + }, + {} as Record + ); + }, [dataList]); + return ( - - {dataList?.map((action) => ( - 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'} - + + {Object.entries(groupList).map(([groupName, items]) => ( +
+ {groupName !== 'default' && {groupName}} + + {items?.map((action) => ( + 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'} + + ))} + +
))} -
+ ); }; diff --git a/src/pages/cluster-management/components/worker-pools.tsx b/src/pages/cluster-management/components/worker-pools.tsx index bb392b2b..ecd960e6 100644 --- a/src/pages/cluster-management/components/worker-pools.tsx +++ b/src/pages/cluster-management/components/worker-pools.tsx @@ -127,7 +127,7 @@ const WorkerPools = () => { } }; - const columns = usePoolsColumns(sortOrder, onSelect); + const columns = usePoolsColumns(onSelect, sortOrder); return ( <> diff --git a/src/pages/cluster-management/config/index.ts b/src/pages/cluster-management/config/index.ts index 2a907e69..aeaee77f 100644 --- a/src/pages/cluster-management/config/index.ts +++ b/src/pages/cluster-management/config/index.ts @@ -55,7 +55,8 @@ export const providerList = [ key: ProviderValueMap.Custom, icon: React.cloneElement(icons.Docker, { style: { color: 'var(--ant-color-primary)' } - }) + }), + group: 'default' }, { label: 'Kubernetes', @@ -64,7 +65,8 @@ export const providerList = [ key: ProviderValueMap.Kubernetes, icon: React.cloneElement(icons.KubernetesOutlined, { style: { color: 'var(--ant-color-primary)' } - }) + }), + group: 'default' }, { label: 'DigitalOcean', @@ -75,7 +77,8 @@ export const providerList = [ style: { color: 'var(--ant-color-primary)' } - }) + }), + group: 'Cloud Provider' }, { label: 'Huawei Cloud', @@ -84,7 +87,8 @@ export const providerList = [ value: ProviderValueMap.HuaweiCloud, key: ProviderValueMap.HuaweiCloud, icon: icons.HuaweiCloud, - description: 'Currently Not Supported' + description: 'Comming soon', + group: 'Cloud Provider' }, { label: 'Ali Cloud', @@ -93,7 +97,8 @@ export const providerList = [ value: ProviderValueMap.AliCloud, key: ProviderValueMap.AliCloud, icon: icons.AliCloud, - description: 'Currently Not Supported' + description: 'Comming soon', + group: 'Cloud Provider' }, { label: 'Tencent Cloud', @@ -102,7 +107,8 @@ export const providerList = [ value: ProviderValueMap.TencentCloud, key: ProviderValueMap.TencentCloud, icon: icons.TencentCloud, - description: 'Currently Not Supported' + description: 'Comming soon', + group: 'Cloud Provider' } ]; @@ -142,13 +148,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', diff --git a/src/pages/cluster-management/config/types.ts b/src/pages/cluster-management/config/types.ts index 0d377c2b..c77db981 100644 --- a/src/pages/cluster-management/config/types.ts +++ b/src/pages/cluster-management/config/types.ts @@ -29,8 +29,10 @@ export interface NodePoolListItem { batch_size: number; labels: Record; cloud_options: Record; + os_image: string; created_at: string; updated_at: string; + cluster_id: number; } export interface NodePoolFormData { diff --git a/src/pages/cluster-management/hooks/use-cluster-columns.tsx b/src/pages/cluster-management/hooks/use-cluster-columns.tsx index f9ded4fc..f9ce56d9 100644 --- a/src/pages/cluster-management/hooks/use-cluster-columns.tsx +++ b/src/pages/cluster-management/hooks/use-cluster-columns.tsx @@ -33,6 +33,7 @@ const useClusterColumns = ( { title: intl.formatMessage({ id: 'common.table.name' }), dataIndex: 'name', + span: 3, render: (text: string, record: ClusterListItem) => ( {ProviderLabelMap[value]} }, { title: intl.formatMessage({ id: 'common.table.status' }), dataIndex: 'state', + span: 3, render: (value: number) => ( ( {record.ready_workers} / {record.workers} @@ -72,17 +76,19 @@ const useClusterColumns = ( { title: 'GPUs', dataIndex: 'gpus', + span: 3, render: (value: number) => {value} }, { title: intl.formatMessage({ id: 'clusters.table.deployments' }), dataIndex: 'models', + span: 2, render: (value: number) => {value} }, { title: intl.formatMessage({ id: 'common.table.createTime' }), dataIndex: 'created_at', - width: 180, + span: 4, render: (value: string) => ( {dayjs(value).format('YYYY-MM-DD HH:mm:ss')} ) @@ -90,6 +96,7 @@ const useClusterColumns = ( { title: intl.formatMessage({ id: 'common.table.operation' }), dataIndex: 'operations', + span: 3, render: (value: string, record: ClusterListItem) => ( void + handleSelect: (val: string, record: ListItem) => void, + sortOrder?: SortOrder ): ColumnsType => { const intl = useIntl(); @@ -40,6 +40,10 @@ const usePoolsColumns = ( ellipsis: { showTitle: false }, + span: 4, + style: { + paddingInline: 'var(--ant-table-cell-padding-inline)' + }, render: (text: string) => ( {text} @@ -49,17 +53,20 @@ const usePoolsColumns = ( { title: intl.formatMessage({ id: 'clusters.workerpool.replicas' }), dataIndex: 'replicas', + span: 3, key: 'replicas' }, { title: intl.formatMessage({ id: 'clusters.workerpool.batchSize' }), dataIndex: 'batch_size', - key: 'batch_size' + key: 'batch_size', + span: 3 }, { title: intl.formatMessage({ id: 'clusters.workerpool.osImage' }), dataIndex: 'os_image', key: 'os_image', + span: 3, ellipsis: { showTitle: false }, @@ -74,6 +81,7 @@ const usePoolsColumns = ( dataIndex: 'labels', key: 'labels', width: 200, + span: 4, render: (text: string, record: ListItem) => ( ) @@ -82,6 +90,7 @@ const usePoolsColumns = ( title: intl.formatMessage({ id: 'common.table.createTime' }), dataIndex: 'create_at', key: 'created_at', + span: 4, showSorterTooltip: false, defaultSortOrder: 'descend', sortOrder: sortOrder, @@ -89,11 +98,18 @@ const usePoolsColumns = ( ellipsis: { showTitle: false }, + style: { + paddingLeft: 42 + }, render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss') }, { title: intl.formatMessage({ id: 'common.table.operation' }), key: 'operations', + span: 3, + style: { + paddingLeft: 36 + }, render: (text: string, record: ListItem) => ( { const { provider, action, currentData } = props; - const countRef = useRef(1); + const countRef = useRef(0); const formRefs = useRef | null>>({}); + const [activeKey, setActiveKey] = useState>(new Set([0])); const [workerPoolList, setWorkerPoolList] = useState< Map >( new Map([ [ - 1, + 0, { instance_type: 'Pool-1' } @@ -68,9 +70,14 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { const newId = updateCount(); setWorkerPoolList((prev) => new Map(prev).set(newId, { - instance_type: `Pool-${newId}` + instance_type: `Pool-${newId + 1}` } as NodePoolFormData) ); + setActiveKey((prev) => new Set([newId])); + + requestAnimationFrame(() => { + window.scrollTo(0, document.body.scrollHeight); + }); }; const handleRemovePool = (id: number) => { @@ -137,6 +144,19 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { }; }; + const handleOnToggle = (open: boolean, key: number) => { + console.log('Active keys changed:', key); + if (open) { + setActiveKey((prev) => new Set([key])); + } else { + setActiveKey((prev) => { + const newSet = new Set(prev); + newSet.delete(key); + return newSet; + }); + } + }; + useImperativeHandle(ref, () => ({ submit: () => {}, validateFields: validateFields, @@ -157,7 +177,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { marginTop={0} left={ - Worker Pools Configuration + Worker Pools } right={ @@ -171,6 +191,7 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { } > + {Array.from(workerPoolList.keys()).map((key, index) => (
@@ -183,34 +204,22 @@ const WorkerPoolsForm = forwardRef((props: WorkerPoolsFormProps, ref) => { formRefs.current[key] = el; } }} + collapseProps={{ + collapsible: true, + open: activeKey.has(key), + defaultOpen: activeKey.has(key), + onToggle: (open: boolean) => handleOnToggle(open, key) + }} + showDelete={workerPoolList.size > 1} onFinish={handleOnFinish} provider={provider} currentData={workerPoolList.get(key)} + onDelete={() => handleRemovePool(key)} > - {index !== Array.from(workerPoolList.keys()).length - 1 && ( - - - - )}
))} -
- -
); });