From afcd3e5693828ef53423d90684591e568e92cf49 Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 8 Jan 2026 20:32:07 +0800 Subject: [PATCH] feat: add cluster detail --- src/components/card-wrapper/index.less | 2 +- src/components/collapse-container/index.tsx | 4 ++ src/components/page-tools/index.tsx | 9 +-- src/config/theme/dark.ts | 4 +- src/config/theme/light.ts | 2 +- src/global.less | 2 +- src/locales/en-US/clusters.ts | 4 +- src/locales/ja-JP/clusters.ts | 8 +-- src/locales/ru-RU/clusters.ts | 4 +- src/locales/zh-CN/clusters.ts | 5 +- src/pages/_components/form-drawer.tsx | 49 ++++++++++--- src/pages/api-keys/index.tsx | 2 +- src/pages/backends/index.tsx | 2 +- .../cluster-management/cluster-create.tsx | 1 - .../cluster-workers-modal.tsx | 60 ++++++++++++++++ src/pages/cluster-management/clusters.tsx | 16 ++++- .../components/add-worker/index.tsx | 2 +- .../components/pool-form.tsx | 5 ++ .../components/worker-pools.tsx | 2 +- src/pages/cluster-management/credentials.tsx | 2 +- .../hooks/use-add-worker.tsx | 27 +++++-- .../hooks/use-cluster-columns.tsx | 17 +++-- .../hooks/use-cluster-workers.ts | 32 +++++++++ .../hooks/use-pools-columns.tsx | 7 +- src/pages/dashboard/components/over-view.tsx | 6 +- src/pages/llmodels/catalog.tsx | 2 +- .../llmodels/components/hf-model-file.tsx | 72 +------------------ src/pages/resources/components/gpus.tsx | 3 +- .../components/worker-detail-content.tsx | 2 +- src/pages/resources/components/workers.tsx | 29 ++++++-- .../resources/hooks/use-worker-columns.tsx | 10 ++- src/pages/users/index.tsx | 2 +- 32 files changed, 255 insertions(+), 139 deletions(-) create mode 100644 src/pages/cluster-management/cluster-workers-modal.tsx create mode 100644 src/pages/cluster-management/hooks/use-cluster-workers.ts diff --git a/src/components/card-wrapper/index.less b/src/components/card-wrapper/index.less index 8671526c..6376d7e8 100644 --- a/src/components/card-wrapper/index.less +++ b/src/components/card-wrapper/index.less @@ -1,6 +1,6 @@ .cardWrapper { border-radius: var(--border-radius-small); - background-color: var(--color-white-1); + background-color: var(--ant-color-bg-container); box-shadow: none; padding: 10px 16px; border: 1px solid var(--ant-color-border); diff --git a/src/components/collapse-container/index.tsx b/src/components/collapse-container/index.tsx index 21fc862a..aa8aee47 100644 --- a/src/components/collapse-container/index.tsx +++ b/src/components/collapse-container/index.tsx @@ -88,6 +88,7 @@ export interface CollapsibleContainerProps { className?: string; children?: React.ReactNode; styles?: { + root?: React.CSSProperties; body?: React.CSSProperties; header?: React.CSSProperties; content?: React.CSSProperties; @@ -191,6 +192,9 @@ export default function CollapsibleContainer({ className={classNames(className, { collapsible, disabled, isOpen })} variant={variant} styles={{ + root: { + ...cardStyles?.root + }, body: { padding: 0, ...cardStyles?.body diff --git a/src/components/page-tools/index.tsx b/src/components/page-tools/index.tsx index f664fbd7..d886ac1a 100644 --- a/src/components/page-tools/index.tsx +++ b/src/components/page-tools/index.tsx @@ -55,6 +55,7 @@ interface ActionItem { icon: React.ReactNode; [key: string]: any; } + interface FilterBarProps { handleInputChange: (e: React.ChangeEvent) => void; handleSelectChange?: (value: any) => void; @@ -76,7 +77,7 @@ interface FilterBarProps { showDeleteButton?: boolean; right?: React.ReactNode; left?: React.ReactNode; - width?: { + widths?: { input?: number; select?: number; }; @@ -102,7 +103,7 @@ export const FilterBar: React.FC = (props) => { selectHolder, right, left, - width + widths } = props; const intl = useIntl(); @@ -181,7 +182,7 @@ export const FilterBar: React.FC = (props) => { id: 'common.filter.name' }) } - style={{ width: width?.input || 230 }} + style={{ width: widths?.input || 230 }} allowClear onChange={handleInputChange} > @@ -190,7 +191,7 @@ export const FilterBar: React.FC = (props) => { allowClear showSearch={false} placeholder={selectHolder} - style={{ width: width?.select || 230 }} + style={{ width: widths?.select || 230 }} size="large" onChange={handleSelectChange} options={selectOptions} diff --git a/src/config/theme/dark.ts b/src/config/theme/dark.ts index 5c681e7c..79d802aa 100644 --- a/src/config/theme/dark.ts +++ b/src/config/theme/dark.ts @@ -97,7 +97,7 @@ export default { borderRadiusSM: 2, fontSize: 14, motion: true, - colorFill: '#000', - colorBgBase: '#000' + colorFill: '#0A0A0A', + colorBgBase: '#0A0A0A' } }; diff --git a/src/config/theme/light.ts b/src/config/theme/light.ts index 9005465b..ed2bedfd 100644 --- a/src/config/theme/light.ts +++ b/src/config/theme/light.ts @@ -89,7 +89,7 @@ export default { darkMode: false, fontFamily: "Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", - colorText: 'rgba(0,0,0,1)', + colorText: '#1F1F1F', colorPrimary: COLOR_PRIMARY, colorSuccess: '#54cc98', colorBorder: '#d3d0d9', diff --git a/src/global.less b/src/global.less index c8dc7cca..51142fc8 100644 --- a/src/global.less +++ b/src/global.less @@ -103,7 +103,7 @@ html[data-theme='realDark'] { --color-spotlight-bg: #3e3e3e; --color-esc-hint-bg: rgba(150, 150, 150, 75%); - background: #141414; + background: #292929; .ant-result-image { opacity: 0.9; diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index 634d21d0..717ff1b0 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -102,7 +102,7 @@ export default { '{count} new worker has been added to the cluster.', 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.', - 'clusters.create.serverUrl': 'Server URL', + 'clusters.create.serverUrl': 'GPUStack Server URL', 'clusters.create.workerConfig': 'Worker Configuration', 'clusters.addworker.containerName': 'Worker Container Name', 'clusters.addworker.containerName.tips': @@ -113,7 +113,7 @@ export default { 'clusters.table.ip.internal': 'Internal', 'clusters.table.ip.external': 'External', 'clusters.form.serverUrl.tips': - 'Specify the server URL accessible from your cloud provider.', + 'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack directly.', 'clusters.form.setDefault': 'Set as Default', 'clusters.form.setDefault.tips': 'Default for deployment.', 'clusters.addworker.noClusters': 'No available Docker clusters found', diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index a2186d7b..755f3a1c 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -102,7 +102,7 @@ export default { '{count} new worker has been added to the cluster.', 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.', - 'clusters.create.serverUrl': 'Server URL', + 'clusters.create.serverUrl': 'GPUStack Server URL', 'clusters.create.workerConfig': 'Worker Configuration', 'clusters.addworker.containerName': 'Worker Container Name', 'clusters.addworker.containerName.tips': @@ -113,7 +113,7 @@ export default { 'clusters.table.ip.internal': 'Internal', 'clusters.table.ip.external': 'External', 'clusters.form.serverUrl.tips': - 'Specify the server URL accessible from your cloud provider.', + 'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack directly.', 'clusters.form.setDefault': 'Set as Default', 'clusters.form.setDefault.tips': 'Default for deployment.', 'clusters.addworker.noClusters': 'No available Docker clusters found', @@ -205,7 +205,7 @@ export default { // 73. 'clusters.addworker.cacheVolume.holder': 'e.g. /data/cache (path must start with /)', // 74. 'clusters.addworker.message.success_single': '{count} new worker has been added to the cluster.', // 75. 'clusters.addworker.message.success_multiple': '{count} new workers have been added to the cluster.', -// 76. 'clusters.create.serverUrl': 'Server URL', +// 76. 'clusters.create.serverUrl': 'GPUStack Server URL', // 77. 'clusters.create.workerConfig': 'Worker Configuration' // 78. 'clusters.addworker.containerName': 'Worker Container Name', // 79. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.', @@ -213,7 +213,7 @@ export default { // 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.', // 79. 'clusters.table.ip.internal': 'Internal', // 80. 'clusters.table.ip.external': 'External', -// 81. 'clusters.form.serverUrl.tips': 'Specify the server URL accessible from your cloud provider.', +// 81. 'clusters.form.serverUrl.tips': 'Specify an externally accessible GPUStack service URL if the worker cannot access GPUStack directly.', // 82. 'clusters.addworker.externalIP.tips': 'Specify an external IP if the worker is in a VPC or private network.', // 83. 'clusters.form.setDefault': 'Set as Default', // 84. 'clusters.form.setDefault.tips': 'Default for deployment', diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index 7bec35bd..a15c1306 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -102,7 +102,7 @@ export default { '{count} новый воркер был добавлен в кластер.', 'clusters.addworker.message.success_multiple': '{count} новых воркеров были добавлены в кластер.', - 'clusters.create.serverUrl': 'URL сервера', + 'clusters.create.serverUrl': 'URL сервера GPUStack', 'clusters.create.workerConfig': 'Конфигурация воркера', 'clusters.addworker.containerName': 'Имя контейнера воркера', 'clusters.addworker.containerName.tips': @@ -113,7 +113,7 @@ export default { 'clusters.table.ip.internal': 'Внутренний', 'clusters.table.ip.external': 'Внешний', 'clusters.form.serverUrl.tips': - 'Укажите URL сервера, доступный из вашего облачного провайдера.', + 'Если рабочий узел не может напрямую получить доступ к GPUStack, укажите внешний URL службы GPUStack.', 'clusters.form.setDefault': 'Установить по умолчанию', 'clusters.form.setDefault.tips': 'Использовать по умолчанию для развертывания.', diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index e2dbaa7e..946b9a73 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -102,7 +102,7 @@ export default { '已将 {count} 个新节点添加到集群中。', 'clusters.addworker.message.success_multiple': '已将 {count} 个新节点添加到集群中。', - 'clusters.create.serverUrl': '服务器地址', + 'clusters.create.serverUrl': 'GPUStack 服务地址', 'clusters.create.workerConfig': '节点配置', 'clusters.addworker.containerName': '节点容器名称', 'clusters.addworker.containerName.tips': '为节点容器指定一个名称。', @@ -110,7 +110,8 @@ export default { 'clusters.addworker.dataVolume.tips': '为 GPUStack 指定数据存储路径。', 'clusters.table.ip.internal': '内', 'clusters.table.ip.external': '外', - 'clusters.form.serverUrl.tips': '指定可从您的云服务提供商访问的服务器地址。', + 'clusters.form.serverUrl.tips': + '如果节点无法直接访问 GPUStack,则指定一个可访问的外部 GPUStack 服务地址。', 'clusters.form.setDefault': '设为默认', 'clusters.form.setDefault.tips': '部署时的默认集群。', 'clusters.addworker.noClusters': '无可用的 Docker 集群', diff --git a/src/pages/_components/form-drawer.tsx b/src/pages/_components/form-drawer.tsx index 0a25aaa4..4b1d1758 100644 --- a/src/pages/_components/form-drawer.tsx +++ b/src/pages/_components/form-drawer.tsx @@ -1,6 +1,8 @@ import ModalFooter from '@/components/modal-footer'; import GSDrawer from '@/components/scroller-modal/gs-drawer'; import ColumnWrapper from '@/pages/_components/column-wrapper'; +import { Tag } from 'antd'; +import React from 'react'; const ModalFooterStyle = { padding: '16px 24px 8px', @@ -9,12 +11,14 @@ const ModalFooterStyle = { }; type AddModalProps = { - title: string; + title: React.ReactNode; open: boolean; - onCancel: () => void; + onCancel?: () => void; children?: React.ReactNode; - onSubmit: () => void; - width?: number; + onSubmit?: () => void; + width?: number | string; + footer?: React.ReactNode; + subTitle?: React.ReactNode; }; const FormDrawer: React.FC = ({ title, @@ -22,11 +26,32 @@ const FormDrawer: React.FC = ({ onCancel, onSubmit, children, - width = 600 + width = 600, + subTitle, + footer }) => { return ( + {title} + {subTitle && ( + + {subTitle} + + )} + + } open={open} onClose={onCancel} destroyOnHidden={true} @@ -43,11 +68,13 @@ const FormDrawer: React.FC = ({ container: { paddingBlock: 0 } }} footer={ - + footer ?? ( + + ) } > {children} diff --git a/src/pages/api-keys/index.tsx b/src/pages/api-keys/index.tsx index 913bf4bd..1fe761e1 100644 --- a/src/pages/api-keys/index.tsx +++ b/src/pages/api-keys/index.tsx @@ -133,7 +133,7 @@ const APIKeys: React.FC = () => { handleClickPrimary={handleAddKey} handleInputChange={handleNameChange} rowSelection={rowSelection} - width={{ input: 300 }} + widths={{ input: 300 }} > { Math.min(prev + 1, steps.length - 1)); } } catch (error) { diff --git a/src/pages/cluster-management/cluster-workers-modal.tsx b/src/pages/cluster-management/cluster-workers-modal.tsx new file mode 100644 index 00000000..08c8658e --- /dev/null +++ b/src/pages/cluster-management/cluster-workers-modal.tsx @@ -0,0 +1,60 @@ +import WorkerList from '@/pages/resources/components/workers'; +import { useIntl } from '@umijs/max'; +import React from 'react'; +import styled from 'styled-components'; +import FormDrawer from '../_components/form-drawer'; +import { ClusterListItem } from './config/types'; + +const Content = styled.div` + .ant-pagination { + position: fixed; + bottom: 8px; + right: 24px; + z-index: 100; + } + .page-tools { + position: sticky; + top: 0; + } +`; + +interface ClusterWorkersModalProps { + cluster: ClusterListItem; + open: boolean; + onClose: () => void; +} + +const ClusterWorkersModal: React.FC = ({ + open, + onClose, + cluster +}) => { + const intl = useIntl(); + return ( + + } + width={'calc(100vw - 220px)'} + > + + + + + ); +}; + +export default ClusterWorkersModal; diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index 0bf6e555..6579e05a 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -31,6 +31,7 @@ import { WORKER_POOLS_API } from './apis'; import ClusterModal from './cluster-modal'; +import ClusterWorkersModal from './cluster-workers-modal'; import AddCluster from './components/add-cluster'; import AddPool from './components/add-pool'; import { @@ -48,6 +49,7 @@ import { } from './config/types'; import useAddWorker from './hooks/use-add-worker'; import useClusterColumns from './hooks/use-cluster-columns'; +import { useClusterWorkers } from './hooks/use-cluster-workers'; import useCreateCluster from './hooks/use-create-cluster'; const Clusters: React.FC = () => { @@ -82,6 +84,11 @@ const Clusters: React.FC = () => { useCreateCluster({ refresh: handleSearch }); + const { + clusterWorkersModalStatus, + openClusterWorkersModal, + closeClusterWorkersModal + } = useClusterWorkers(); const [openAddModal, setOpenAddModal] = useState<{ open: boolean; @@ -311,7 +318,7 @@ const Clusters: React.FC = () => { ); }; - const columns = useClusterColumns(handleSelect); + const columns = useClusterColumns(handleSelect, openClusterWorkersModal); return ( <> @@ -320,7 +327,7 @@ const Clusters: React.FC = () => { showSelect={false} marginBottom={22} marginTop={30} - width={{ input: 300 }} + widths={{ input: 300 }} buttonText={intl.formatMessage({ id: 'clusters.button.add' })} rowSelection={rowSelection} handleInputChange={handleNameChange} @@ -416,6 +423,11 @@ const Clusters: React.FC = () => { open={clusterModalStatus.open} onClose={closeClusterModal} > + {AddWorkerModal} ); diff --git a/src/pages/cluster-management/components/add-worker/index.tsx b/src/pages/cluster-management/components/add-worker/index.tsx index 9ff7adb6..cf48e10e 100644 --- a/src/pages/cluster-management/components/add-worker/index.tsx +++ b/src/pages/cluster-management/components/add-worker/index.tsx @@ -27,7 +27,7 @@ const Footer = styled.div` type AddWorkerProps = { open: boolean; provider: ProviderType; - title: string; + title: React.ReactNode; clusterList?: Global.BaseOption[]; clusterLoading?: boolean; stepList: StepName[]; diff --git a/src/pages/cluster-management/components/pool-form.tsx b/src/pages/cluster-management/components/pool-form.tsx index 93de832e..eec1278f 100644 --- a/src/pages/cluster-management/components/pool-form.tsx +++ b/src/pages/cluster-management/components/pool-form.tsx @@ -310,6 +310,11 @@ const PoolForm: React.FC = forwardRef((props, ref) => { title={title} collapsible={collapsible} onToggle={onToggle} + styles={{ + root: { + background: 'unset' + } + }} deleteBtn={ showDelete && (
{ dataSource={dataSource.dataList} loading={dataSource.loading} rowKey="id" + scroll={{ x: true }} onChange={handleTableChange} pagination={{ showSizeChanger: true, diff --git a/src/pages/resources/components/worker-detail-content.tsx b/src/pages/resources/components/worker-detail-content.tsx index df72a066..9f6ae882 100644 --- a/src/pages/resources/components/worker-detail-content.tsx +++ b/src/pages/resources/components/worker-detail-content.tsx @@ -95,7 +95,7 @@ const WorkerDetailContent: React.FC<{ worker_id: number | undefined }> = ({ selectHolder={intl.formatMessage({ id: 'resources.filter.status' })} showSelect={true} selectOptions={statusList} - width={{ input: 300 }} + widths={{ input: 300 }} >
{ +const Workers: React.FC<{ + cluster: ClusterListItem; + showSelect?: boolean; + showAddButton?: boolean; + widths?: { input: number }; + sourceType?: string; +}> = ({ + cluster, + showSelect = true, + showAddButton = true, + widths = { input: 200 }, + sourceType = 'resources' +}) => { const { dataSource, rowSelection, @@ -48,7 +60,10 @@ const Workers: React.FC = () => { events: ['UPDATE', 'DELETE', 'INSERT'], contentForDelete: 'resources.worker', watch: true, - API: WORKERS_API + API: WORKERS_API, + defaultQueryParams: { + cluster_id: cluster?.id || undefined + } }); const { TerminalPanel, terminals, handleAddTerminal } = useTerminalTabs(); const { MaintenanceModal, handleStopMaintenance, setOpenStatus } = @@ -233,7 +248,8 @@ const Workers: React.FC = () => { loadend: dataSource.loadend, firstLoad: extraStatus.firstLoad, sortOrder, - handleSelect + handleSelect, + sourceType }); useEffect(() => { @@ -245,7 +261,7 @@ const Workers: React.FC = () => { <> { handleDeleteByBatch={handleDeleteBatch} handleSearch={handleSearch} handleSelectChange={handleClusterChange} - handleClickPrimary={handleOnAddWorker} + handleClickPrimary={showAddButton ? handleOnAddWorker : undefined} handleInputChange={handleNameChange} rowSelection={rowSelection} selectOptions={clusterData.list} - width={{ input: 200 }} + widths={widths} >
{ dataSource={dataSource.dataList} loading={dataSource.loading} rowKey="id" + scroll={{ x: true }} onChange={handleTableChange} rowSelection={rowSelection} pagination={{ diff --git a/src/pages/resources/hooks/use-worker-columns.tsx b/src/pages/resources/hooks/use-worker-columns.tsx index bf0994ed..3902f3a0 100644 --- a/src/pages/resources/hooks/use-worker-columns.tsx +++ b/src/pages/resources/hooks/use-worker-columns.tsx @@ -232,6 +232,7 @@ const useWorkerColumns = ({ loadend, firstLoad, sortOrder, + sourceType, handleSelect }: { clusterData: { @@ -241,6 +242,7 @@ const useWorkerColumns = ({ loadend: boolean; firstLoad: boolean; sortOrder: string[]; + sourceType: string; handleSelect: (action: string, record: ListItem) => void; }): ColumnsType => { const intl = useIntl(); @@ -272,7 +274,10 @@ const useWorkerColumns = ({ return useMemo>( () => [ { - title: intl.formatMessage({ id: 'common.table.name' }), + title: + sourceType === 'resources' + ? intl.formatMessage({ id: 'common.table.name' }) + : intl.formatMessage({ id: 'resources.table.workername' }), dataIndex: 'name', width: 100, sorter: tableSorter(1), @@ -291,6 +296,7 @@ const useWorkerColumns = ({ { title: intl.formatMessage({ id: 'clusters.title' }), dataIndex: 'cluster_id', + hidden: sourceType === 'cluster', render: (id: number) => ( {_.get(clusterData.data, id, '')} @@ -402,7 +408,7 @@ const useWorkerColumns = ({ ) } ], - [intl, sortOrder, clusterData, loadend, firstLoad, handleSelect] + [intl, sourceType, sortOrder, clusterData, loadend, firstLoad, handleSelect] ); }; diff --git a/src/pages/users/index.tsx b/src/pages/users/index.tsx index 10c6f3dd..2e8191dd 100644 --- a/src/pages/users/index.tsx +++ b/src/pages/users/index.tsx @@ -182,7 +182,7 @@ const Users: React.FC = () => { handleClickPrimary={handleAddUser} handleInputChange={handleNameChange} rowSelection={rowSelection} - width={{ input: 300 }} + widths={{ input: 300 }} >