diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts
index b217004e..fe9b6d49 100644
--- a/src/locales/en-US/clusters.ts
+++ b/src/locales/en-US/clusters.ts
@@ -70,7 +70,11 @@ export default {
'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.',
'clusters.addworker.selectGPU': 'Select GPU Vendor',
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
+ 'clusters.addworker.selectGPU.subtitle':
+ 'You can select multiple GPU Vendors or none for CPU-only clusters',
'clusters.addworker.checkEnv': 'Check Environment',
+ 'clusters.addworker.checkEnv.cpuOnlyTips':
+ 'Use the following command to verify that the Kubernetes cluster has at least one ready node. You are registering a CPU-only cluster.',
'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.dtkVersion': 'DTK Version',
'clusters.addworker.runCommand': 'Run Command',
diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts
index 60a668c7..098dd2af 100644
--- a/src/locales/ja-JP/clusters.ts
+++ b/src/locales/ja-JP/clusters.ts
@@ -70,7 +70,11 @@ export default {
'For non-Docker clusters, please register clusters or manage worker pools from the Clusters page.',
'clusters.addworker.selectGPU': 'Select GPU Vendor',
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
+ 'clusters.addworker.selectGPU.subtitle':
+ '複数の GPU ベンダーを選択するか、CPU クラスター専用の場合は選択不要です',
'clusters.addworker.checkEnv': 'Check Environment',
+ 'clusters.addworker.checkEnv.cpuOnlyTips':
+ '以下のコマンドを使用して、Kubernetes クラスターに少なくとも 1 つのレディーノードがあることを確認してください。CPU クラスターを登録しています。',
'clusters.addworker.specifyArgs': 'Specify Arguments',
'clusters.addworker.dtkVersion': 'DTK バージョン',
'clusters.addworker.runCommand': 'Run Command',
diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts
index aed4a17e..9b5f28e4 100644
--- a/src/locales/ru-RU/clusters.ts
+++ b/src/locales/ru-RU/clusters.ts
@@ -70,7 +70,11 @@ export default {
'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
+ 'clusters.addworker.selectGPU.subtitle':
+ 'Вы можете выбрать несколько производителей GPU или не выбирать для кластера только с CPU',
'clusters.addworker.checkEnv': 'Проверить окружение',
+ 'clusters.addworker.checkEnv.cpuOnlyTips':
+ 'Используйте следующую команду, чтобы убедиться, что в кластере Kubernetes есть хотя бы один готовый узел. Вы регистрируете кластер только с CPU.',
'clusters.addworker.specifyArgs': 'Указать аргументы',
'clusters.addworker.dtkVersion': 'Версия DTK',
'clusters.addworker.runCommand': 'Выполнить команду',
diff --git a/src/locales/tr-TR/clusters.ts b/src/locales/tr-TR/clusters.ts
index f5f84ddc..676726eb 100644
--- a/src/locales/tr-TR/clusters.ts
+++ b/src/locales/tr-TR/clusters.ts
@@ -70,7 +70,11 @@ export default {
'Docker dışı kümeler için lütfen Kümeler sayfasından küme kaydı oluşturun veya işçi havuzlarını yönetin.',
'clusters.addworker.selectGPU': 'GPU Üreticisi Seç',
'clusters.addworker.selectGPU.multiTag': 'Multi-select',
+ 'clusters.addworker.selectGPU.subtitle':
+ 'Birden fazla GPU Üreticisi seçebilir veya yalnızca CPU kümeleri için hiçbirini seçmeyebilirsiniz',
'clusters.addworker.checkEnv': 'Ortamı Kontrol Et',
+ 'clusters.addworker.checkEnv.cpuOnlyTips':
+ 'Kubernetes kümesinde en az bir hazır düğüm olduğunu doğrulamak için aşağıdaki komutu kullanın. Yalnızca CPU kümelerini kaydediyorsunuz.',
'clusters.addworker.specifyArgs': 'Argümanları Belirle',
'clusters.addworker.dtkVersion': 'DTK Sürümü',
'clusters.addworker.runCommand': 'Komutu Çalıştır',
diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts
index d12d070b..7afe3698 100644
--- a/src/locales/zh-CN/clusters.ts
+++ b/src/locales/zh-CN/clusters.ts
@@ -68,7 +68,11 @@ export default {
'非 Docker 集群请前往集群页面注册集群或管理节点池。',
'clusters.addworker.selectGPU': '选择 GPU 厂商',
'clusters.addworker.selectGPU.multiTag': '可多选',
+ 'clusters.addworker.selectGPU.subtitle':
+ '可选择多个 GPU 厂商,或不选择以用于仅 CPU 的集群',
'clusters.addworker.checkEnv': '检查环境',
+ 'clusters.addworker.checkEnv.cpuOnlyTips':
+ '使用以下命令验证 Kubernetes 集群中至少有一个就绪节点。你正在注册仅 CPU 的集群。',
'clusters.addworker.specifyArgs': '指定参数',
'clusters.addworker.dtkVersion': 'DTK 版本',
'clusters.addworker.runCommand': '运行指令',
diff --git a/src/pages/cluster-management/components/add-worker/add-worker-step.tsx b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx
index 5445e0d8..e8e6e436 100644
--- a/src/pages/cluster-management/components/add-worker/add-worker-step.tsx
+++ b/src/pages/cluster-management/components/add-worker/add-worker-step.tsx
@@ -99,24 +99,30 @@ const AddWorkerSteps: React.FC = (props) => {
// Downstream steps (check env, run command, ...) only make sense after a
// GPU vendor has been chosen. If the user toggled off every vendor in
// multi-select, gate them shut so the wrong panel can't be opened.
+ // Exception: for K8s clusters, Run Command is always accessible (CPU-only
+ // workers don't need a GPU vendor).
const selectedGPUs =
(summary.get('selectedGPUs') as string[] | undefined) || [];
const currentGPU = (summary.get('currentGPU') as string | undefined) || '';
const noVendorSelected = !currentGPU && selectedGPUs.length === 0;
- const downstreamDisabled = disabled || noVendorSelected;
+ const isK8s = provider === ProviderValueMap.Kubernetes;
+ // For non-K8s providers, all downstream steps are gated by vendor selection.
+ // For K8s, both CheckEnv and RunCommand are always accessible — CheckEnv
+ // shows a ready-nodes check when no vendor is selected (CPU-only workers).
+ const downstreamDisabled = disabled || (noVendorSelected && !isK8s);
React.useEffect(() => {
- // If the user just deselected everything, collapse any downstream
- // panel back to the GPU step so they aren't left looking at a stale
- // disabled-but-open command. Functional updater so we don't have to
- // depend on `collapseKey` and re-run the effect on every toggle.
- if (!noVendorSelected) return;
+ // If the user just deselected everything on a non-K8s provider, collapse
+ // any downstream panel back to the GPU step so they aren't left looking at
+ // a stale disabled-but-open command. For K8s, CheckEnv stays open with a
+ // ready-nodes check, so no collapsing needed.
+ if (!noVendorSelected || isK8s) return;
setCollapseKey((prev) =>
prev.has(StepNamesMap.SelectGPU)
? prev
: new Set([StepNamesMap.SelectGPU])
);
- }, [noVendorSelected]);
+ }, [noVendorSelected, isK8s]);
return (
= (props) => {
!stepList.includes(StepNamesMap.SelectCluster)) && (
<>
-
+ {(isK8s || !noVendorSelected) && (
+
+ )}
{provider === ProviderValueMap.Kubernetes && (
diff --git a/src/pages/cluster-management/components/add-worker/check-environment.tsx b/src/pages/cluster-management/components/add-worker/check-environment.tsx
index f8f36844..1c1ec6ec 100644
--- a/src/pages/cluster-management/components/add-worker/check-environment.tsx
+++ b/src/pages/cluster-management/components/add-worker/check-environment.tsx
@@ -1,6 +1,7 @@
import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Alert, Typography } from 'antd';
+import { ProviderValueMap } from '../../config';
import CheckEnvCommand from '../check-env-command';
import { useAddWorkerContext } from './add-worker-context';
import { AddWorkerStepProps, StepNamesMap } from './config';
@@ -10,8 +11,8 @@ import StepCollapse from './step-collapse';
const CheckEnvironment: React.FC = ({ disabled }) => {
const { stepList, summary, provider } = useAddWorkerContext();
const intl = useIntl();
- const currentGPU = summary.get('currentGPU');
- const currentGPUs: string[] = summary.get('selectedGPUs') || [];
+ const currentGPU = summary.get('currentGPU') as string;
+ const currentGPUs: string[] = (summary.get('selectedGPUs') as string[]) || [];
const workerCommand = summary.get('workerCommand') || {
label: '',
link: '',
@@ -19,6 +20,8 @@ const CheckEnvironment: React.FC = ({ disabled }) => {
};
const stepIndex = stepList.indexOf(StepNamesMap.CheckEnv) + 1;
+ const isK8s = provider === ProviderValueMap.Kubernetes;
+ const isCPUOnly = isK8s && !currentGPU && currentGPUs.length === 0;
return (
= ({ disabled }) => {
}
>
- }
- style={{
- marginBottom: 8
- }}
- title={
-
+ {intl.formatMessage({
+ id: 'clusters.addworker.checkEnv.cpuOnlyTips'
+ })}
+
+ ) : (
+ <>
+ }
+ style={{
+ marginBottom: 8
}}
- >
- }
- >
-
- {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
-
+ title={
+
+ }
+ >
+
+ {intl.formatMessage({ id: 'cluster.create.checkEnv.tips' })}
+
+ >
+ )}
= ({ disabled }) => {
};
useEffect(() => {
- // init a default selection
+ // K8s clusters allow selecting none (CPU-only workers), so skip default
+ // selection. Other providers still default to NVIDIA.
+ if (multiCapable) return;
handleSelect(GPUDriverMap.NVIDIA, {
label: 'NVIDIA',
hiddenTitle: true,
@@ -116,18 +117,18 @@ const SelectVendor: React.FC = ({ disabled }) => {
{stepIndex}.{' '}
{intl.formatMessage({ id: 'clusters.addworker.selectGPU' })}
{multiCapable && (
-
{intl.formatMessage({
- id: 'clusters.addworker.selectGPU.multiTag'
+ id: 'clusters.addworker.selectGPU.subtitle'
})}
-
+
)}
}
diff --git a/src/pages/cluster-management/components/check-env-command.tsx b/src/pages/cluster-management/components/check-env-command.tsx
index 379ef787..2d97560b 100644
--- a/src/pages/cluster-management/components/check-env-command.tsx
+++ b/src/pages/cluster-management/components/check-env-command.tsx
@@ -1,7 +1,12 @@
import { addWorkerGuide } from '@/pages/resources/config';
import { HighlightCode } from '@gpustack/core-ui';
import React from 'react';
-import { ProviderType } from '../config';
+import { ProviderType, ProviderValueMap } from '../config';
+
+// CPU-only (no GPU vendor selected) check for Kubernetes: verify at least one
+// ready node exists in the target cluster.
+const K8S_CPU_ONLY_CHECK =
+ 'kubectl get nodes -o jsonpath=\'{.items[*].status.conditions[?(@.type=="Ready")].status}\' 2>/dev/null | grep -q "True" && echo "Ready nodes found. You are registering a CPU-only cluster." || (echo "No ready nodes found"; exit 1)';
type ViewModalProps = {
provider: ProviderType;
@@ -25,6 +30,12 @@ const AddWorkerCommand: React.FC = ({
: currentGPU
? [currentGPU]
: [];
+
+ // CPU-only K8s flow: no GPU vendor selected, check for ready nodes instead.
+ if (!keys.length && provider === ProviderValueMap.Kubernetes) {
+ return K8S_CPU_ONLY_CHECK;
+ }
+
if (!keys.length) return '';
const lines = keys
.map((k) => configs.checkEnvCommand(k)?.[provider || ''])
diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx
index 49b4a0fe..3b409b96 100644
--- a/src/pages/cluster-management/components/provider-catalog.tsx
+++ b/src/pages/cluster-management/components/provider-catalog.tsx
@@ -1,3 +1,4 @@
+import { CheckOutlined } from '@ant-design/icons';
import { IconFont, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Tooltip } from 'antd';
@@ -62,6 +63,29 @@ const Header = styled.div`
const CardBox = styled.div`
display: flex;
overflow: hidden;
+ position: relative;
+`;
+
+const ActiveBadge = styled.div`
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ width: 20px;
+ height: 20px;
+ background-color: var(--ant-color-primary);
+ clip-path: path('M 20 0 L 20 15 Q 20 20 15 20 L 0 20 Z');
+ z-index: 1;
+ display: flex;
+ align-items: flex-end;
+ justify-content: flex-end;
+ pointer-events: none;
+
+ .anticon {
+ color: #fff;
+ font-size: 10px;
+ margin-right: 2px;
+ margin-bottom: 2px;
+ }
`;
interface ProviderCatalogProps {
@@ -160,6 +184,13 @@ const ProviderCatalog: React.FC = ({
header={renderTitle(action)}
icon={action.icon}
>
+ {(Array.isArray(current)
+ ? current.includes(action.key)
+ : current === action.key) && (
+
+
+
+ )}
))}
diff --git a/src/pages/cluster-management/components/support-gpus.tsx b/src/pages/cluster-management/components/support-gpus.tsx
index 30acd5f2..ec5a844a 100644
--- a/src/pages/cluster-management/components/support-gpus.tsx
+++ b/src/pages/cluster-management/components/support-gpus.tsx
@@ -24,6 +24,7 @@ const Box = styled.div`
background-color: rgba(255, 255, 255, 0.1);
&.active {
background-color: var(--ant-color-fill-tertiary);
+ border-color: var(--ant-color-primary);
}
.template-card-icon {
margin-right: 0;
@@ -57,6 +58,7 @@ const Box = styled.div`
}
&.active {
background-color: var(--ant-color-bg-solid);
+ border-color: var(--ant-color-primary);
}
}
}