From e314f25a77dbbee5d6b64103b4b062c313cb0fcf Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 1 Jun 2026 16:34:57 +0800 Subject: [PATCH] style: gpu instance service --- src/locales/en-US/clusters.ts | 4 +- src/locales/ja-JP/clusters.ts | 4 +- src/locales/ru-RU/clusters.ts | 4 +- src/locales/tr-TR/clusters.ts | 4 +- src/locales/zh-CN/clusters.ts | 4 +- .../components/cluster-form.tsx | 47 +------ .../components/k8s-pod-spec.tsx | 115 ++++++------------ .../step-forms/advance-config.tsx | 1 + 8 files changed, 49 insertions(+), 134 deletions(-) diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index 254d4995..686a7188 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -165,7 +165,7 @@ export default { 'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry', 'clusters.systemDefaultContainerRegistry.tip': 'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.', - 'clusters.k8sOptions.title': 'K8s Deployment Options', + 'clusters.k8sOptions.title': 'Kubernetes Deployment Options', 'clusters.imageCredentials.title': 'Image Credentials', 'clusters.imageCredentials.add': 'Add Credential', 'clusters.imageCredentials.registry': 'Registry', @@ -180,7 +180,7 @@ export default { 'clusters.namespace.title': 'Namespace', 'clusters.namespace.tip': 'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.', - 'clusters.gpuInstances.title': 'GPU Instances', + 'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress.tip': diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index c48e9f50..25c6b04e 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -165,7 +165,7 @@ export default { 'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry', 'clusters.systemDefaultContainerRegistry.tip': 'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.', - 'clusters.k8sOptions.title': 'K8s Deployment Options', + 'clusters.k8sOptions.title': 'Kubernetes Deployment Options', 'clusters.imageCredentials.title': 'Image Credentials', 'clusters.imageCredentials.add': 'Add Credential', 'clusters.imageCredentials.registry': 'Registry', @@ -180,7 +180,7 @@ export default { 'clusters.namespace.title': 'Namespace', 'clusters.namespace.tip': 'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.', - 'clusters.gpuInstances.title': 'GPU Instances', + 'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress.tip': diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index 74751b2a..9158c9d0 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -166,7 +166,7 @@ export default { 'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry', 'clusters.systemDefaultContainerRegistry.tip': 'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.', - 'clusters.k8sOptions.title': 'K8s Deployment Options', + 'clusters.k8sOptions.title': 'Kubernetes Deployment Options', 'clusters.imageCredentials.title': 'Image Credentials', 'clusters.imageCredentials.add': 'Add Credential', 'clusters.imageCredentials.registry': 'Registry', @@ -181,7 +181,7 @@ export default { 'clusters.namespace.title': 'Namespace', 'clusters.namespace.tip': 'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.', - 'clusters.gpuInstances.title': 'GPU Instances', + 'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress.tip': diff --git a/src/locales/tr-TR/clusters.ts b/src/locales/tr-TR/clusters.ts index eb02c863..8ee15324 100644 --- a/src/locales/tr-TR/clusters.ts +++ b/src/locales/tr-TR/clusters.ts @@ -166,7 +166,7 @@ export default { 'clusters.systemDefaultContainerRegistry.title': 'Default Container Registry', 'clusters.systemDefaultContainerRegistry.tip': 'Default registry used to resolve GPUStack images for this cluster. Falls back to the server default when unset.', - 'clusters.k8sOptions.title': 'K8s Deployment Options', + 'clusters.k8sOptions.title': 'Kubernetes Deployment Options', 'clusters.imageCredentials.title': 'Image Credentials', 'clusters.imageCredentials.add': 'Add Credential', 'clusters.imageCredentials.registry': 'Registry', @@ -181,7 +181,7 @@ export default { 'clusters.namespace.title': 'Namespace', 'clusters.namespace.tip': 'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.', - 'clusters.gpuInstances.title': 'GPU Instances', + 'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress.tip': diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 698336d2..3e30f57e 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -158,7 +158,7 @@ export default { 'clusters.systemDefaultContainerRegistry.title': '默认容器镜像仓库', 'clusters.systemDefaultContainerRegistry.tip': '用于解析该集群 GPUStack 镜像的默认镜像仓库。未设置时回退到服务端默认值。', - 'clusters.k8sOptions.title': 'K8s 部署选项', + 'clusters.k8sOptions.title': 'Kubernetes 部署选项', 'clusters.imageCredentials.title': '镜像仓库凭证', 'clusters.imageCredentials.add': '添加凭证', 'clusters.imageCredentials.registry': '镜像仓库地址', @@ -173,7 +173,7 @@ export default { 'clusters.namespace.title': '命名空间', 'clusters.namespace.tip': '集群清单渲染所使用的 Kubernetes 命名空间。留空则使用 gpustack-system。', - 'clusters.gpuInstances.title': 'GPU 实例', + 'clusters.gpuInstances.title': 'GPU 实例服务', 'clusters.gpuInstances.tip': '为该集群启用 GPU 实例支持。', 'clusters.gpuInstances.staticAddress': '静态访问地址', 'clusters.gpuInstances.staticAddress.tip': diff --git a/src/pages/cluster-management/components/cluster-form.tsx b/src/pages/cluster-management/components/cluster-form.tsx index 85d46526..b6e62763 100644 --- a/src/pages/cluster-management/components/cluster-form.tsx +++ b/src/pages/cluster-management/components/cluster-form.tsx @@ -34,8 +34,6 @@ const ClusterForm: React.FC = forwardRef( const [form] = Form.useForm(); const intl = useIntl(); const [activeKey, setActiveKey] = React.useState([]); - // K8s deployment options is its own top-level section (sibling of Advanced), - // open by default so the fields are visible without an extra click. const [k8sActiveKey, setK8sActiveKey] = React.useState([ 'k8sOptions' ]); @@ -59,20 +57,8 @@ const ClusterForm: React.FC = forwardRef( } }, [activeKey, action]); - // The backend models the optional k8s_options string knobs as - // Optional[str] and treats null/absent as "use the server default" or - // "no auth". Coerce empty form values to null before sending so a blank - // input is unambiguous rather than an empty string that defeats fallbacks. const normalizeOutgoing = (values: any): any => { const base: any = { ...values }; - // Top-level cluster field shared by Docker and K8s. Trim then coerce a - // blank input to null so clearing it on edit (or a whitespace-only - // value) falls back to the server default rather than persisting an - // empty string. - if (base.system_default_container_registry !== undefined) { - base.system_default_container_registry = - base.system_default_container_registry?.trim() || null; - } const opts = base.k8s_options; if (!opts) return base; @@ -88,32 +74,14 @@ const ClusterForm: React.FC = forwardRef( })); } - next.operatorImage = opts.operatorImage || null; - next.namespace = opts.namespace || null; - - // Presence of gpuInstanceOptions is the enable flag; keep it only when - // the toggle left an object behind, coercing a blank address to null. - if (opts.gpuInstanceOptions) { - next.gpuInstanceOptions = { - gpuInstancesAccessStaticAddress: - opts.gpuInstanceOptions.gpuInstancesAccessStaticAddress || null - }; - } - return { ...base, k8s_options: next }; }; - const handleOnFinish = (_values: FormData) => { + const handleOnFinish = (values: FormData) => { const workerConfig = yaml2Json(advanceConfigRef.current?.getYamlValue()); - // antd's onFinish only delivers values for registered Form.Items. - // Spreading those on top of `getFieldsValue(true)` clobbers nested - // objects (e.g. `k8s_options` would lose values set via setFieldValue), - // so we go straight to the full store. - const fullValues = form.getFieldsValue(true); - onFinish( normalizeOutgoing({ - ...fullValues, + ...values, worker_config: { ...workerConfig } @@ -203,10 +171,6 @@ const ClusterForm: React.FC = forwardRef( }; }, validateFields: async () => { - // Run validation first to display any field errors. Then read the - // FULL store via `getFieldsValue(true)` so values that were set via - // setFieldValue on non-registered paths are still included in what we - // hand to the API. await form.validateFields(); const values = form.getFieldsValue(true); @@ -268,7 +232,7 @@ const ClusterForm: React.FC = forwardRef( style={{ marginBottom: 8 }} > @@ -286,11 +250,6 @@ const ClusterForm: React.FC = forwardRef( label: intl.formatMessage({ id: 'clusters.k8sOptions.title' }), forceRender: true, children: ( - // Key by cluster id so the section fully remounts when the - // active cluster changes. GpuInstanceOptionsForm seeds its - // local state from initialValue only once (initializedRef), - // so without a remount a reused form instance could carry a - // previous cluster's GPU instance config into the next one. { value || null} > { value || null} > { ); }; -// GPU-instance support. The backend treats the mere presence of -// `gpuInstanceOptions` as the enable flag, so the switch toggles the whole -// object in/out of the form rather than setting a boolean field; the static -// address (optional even when enabled) is nested underneath. -// -// We drive the toggle from local state (not Form.useWatch) because the -// gpuInstanceOptions path has no registered Form.Item of its own — useWatch -// doesn't reliably re-render on setFieldValue for such paths, which left the -// switch unresponsive. Local state owns the visible state and we mirror it -// into the form via setFieldValue so submit still collects it. const GpuInstanceOptionsForm: React.FC<{ initialValue?: GpuInstanceOptions; }> = ({ initialValue }) => { const intl = useIntl(); - const form = Form.useFormInstance(); const [enabled, setEnabled] = useState(!!initialValue); - const [address, setAddress] = useState( - initialValue?.gpuInstancesAccessStaticAddress || '' - ); - const initializedRef = useRef(!!initialValue); - - const writeForm = (en: boolean, addr: string) => { - form.setFieldValue( - ['k8s_options', 'gpuInstanceOptions'], - en ? { gpuInstancesAccessStaticAddress: addr } : undefined - ); - }; - - // Mirror a seeded initial value into the form on mount so submit collects it. - useEffect(() => { - if (initialValue) { - writeForm(true, initialValue.gpuInstancesAccessStaticAddress || ''); - } - }, []); - - // Adopt currentData arriving after mount (async edit load), once. After the - // user has interacted (`initializedRef`), local state owns the section. - useEffect(() => { - if (initializedRef.current) return; - if (initialValue) { - setEnabled(true); - setAddress(initialValue.gpuInstancesAccessStaticAddress || ''); - writeForm(true, initialValue.gpuInstancesAccessStaticAddress || ''); - initializedRef.current = true; - } - }, [initialValue]); const handleToggle = (checked: boolean) => { - initializedRef.current = true; setEnabled(checked); - if (!checked) { - setAddress(''); - } - writeForm(checked, checked ? address : ''); - }; - - const handleAddressChange = (e: any) => { - const next = typeof e === 'string' ? e : (e?.target?.value ?? ''); - setAddress(next); - writeForm(true, next); }; return ( - - - <div className="flex-center gap-8"> - <span className="flex-center gap-4"> - <span> - {intl.formatMessage({ id: 'clusters.gpuInstances.title' })} - </span> - <Tooltip - title={intl.formatMessage({ id: 'clusters.gpuInstances.tip' })} - > - <QuestionCircleOutlined - style={{ color: 'var(--ant-color-text-secondary)' }} - /> - </Tooltip> - </span> - <Switch checked={enabled} onChange={handleToggle} /> - </div> - - {enabled && ( + + value || null} + noStyle + > - )} - + + ); }; @@ -315,12 +270,12 @@ const K8sPodSpec: React.FC<{ }> = ({ action, initialGpuInstanceOptions }) => { return ( <> + - ); }; diff --git a/src/pages/cluster-management/step-forms/advance-config.tsx b/src/pages/cluster-management/step-forms/advance-config.tsx index 69bf13ff..20a27b00 100644 --- a/src/pages/cluster-management/step-forms/advance-config.tsx +++ b/src/pages/cluster-management/step-forms/advance-config.tsx @@ -95,6 +95,7 @@ const ClusterAdvanceConfig: React.FC<{ name="system_default_container_registry" style={{ marginBottom: 16 }} + normalize={(value) => value?.trim?.() || null} >