feat: add cluster detail

This commit is contained in:
jialin
2026-01-09 20:51:48 +08:00
parent 7b5ce83cbd
commit afcd3e5693
32 changed files with 255 additions and 139 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
.cardWrapper { .cardWrapper {
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
background-color: var(--color-white-1); background-color: var(--ant-color-bg-container);
box-shadow: none; box-shadow: none;
padding: 10px 16px; padding: 10px 16px;
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
@@ -88,6 +88,7 @@ export interface CollapsibleContainerProps {
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
styles?: { styles?: {
root?: React.CSSProperties;
body?: React.CSSProperties; body?: React.CSSProperties;
header?: React.CSSProperties; header?: React.CSSProperties;
content?: React.CSSProperties; content?: React.CSSProperties;
@@ -191,6 +192,9 @@ export default function CollapsibleContainer({
className={classNames(className, { collapsible, disabled, isOpen })} className={classNames(className, { collapsible, disabled, isOpen })}
variant={variant} variant={variant}
styles={{ styles={{
root: {
...cardStyles?.root
},
body: { body: {
padding: 0, padding: 0,
...cardStyles?.body ...cardStyles?.body
+5 -4
View File
@@ -55,6 +55,7 @@ interface ActionItem {
icon: React.ReactNode; icon: React.ReactNode;
[key: string]: any; [key: string]: any;
} }
interface FilterBarProps { interface FilterBarProps {
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void; handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSelectChange?: (value: any) => void; handleSelectChange?: (value: any) => void;
@@ -76,7 +77,7 @@ interface FilterBarProps {
showDeleteButton?: boolean; showDeleteButton?: boolean;
right?: React.ReactNode; right?: React.ReactNode;
left?: React.ReactNode; left?: React.ReactNode;
width?: { widths?: {
input?: number; input?: number;
select?: number; select?: number;
}; };
@@ -102,7 +103,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
selectHolder, selectHolder,
right, right,
left, left,
width widths
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -181,7 +182,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
id: 'common.filter.name' id: 'common.filter.name'
}) })
} }
style={{ width: width?.input || 230 }} style={{ width: widths?.input || 230 }}
allowClear allowClear
onChange={handleInputChange} onChange={handleInputChange}
></Input> ></Input>
@@ -190,7 +191,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
allowClear allowClear
showSearch={false} showSearch={false}
placeholder={selectHolder} placeholder={selectHolder}
style={{ width: width?.select || 230 }} style={{ width: widths?.select || 230 }}
size="large" size="large"
onChange={handleSelectChange} onChange={handleSelectChange}
options={selectOptions} options={selectOptions}
+2 -2
View File
@@ -97,7 +97,7 @@ export default {
borderRadiusSM: 2, borderRadiusSM: 2,
fontSize: 14, fontSize: 14,
motion: true, motion: true,
colorFill: '#000', colorFill: '#0A0A0A',
colorBgBase: '#000' colorBgBase: '#0A0A0A'
} }
}; };
+1 -1
View File
@@ -89,7 +89,7 @@ export default {
darkMode: false, darkMode: false,
fontFamily: fontFamily:
"Helvetica Neue, -apple-system, BlinkMacSystemFont, Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", "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, colorPrimary: COLOR_PRIMARY,
colorSuccess: '#54cc98', colorSuccess: '#54cc98',
colorBorder: '#d3d0d9', colorBorder: '#d3d0d9',
+1 -1
View File
@@ -103,7 +103,7 @@ html[data-theme='realDark'] {
--color-spotlight-bg: #3e3e3e; --color-spotlight-bg: #3e3e3e;
--color-esc-hint-bg: rgba(150, 150, 150, 75%); --color-esc-hint-bg: rgba(150, 150, 150, 75%);
background: #141414; background: #292929;
.ant-result-image { .ant-result-image {
opacity: 0.9; opacity: 0.9;
+2 -2
View File
@@ -102,7 +102,7 @@ export default {
'{count} new worker has been added to the cluster.', '{count} new worker has been added to the cluster.',
'clusters.addworker.message.success_multiple': 'clusters.addworker.message.success_multiple':
'{count} new workers have been added to the cluster.', '{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.create.workerConfig': 'Worker Configuration',
'clusters.addworker.containerName': 'Worker Container Name', 'clusters.addworker.containerName': 'Worker Container Name',
'clusters.addworker.containerName.tips': 'clusters.addworker.containerName.tips':
@@ -113,7 +113,7 @@ export default {
'clusters.table.ip.internal': 'Internal', 'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External', 'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips': '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': 'Set as Default',
'clusters.form.setDefault.tips': 'Default for deployment.', 'clusters.form.setDefault.tips': 'Default for deployment.',
'clusters.addworker.noClusters': 'No available Docker clusters found', 'clusters.addworker.noClusters': 'No available Docker clusters found',
+4 -4
View File
@@ -102,7 +102,7 @@ export default {
'{count} new worker has been added to the cluster.', '{count} new worker has been added to the cluster.',
'clusters.addworker.message.success_multiple': 'clusters.addworker.message.success_multiple':
'{count} new workers have been added to the cluster.', '{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.create.workerConfig': 'Worker Configuration',
'clusters.addworker.containerName': 'Worker Container Name', 'clusters.addworker.containerName': 'Worker Container Name',
'clusters.addworker.containerName.tips': 'clusters.addworker.containerName.tips':
@@ -113,7 +113,7 @@ export default {
'clusters.table.ip.internal': 'Internal', 'clusters.table.ip.internal': 'Internal',
'clusters.table.ip.external': 'External', 'clusters.table.ip.external': 'External',
'clusters.form.serverUrl.tips': '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': 'Set as Default',
'clusters.form.setDefault.tips': 'Default for deployment.', 'clusters.form.setDefault.tips': 'Default for deployment.',
'clusters.addworker.noClusters': 'No available Docker clusters found', '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 /)', // 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.', // 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.', // 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' // 77. 'clusters.create.workerConfig': 'Worker Configuration'
// 78. 'clusters.addworker.containerName': 'Worker Container Name', // 78. 'clusters.addworker.containerName': 'Worker Container Name',
// 79. 'clusters.addworker.containerName.tips':'Specify a name for the worker container.', // 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.', // 78. 'clusters.addworker.dataVolume.tips': 'Specify a data storage path for GPUStack.',
// 79. 'clusters.table.ip.internal': 'Internal', // 79. 'clusters.table.ip.internal': 'Internal',
// 80. 'clusters.table.ip.external': 'External', // 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.', // 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', // 83. 'clusters.form.setDefault': 'Set as Default',
// 84. 'clusters.form.setDefault.tips': 'Default for deployment', // 84. 'clusters.form.setDefault.tips': 'Default for deployment',
+2 -2
View File
@@ -102,7 +102,7 @@ export default {
'{count} новый воркер был добавлен в кластер.', '{count} новый воркер был добавлен в кластер.',
'clusters.addworker.message.success_multiple': 'clusters.addworker.message.success_multiple':
'{count} новых воркеров были добавлены в кластер.', '{count} новых воркеров были добавлены в кластер.',
'clusters.create.serverUrl': 'URL сервера', 'clusters.create.serverUrl': 'URL сервера GPUStack',
'clusters.create.workerConfig': 'Конфигурация воркера', 'clusters.create.workerConfig': 'Конфигурация воркера',
'clusters.addworker.containerName': 'Имя контейнера воркера', 'clusters.addworker.containerName': 'Имя контейнера воркера',
'clusters.addworker.containerName.tips': 'clusters.addworker.containerName.tips':
@@ -113,7 +113,7 @@ export default {
'clusters.table.ip.internal': 'Внутренний', 'clusters.table.ip.internal': 'Внутренний',
'clusters.table.ip.external': 'Внешний', 'clusters.table.ip.external': 'Внешний',
'clusters.form.serverUrl.tips': 'clusters.form.serverUrl.tips':
'Укажите URL сервера, доступный из вашего облачного провайдера.', 'Если рабочий узел не может напрямую получить доступ к GPUStack, укажите внешний URL службы GPUStack.',
'clusters.form.setDefault': 'Установить по умолчанию', 'clusters.form.setDefault': 'Установить по умолчанию',
'clusters.form.setDefault.tips': 'clusters.form.setDefault.tips':
'Использовать по умолчанию для развертывания.', 'Использовать по умолчанию для развертывания.',
+3 -2
View File
@@ -102,7 +102,7 @@ export default {
'已将 {count} 个新节点添加到集群中。', '已将 {count} 个新节点添加到集群中。',
'clusters.addworker.message.success_multiple': 'clusters.addworker.message.success_multiple':
'已将 {count} 个新节点添加到集群中。', '已将 {count} 个新节点添加到集群中。',
'clusters.create.serverUrl': '服务地址', 'clusters.create.serverUrl': 'GPUStack 服务地址',
'clusters.create.workerConfig': '节点配置', 'clusters.create.workerConfig': '节点配置',
'clusters.addworker.containerName': '节点容器名称', 'clusters.addworker.containerName': '节点容器名称',
'clusters.addworker.containerName.tips': '为节点容器指定一个名称。', 'clusters.addworker.containerName.tips': '为节点容器指定一个名称。',
@@ -110,7 +110,8 @@ export default {
'clusters.addworker.dataVolume.tips': '为 GPUStack 指定数据存储路径。', 'clusters.addworker.dataVolume.tips': '为 GPUStack 指定数据存储路径。',
'clusters.table.ip.internal': '内', 'clusters.table.ip.internal': '内',
'clusters.table.ip.external': '外', 'clusters.table.ip.external': '外',
'clusters.form.serverUrl.tips': '指定可从您的云服务提供商访问的服务器地址。', 'clusters.form.serverUrl.tips':
'如果节点无法直接访问 GPUStack,则指定一个可访问的外部 GPUStack 服务地址。',
'clusters.form.setDefault': '设为默认', 'clusters.form.setDefault': '设为默认',
'clusters.form.setDefault.tips': '部署时的默认集群。', 'clusters.form.setDefault.tips': '部署时的默认集群。',
'clusters.addworker.noClusters': '无可用的 Docker 集群', 'clusters.addworker.noClusters': '无可用的 Docker 集群',
+38 -11
View File
@@ -1,6 +1,8 @@
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import GSDrawer from '@/components/scroller-modal/gs-drawer'; import GSDrawer from '@/components/scroller-modal/gs-drawer';
import ColumnWrapper from '@/pages/_components/column-wrapper'; import ColumnWrapper from '@/pages/_components/column-wrapper';
import { Tag } from 'antd';
import React from 'react';
const ModalFooterStyle = { const ModalFooterStyle = {
padding: '16px 24px 8px', padding: '16px 24px 8px',
@@ -9,12 +11,14 @@ const ModalFooterStyle = {
}; };
type AddModalProps = { type AddModalProps = {
title: string; title: React.ReactNode;
open: boolean; open: boolean;
onCancel: () => void; onCancel?: () => void;
children?: React.ReactNode; children?: React.ReactNode;
onSubmit: () => void; onSubmit?: () => void;
width?: number; width?: number | string;
footer?: React.ReactNode;
subTitle?: React.ReactNode;
}; };
const FormDrawer: React.FC<AddModalProps> = ({ const FormDrawer: React.FC<AddModalProps> = ({
title, title,
@@ -22,11 +26,32 @@ const FormDrawer: React.FC<AddModalProps> = ({
onCancel, onCancel,
onSubmit, onSubmit,
children, children,
width = 600 width = 600,
subTitle,
footer
}) => { }) => {
return ( return (
<GSDrawer <GSDrawer
title={title} title={
<>
{title}
{subTitle && (
<Tag
variant="outlined"
style={{
fontSize: 12,
fontWeight: 400,
marginLeft: 8,
borderRadius: 4,
borderColor: 'var(--ant-color-border-secondary)',
color: 'var(--ant-color-text-secondary)'
}}
>
{subTitle}
</Tag>
)}
</>
}
open={open} open={open}
onClose={onCancel} onClose={onCancel}
destroyOnHidden={true} destroyOnHidden={true}
@@ -43,11 +68,13 @@ const FormDrawer: React.FC<AddModalProps> = ({
container: { paddingBlock: 0 } container: { paddingBlock: 0 }
}} }}
footer={ footer={
<ModalFooter footer ?? (
onOk={onSubmit} <ModalFooter
onCancel={onCancel} onOk={onSubmit}
style={ModalFooterStyle} onCancel={onCancel}
></ModalFooter> style={ModalFooterStyle}
></ModalFooter>
)
} }
> >
{children} {children}
+1 -1
View File
@@ -133,7 +133,7 @@ const APIKeys: React.FC = () => {
handleClickPrimary={handleAddKey} handleClickPrimary={handleAddKey}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
rowSelection={rowSelection} rowSelection={rowSelection}
width={{ input: 300 }} widths={{ input: 300 }}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table
+1 -1
View File
@@ -181,7 +181,7 @@ const BackendList = () => {
<FilterBar <FilterBar
marginBottom={22} marginBottom={22}
marginTop={30} marginTop={30}
width={{ widths={{
input: 300 input: 300
}} }}
inputHolder={intl.formatMessage({ id: 'common.filter.name' })} inputHolder={intl.formatMessage({ id: 'common.filter.name' })}
@@ -171,7 +171,6 @@ const ClusterCreate: React.FC<{
if (result) { if (result) {
await callback?.(result as ClusterFormData); await callback?.(result as ClusterFormData);
const step = steps[currentStep]; const step = steps[currentStep];
step.beforeNext?.();
setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1)); setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1));
} }
} catch (error) { } catch (error) {
@@ -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<ClusterWorkersModalProps> = ({
open,
onClose,
cluster
}) => {
const intl = useIntl();
return (
<FormDrawer
title={`${intl.formatMessage({ id: 'clusters.title' })}: ${cluster.name}`}
open={open}
onCancel={onClose}
footer={
<div
style={{
height: 56
}}
></div>
}
width={'calc(100vw - 220px)'}
>
<Content>
<WorkerList
cluster={cluster}
showAddButton={false}
showSelect={false}
widths={{ input: 360 }}
sourceType="cluster"
/>
</Content>
</FormDrawer>
);
};
export default ClusterWorkersModal;
+14 -2
View File
@@ -31,6 +31,7 @@ import {
WORKER_POOLS_API WORKER_POOLS_API
} from './apis'; } from './apis';
import ClusterModal from './cluster-modal'; import ClusterModal from './cluster-modal';
import ClusterWorkersModal from './cluster-workers-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 {
@@ -48,6 +49,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 { useClusterWorkers } from './hooks/use-cluster-workers';
import useCreateCluster from './hooks/use-create-cluster'; import useCreateCluster from './hooks/use-create-cluster';
const Clusters: React.FC = () => { const Clusters: React.FC = () => {
@@ -82,6 +84,11 @@ const Clusters: React.FC = () => {
useCreateCluster({ useCreateCluster({
refresh: handleSearch refresh: handleSearch
}); });
const {
clusterWorkersModalStatus,
openClusterWorkersModal,
closeClusterWorkersModal
} = useClusterWorkers();
const [openAddModal, setOpenAddModal] = useState<{ const [openAddModal, setOpenAddModal] = useState<{
open: boolean; open: boolean;
@@ -311,7 +318,7 @@ const Clusters: React.FC = () => {
); );
}; };
const columns = useClusterColumns(handleSelect); const columns = useClusterColumns(handleSelect, openClusterWorkersModal);
return ( return (
<> <>
@@ -320,7 +327,7 @@ const Clusters: React.FC = () => {
showSelect={false} showSelect={false}
marginBottom={22} marginBottom={22}
marginTop={30} marginTop={30}
width={{ input: 300 }} widths={{ input: 300 }}
buttonText={intl.formatMessage({ id: 'clusters.button.add' })} buttonText={intl.formatMessage({ id: 'clusters.button.add' })}
rowSelection={rowSelection} rowSelection={rowSelection}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
@@ -416,6 +423,11 @@ const Clusters: React.FC = () => {
open={clusterModalStatus.open} open={clusterModalStatus.open}
onClose={closeClusterModal} onClose={closeClusterModal}
></ClusterModal> ></ClusterModal>
<ClusterWorkersModal
open={clusterWorkersModalStatus.open}
onClose={closeClusterWorkersModal}
cluster={clusterWorkersModalStatus.cluster}
></ClusterWorkersModal>
{AddWorkerModal} {AddWorkerModal}
</> </>
); );
@@ -27,7 +27,7 @@ const Footer = styled.div`
type AddWorkerProps = { type AddWorkerProps = {
open: boolean; open: boolean;
provider: ProviderType; provider: ProviderType;
title: string; title: React.ReactNode;
clusterList?: Global.BaseOption<number, ClusterListItem>[]; clusterList?: Global.BaseOption<number, ClusterListItem>[];
clusterLoading?: boolean; clusterLoading?: boolean;
stepList: StepName[]; stepList: StepName[];
@@ -310,6 +310,11 @@ const PoolForm: React.FC<AddModalProps> = forwardRef((props, ref) => {
title={title} title={title}
collapsible={collapsible} collapsible={collapsible}
onToggle={onToggle} onToggle={onToggle}
styles={{
root: {
background: 'unset'
}
}}
deleteBtn={ deleteBtn={
showDelete && ( showDelete && (
<Button <Button
@@ -138,7 +138,7 @@ const WorkerPools = () => {
showSelect={false} showSelect={false}
marginBottom={22} marginBottom={22}
marginTop={22} marginTop={22}
width={{ input: 300 }} widths={{ input: 300 }}
buttonText={intl.formatMessage({ id: 'clusters.button.addNodePool' })} buttonText={intl.formatMessage({ id: 'clusters.button.addNodePool' })}
rowSelection={rowSelection} rowSelection={rowSelection}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
+1 -1
View File
@@ -196,7 +196,7 @@ const Credentials: React.FC = () => {
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
handleClickPrimary={handleAddCredential} handleClickPrimary={handleAddCredential}
rowSelection={rowSelection} rowSelection={rowSelection}
width={{ input: 300 }} widths={{ input: 300 }}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
@@ -1,7 +1,7 @@
import AddWorker from '@/pages/cluster-management/components/add-worker'; import AddWorker from '@/pages/cluster-management/components/add-worker';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { message } from 'antd'; import { message, Tag } from 'antd';
import { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { StepName } from '../components/add-worker/config'; import { StepName } from '../components/add-worker/config';
import { import {
ClusterStatusValueMap, ClusterStatusValueMap,
@@ -20,7 +20,7 @@ const useAddWorker = (props: {
const [openAddWorker, setOpenAddWorker] = useState<{ const [openAddWorker, setOpenAddWorker] = useState<{
open: boolean; open: boolean;
provider: ProviderType; provider: ProviderType;
title: string; title: React.ReactNode;
cluster_id: number | null; cluster_id: number | null;
}>({ }>({
open: false, open: false,
@@ -56,7 +56,26 @@ const useAddWorker = (props: {
setOpenAddWorker({ setOpenAddWorker({
open: true, open: true,
title: title, title: (
<span>
{title}
{row.name && (
<Tag
variant="outlined"
style={{
fontSize: 12,
fontWeight: 400,
marginLeft: 8,
borderRadius: 4,
borderColor: 'var(--ant-color-border-secondary)',
color: 'var(--ant-color-text-secondary)'
}}
>
{intl.formatMessage({ id: 'clusters.title' })}: {row.name}
</Tag>
)}
</span>
),
provider: row.provider as ProviderType, provider: row.provider as ProviderType,
cluster_id: clusterId cluster_id: clusterId
}); });
@@ -6,7 +6,7 @@ import StatusTag from '@/components/status-tag';
import { tableSorter } from '@/config/settings'; import { tableSorter } from '@/config/settings';
import { StarFilled } from '@ant-design/icons'; import { StarFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Tooltip } from 'antd'; import { Tooltip, Typography } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import {
@@ -27,7 +27,8 @@ const setActionsItems = (row: ClusterListItem) => {
}; };
const useClusterColumns = ( const useClusterColumns = (
handleSelect: (val: string, record: ClusterListItem) => void handleSelect: (val: string, record: ClusterListItem) => void,
onViewWorkers: (record: ClusterListItem) => void
): SealColumnProps[] => { ): SealColumnProps[] => {
const intl = useIntl(); const intl = useIntl();
@@ -40,7 +41,15 @@ const useClusterColumns = (
span: 3, span: 3,
render: (text: string, record: ClusterListItem) => ( render: (text: string, record: ClusterListItem) => (
<> <>
<AutoTooltip ghost>{text}</AutoTooltip> <AutoTooltip ghost title={text}>
{record.workers > 0 ? (
<Typography.Link onClick={() => onViewWorkers(record)}>
{record.name}
</Typography.Link>
) : (
text
)}
</AutoTooltip>
{record.is_default && ( {record.is_default && (
<Tooltip <Tooltip
title={intl.formatMessage({ title={intl.formatMessage({
@@ -127,7 +136,7 @@ const useClusterColumns = (
) )
} }
]; ];
}, [handleSelect]); }, [handleSelect, onViewWorkers]);
}; };
export default useClusterColumns; export default useClusterColumns;
@@ -0,0 +1,32 @@
import { useState } from 'react';
import { ClusterListItem } from '../config/types';
export const useClusterWorkers = () => {
const [clusterWorkersModalStatus, setClusterWorkersModalStatus] = useState<{
open: boolean;
cluster: ClusterListItem;
}>({
open: false,
cluster: {} as ClusterListItem
});
const openClusterWorkersModal = (cluster: ClusterListItem) => {
setClusterWorkersModalStatus({
open: true,
cluster
});
};
const closeClusterWorkersModal = () => {
setClusterWorkersModalStatus({
open: false,
cluster: {} as ClusterListItem
});
};
return {
clusterWorkersModalStatus,
openClusterWorkersModal,
closeClusterWorkersModal
};
};
@@ -3,7 +3,6 @@ import DropdownButtons from '@/components/drop-down-buttons';
import { SealColumnProps } from '@/components/seal-table/types'; import { SealColumnProps } from '@/components/seal-table/types';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import type { SortOrder } from 'antd/es/table/interface';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -29,7 +28,7 @@ const actionItems = [
const usePoolsColumns = ( const usePoolsColumns = (
handleSelect: (val: string, record: ListItem) => void, handleSelect: (val: string, record: ListItem) => void,
sortOrder?: SortOrder sortOrder?: string[]
): SealColumnProps[] => { ): SealColumnProps[] => {
const intl = useIntl(); const intl = useIntl();
@@ -83,7 +82,6 @@ const usePoolsColumns = (
}} }}
/> />
} }
SHOWTITLE
ghost ghost
minWidth={20} minWidth={20}
> >
@@ -162,7 +160,6 @@ const usePoolsColumns = (
key: 'created_at', key: 'created_at',
span: 4, span: 4,
showSorterTootip: false, showSorterTootip: false,
sortOrder: sortOrder,
sorter: false, sorter: false,
ellipsis: { ellipsis: {
showTitle: false showTitle: false
@@ -192,7 +189,7 @@ const usePoolsColumns = (
) )
} }
]; ];
}, [sortOrder, handleSelect]); }, [handleSelect]);
}; };
export default usePoolsColumns; export default usePoolsColumns;
+1 -5
View File
@@ -14,11 +14,7 @@ const renderCardItem = (data: {
}) => { }) => {
const { label, value, bgColor } = data; const { label, value, bgColor } = data;
return ( return (
<Card <Card variant="borderless" className={styles['card-body']}>
variant="borderless"
style={{ background: 'var(--color-white-1)' }}
className={styles['card-body']}
>
<div className={styles.content}> <div className={styles.content}>
<div className="label text-secondary">{label}</div> <div className="label text-secondary">{label}</div>
<div className="value font-600 font-size-16">{value}</div> <div className="value font-600 font-size-16">{value}</div>
+1 -1
View File
@@ -138,7 +138,7 @@ const Catalog: React.FC = () => {
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
selectOptions={categoryOptions} selectOptions={categoryOptions}
buttonIcon={<SearchOutlined />} buttonIcon={<SearchOutlined />}
width={{ input: 230, select: 200 }} widths={{ input: 230, select: 200 }}
></FilterBar> ></FilterBar>
<ScrollerContext.Provider <ScrollerContext.Provider
value={{ value={{
@@ -1,7 +1,6 @@
import { getRequestId } from '@/atoms/models'; import { getRequestId } from '@/atoms/models';
import BaseSelect from '@/components/seal-form/base/select'; import BaseSelect from '@/components/seal-form/base/select';
import SimpleOverlay from '@/components/simple-overlay'; import SimpleOverlay from '@/components/simple-overlay';
import { createAxiosToken } from '@/hooks/use-chunk-request';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Empty, Spin } from 'antd'; import { Empty, Spin } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
@@ -14,13 +13,8 @@ import React, {
} from 'react'; } from 'react';
import 'simplebar-react/dist/simplebar.min.css'; import 'simplebar-react/dist/simplebar.min.css';
import styled from 'styled-components'; import styled from 'styled-components';
import { import { queryHuggingfaceModelFiles, queryModelScopeModelFiles } from '../apis';
evaluationsModelSpec,
queryHuggingfaceModelFiles,
queryModelScopeModelFiles
} from '../apis';
import { modelSourceMap } from '../config'; import { modelSourceMap } from '../config';
import { backendOptionsMap } from '../config/backend-parameters';
import '../style/hf-model-file.less'; import '../style/hf-model-file.less';
import FileSkeleton from './file-skeleton'; import FileSkeleton from './file-skeleton';
import ModelFileItem from './model-file-item'; import ModelFileItem from './model-file-item';
@@ -226,70 +220,6 @@ const HFModelFile: React.FC<HFModelFileProps> = forwardRef((props, ref) => {
} }
}; };
const getEvaluateResults = async (repoList: any[]) => {
try {
checkTokenRef.current?.cancel?.();
checkTokenRef.current = createAxiosToken();
const evaluations = await evaluationsModelSpec(
{
model_specs: repoList
},
{
token: checkTokenRef.current.token
}
);
return evaluations.results || [];
} catch (error) {
return [];
}
};
const handleEvaluate = async (list: any[]) => {
if (isDownload) {
return;
}
try {
const evaluateFileList = list.map((item: any) => {
// TODO: llamabox is remove from backend options, user should select a available backend
return {
backend: backendOptionsMap.llamaBox,
source: modelSource,
...(modelSource === modelSourceMap.huggingface_value
? {
huggingface_repo_id: props.selectedModel.name,
huggingface_filename: item.fakeName
}
: {
model_scope_model_id: props.selectedModel.name,
model_scope_file_path: item.fakeName
})
};
});
setIsEvaluating(true);
const evaluationList = await getEvaluateResults(evaluateFileList);
const resultList = _.map(list, (item: any, index: number) => {
return {
...item,
evaluateResult: evaluationList[index]
};
});
const currentItem = _.find(
resultList,
(item: any) => item.path === currentPathRef.current
);
if (currentItem) {
onSelectFileAfterEvaluate?.(currentItem);
}
setDataSource({ fileList: resultList, loading: false });
setIsEvaluating(false);
} catch (error) {
setIsEvaluating(false);
}
};
const handleFetchModelFiles = async () => { const handleFetchModelFiles = async () => {
if (!props.selectedModel.name) { if (!props.selectedModel.name) {
setDataSource({ fileList: [], loading: false }); setDataSource({ fileList: [], loading: false });
+2 -1
View File
@@ -98,7 +98,7 @@ const GPUList: React.FC = () => {
handleSelectChange={handleClusterChange} handleSelectChange={handleClusterChange}
selectOptions={clusterList} selectOptions={clusterList}
showSelect showSelect
width={{ input: 200 }} widths={{ input: 200 }}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table
@@ -110,6 +110,7 @@ const GPUList: React.FC = () => {
dataSource={dataSource.dataList} dataSource={dataSource.dataList}
loading={dataSource.loading} loading={dataSource.loading}
rowKey="id" rowKey="id"
scroll={{ x: true }}
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ pagination={{
showSizeChanger: true, showSizeChanger: true,
@@ -95,7 +95,7 @@ const WorkerDetailContent: React.FC<{ worker_id: number | undefined }> = ({
selectHolder={intl.formatMessage({ id: 'resources.filter.status' })} selectHolder={intl.formatMessage({ id: 'resources.filter.status' })}
showSelect={true} showSelect={true}
selectOptions={statusList} selectOptions={statusList}
width={{ input: 300 }} widths={{ input: 300 }}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table
+23 -6
View File
@@ -26,7 +26,19 @@ import useWorkerMaintenance from '../hooks/use-worker-maintenance';
import UpdateLabels from './update-labels'; import UpdateLabels from './update-labels';
import WorkerDetailModal from './worker-detail-modal'; import WorkerDetailModal from './worker-detail-modal';
const Workers: React.FC = () => { 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 { const {
dataSource, dataSource,
rowSelection, rowSelection,
@@ -48,7 +60,10 @@ const Workers: React.FC = () => {
events: ['UPDATE', 'DELETE', 'INSERT'], events: ['UPDATE', 'DELETE', 'INSERT'],
contentForDelete: 'resources.worker', contentForDelete: 'resources.worker',
watch: true, watch: true,
API: WORKERS_API API: WORKERS_API,
defaultQueryParams: {
cluster_id: cluster?.id || undefined
}
}); });
const { TerminalPanel, terminals, handleAddTerminal } = useTerminalTabs(); const { TerminalPanel, terminals, handleAddTerminal } = useTerminalTabs();
const { MaintenanceModal, handleStopMaintenance, setOpenStatus } = const { MaintenanceModal, handleStopMaintenance, setOpenStatus } =
@@ -233,7 +248,8 @@ const Workers: React.FC = () => {
loadend: dataSource.loadend, loadend: dataSource.loadend,
firstLoad: extraStatus.firstLoad, firstLoad: extraStatus.firstLoad,
sortOrder, sortOrder,
handleSelect handleSelect,
sourceType
}); });
useEffect(() => { useEffect(() => {
@@ -245,7 +261,7 @@ const Workers: React.FC = () => {
<> <>
<PageBox> <PageBox>
<FilterBar <FilterBar
showSelect={true} showSelect={showSelect}
selectHolder={intl.formatMessage({ id: 'clusters.filterBy.cluster' })} selectHolder={intl.formatMessage({ id: 'clusters.filterBy.cluster' })}
marginBottom={22} marginBottom={22}
marginTop={30} marginTop={30}
@@ -253,11 +269,11 @@ const Workers: React.FC = () => {
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
handleSearch={handleSearch} handleSearch={handleSearch}
handleSelectChange={handleClusterChange} handleSelectChange={handleClusterChange}
handleClickPrimary={handleOnAddWorker} handleClickPrimary={showAddButton ? handleOnAddWorker : undefined}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
rowSelection={rowSelection} rowSelection={rowSelection}
selectOptions={clusterData.list} selectOptions={clusterData.list}
width={{ input: 200 }} widths={widths}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table
@@ -269,6 +285,7 @@ const Workers: React.FC = () => {
dataSource={dataSource.dataList} dataSource={dataSource.dataList}
loading={dataSource.loading} loading={dataSource.loading}
rowKey="id" rowKey="id"
scroll={{ x: true }}
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={{ pagination={{
@@ -232,6 +232,7 @@ const useWorkerColumns = ({
loadend, loadend,
firstLoad, firstLoad,
sortOrder, sortOrder,
sourceType,
handleSelect handleSelect
}: { }: {
clusterData: { clusterData: {
@@ -241,6 +242,7 @@ const useWorkerColumns = ({
loadend: boolean; loadend: boolean;
firstLoad: boolean; firstLoad: boolean;
sortOrder: string[]; sortOrder: string[];
sourceType: string;
handleSelect: (action: string, record: ListItem) => void; handleSelect: (action: string, record: ListItem) => void;
}): ColumnsType<ListItem> => { }): ColumnsType<ListItem> => {
const intl = useIntl(); const intl = useIntl();
@@ -272,7 +274,10 @@ const useWorkerColumns = ({
return useMemo<ColumnsType<ListItem>>( return useMemo<ColumnsType<ListItem>>(
() => [ () => [
{ {
title: intl.formatMessage({ id: 'common.table.name' }), title:
sourceType === 'resources'
? intl.formatMessage({ id: 'common.table.name' })
: intl.formatMessage({ id: 'resources.table.workername' }),
dataIndex: 'name', dataIndex: 'name',
width: 100, width: 100,
sorter: tableSorter(1), sorter: tableSorter(1),
@@ -291,6 +296,7 @@ const useWorkerColumns = ({
{ {
title: intl.formatMessage({ id: 'clusters.title' }), title: intl.formatMessage({ id: 'clusters.title' }),
dataIndex: 'cluster_id', dataIndex: 'cluster_id',
hidden: sourceType === 'cluster',
render: (id: number) => ( render: (id: number) => (
<AutoTooltip ghost maxWidth={240}> <AutoTooltip ghost maxWidth={240}>
{_.get(clusterData.data, id, '')} {_.get(clusterData.data, id, '')}
@@ -402,7 +408,7 @@ const useWorkerColumns = ({
) )
} }
], ],
[intl, sortOrder, clusterData, loadend, firstLoad, handleSelect] [intl, sourceType, sortOrder, clusterData, loadend, firstLoad, handleSelect]
); );
}; };
+1 -1
View File
@@ -182,7 +182,7 @@ const Users: React.FC = () => {
handleClickPrimary={handleAddUser} handleClickPrimary={handleAddUser}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
rowSelection={rowSelection} rowSelection={rowSelection}
width={{ input: 300 }} widths={{ input: 300 }}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table