fix: remove k8s options section and merge into advance

And re-order the configuration inputs. The current input order is:
- namespace
- volume mounts
- registry credentials
- node selector
- system-default-registry
- operator image
- gpu instance static access address
- worker config yaml
This commit is contained in:
Yuxing Deng
2026-06-03 23:17:38 +08:00
committed by jialin
parent 4810ff2dbe
commit 8f0e3a7576
9 changed files with 121 additions and 86 deletions
+2 -1
View File
@@ -182,7 +182,8 @@ export default {
'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.', 'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.',
'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.title': 'GPU Instance Service',
'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.',
'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress':
'GPU Instance Service Static Access Address',
'clusters.gpuInstances.staticAddress.tip': 'clusters.gpuInstances.staticAddress.tip':
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.' 'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
}; };
+2 -1
View File
@@ -182,7 +182,8 @@ export default {
'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.', 'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.',
'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.title': 'GPU Instance Service',
'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.',
'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress':
'GPU Instance Service Static Access Address',
'clusters.gpuInstances.staticAddress.tip': 'clusters.gpuInstances.staticAddress.tip':
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.' 'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
}; };
+2 -1
View File
@@ -183,7 +183,8 @@ export default {
'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.', 'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.',
'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.title': 'GPU Instance Service',
'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.',
'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress':
'GPU Instance Service Static Access Address',
'clusters.gpuInstances.staticAddress.tip': 'clusters.gpuInstances.staticAddress.tip':
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.' 'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
}; };
+2 -1
View File
@@ -183,7 +183,8 @@ export default {
'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.', 'Kubernetes namespace the clusters manifests render into. Leave empty to use gpustack-system.',
'clusters.gpuInstances.title': 'GPU Instance Service', 'clusters.gpuInstances.title': 'GPU Instance Service',
'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.', 'clusters.gpuInstances.tip': 'Enable GPU instance support for this cluster.',
'clusters.gpuInstances.staticAddress': 'Static Access Address', 'clusters.gpuInstances.staticAddress':
'GPU Instance Service Static Access Address',
'clusters.gpuInstances.staticAddress.tip': 'clusters.gpuInstances.staticAddress.tip':
'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.' 'Static address the operator uses to access GPU instances in this cluster (e.g. a LoadBalancer VIP). Optional.'
}; };
+1 -1
View File
@@ -175,7 +175,7 @@ export default {
'集群清单渲染所使用的 Kubernetes 命名空间。留空则使用 gpustack-system。', '集群清单渲染所使用的 Kubernetes 命名空间。留空则使用 gpustack-system。',
'clusters.gpuInstances.title': 'GPU 实例服务', 'clusters.gpuInstances.title': 'GPU 实例服务',
'clusters.gpuInstances.tip': '为该集群启用 GPU 实例支持。', 'clusters.gpuInstances.tip': '为该集群启用 GPU 实例支持。',
'clusters.gpuInstances.staticAddress': '静态访问地址', 'clusters.gpuInstances.staticAddress': 'GPU实例服务静态访问地址',
'clusters.gpuInstances.staticAddress.tip': 'clusters.gpuInstances.staticAddress.tip':
'Operator 访问该集群 GPU 实例所使用的静态地址(例如 LoadBalancer VIP)。可选。' 'Operator 访问该集群 GPU 实例所使用的静态地址(例如 LoadBalancer VIP)。可选。'
}; };
@@ -25,7 +25,7 @@ import {
} from '../config/types'; } from '../config/types';
import AdvanceConfig from '../step-forms/advance-config'; import AdvanceConfig from '../step-forms/advance-config';
import CloudProvider from './cloud-provider-form'; import CloudProvider from './cloud-provider-form';
import K8sPodSpec from './k8s-pod-spec'; import K8sAdvancedOptions, { GpuInstanceServiceSwitch } from './k8s-pod-spec';
type AddModalProps = { type AddModalProps = {
action: PageActionType; action: PageActionType;
@@ -40,9 +40,6 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const [activeKey, setActiveKey] = React.useState<string[]>([]); const [activeKey, setActiveKey] = React.useState<string[]>([]);
const [k8sActiveKey, setK8sActiveKey] = React.useState<string[]>([
'k8sOptions'
]);
const [submitAttempted, setSubmitAttempted] = useState(false); const [submitAttempted, setSubmitAttempted] = useState(false);
const advanceConfigRef = React.useRef<any>(null); const advanceConfigRef = React.useRef<any>(null);
const systemConfig = useAtomValue(systemConfigAtom); const systemConfig = useAtomValue(systemConfigAtom);
@@ -247,7 +244,14 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
<Form.Item<FormData> <Form.Item<FormData>
name="description" name="description"
rules={[{ required: false }]} rules={[{ required: false }]}
style={{ marginBottom: 8 }} // For Kubernetes the GPU instance service switch follows directly,
// so fall back to the default item margin (matching the name field)
// to keep the description spacing symmetric; other providers keep
// the tighter gap before the advanced panel.
style={{
marginBottom:
provider === ProviderValueMap.Kubernetes ? undefined : 8
}}
> >
<SealTextArea <SealTextArea
scaleSize scaleSize
@@ -256,28 +260,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
</Form.Item> </Form.Item>
{provider === ProviderValueMap.Kubernetes && ( {provider === ProviderValueMap.Kubernetes && (
<CollapsePanel <GpuInstanceServiceSwitch />
accordion={false}
activeKey={k8sActiveKey}
onChange={(keys) =>
setK8sActiveKey(Array.isArray(keys) ? keys : [keys])
}
items={[
{
key: 'k8sOptions',
label: intl.formatMessage({
id: 'clusters.k8sOptions.title'
}),
forceRender: true,
children: (
<K8sPodSpec
key={currentData?.id ?? 'new'}
action={action}
></K8sPodSpec>
)
}
]}
></CollapsePanel>
)} )}
<CollapsePanel <CollapsePanel
@@ -290,12 +273,20 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
label: intl.formatMessage({ id: 'resources.form.advanced' }), label: intl.formatMessage({ id: 'resources.form.advanced' }),
forceRender: true, forceRender: true,
children: ( children: (
<AdvanceConfig <>
action={action} {provider === ProviderValueMap.Kubernetes && (
provider={provider} <K8sAdvancedOptions
currentData={currentData} key={currentData?.id ?? 'new'}
ref={advanceConfigRef} action={action}
></AdvanceConfig> ></K8sAdvancedOptions>
)}
<AdvanceConfig
action={action}
provider={provider}
currentData={currentData}
ref={advanceConfigRef}
></AdvanceConfig>
</>
) )
} }
]} ]}
@@ -7,21 +7,20 @@ import styled from 'styled-components';
import ImageCredential from './image-credential'; import ImageCredential from './image-credential';
import K8SVolumeMount from './k8s-volume-mount'; import K8SVolumeMount from './k8s-volume-mount';
const Title = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
background-color: transparent;
font-weight: 500;
font-size: 14px;
padding-top: 0px;
padding-bottom: 8px;
`;
const SectionWrap = styled.div` const SectionWrap = styled.div`
margin-bottom: 16px; margin-bottom: 16px;
`; `;
// SwitchCard always renders a content <div> while the switch is on (it's meant
// to wrap expandable children). We render no children here, so that div is
// empty — but the card is a flex column with an 8px gap, so the empty div still
// adds height and makes the card jump as the switch toggles. Hide it.
const SwitchCardWrap = styled.div`
& > div > div:empty {
display: none;
}
`;
const NodeSelectorForm: React.FC = () => { const NodeSelectorForm: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
@@ -57,7 +56,7 @@ const NamespaceForm: React.FC = () => {
); );
}; };
const OperatorImageForm: React.FC = () => { export const OperatorImageForm: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
return ( return (
@@ -76,55 +75,87 @@ const OperatorImageForm: React.FC = () => {
); );
}; };
const GpuInstanceOptionsForm: React.FC = () => { // The presence of `gpuInstanceOptions` on `k8s_options` is the source of truth
// for whether GPU instances are enabled. Both the switch (rendered up top) and
// the static-address field (rendered in the advanced section) watch this same
// path so they stay in sync without sharing local state.
const GPU_INSTANCE_OPTIONS_PATH = ['k8s_options', 'gpuInstanceOptions'];
// Standalone switch shown directly under the cluster description. Toggling it
// only flips the presence of `gpuInstanceOptions` on the form; the related
// inputs live in the advanced section.
export const GpuInstanceServiceSwitch: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance(); const form = Form.useFormInstance();
// The presence of `gpuInstanceOptions` on `k8s_options` is the source of // `preserve: true` makes useWatch read the full form store rather than only
// truth for whether GPU instances are enabled. Derive the toggle directly // registered fields — required here because `gpuInstanceOptions` is set via
// from the watched form value rather than mirroring it into local state, so // setFieldValue and has no mounted Form.Item of its own while the switch is
// it stays in sync when the form loads async values (EDIT) or resets. // off, so a default watch would never see it flip on.
const gpuInstanceOptions = Form.useWatch( const gpuInstanceOptions = Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
['k8s_options', 'gpuInstanceOptions'], form,
form preserve: true
); });
const enabled = !!gpuInstanceOptions; const enabled = !!gpuInstanceOptions;
const handleToggle = (checked: boolean) => { const handleToggle = (checked: boolean) => {
if (!form) return; if (!form) return;
// The presence of `gpuInstanceOptions` is what signals "GPU instances // When on, ensure the object exists (defaulting to {} so it survives even
// enabled" to the backend — when on, ensure the object exists (defaulting // when the static address is left blank); when off, remove it entirely.
// to {} so it survives even when the static address is left blank); when
// off, remove it entirely.
const path = ['k8s_options', 'gpuInstanceOptions'];
if (checked) { if (checked) {
form.setFieldValue(path, form.getFieldValue(path) ?? {}); form.setFieldValue(
GPU_INSTANCE_OPTIONS_PATH,
form.getFieldValue(GPU_INSTANCE_OPTIONS_PATH) ?? {}
);
} else { } else {
form.setFieldValue(path, undefined); form.setFieldValue(GPU_INSTANCE_OPTIONS_PATH, undefined);
} }
}; };
return ( return (
<SwitchCard <SwitchCardWrap>
styles={{ <SwitchCard
wrapper: { styles={{
borderRadius: 'var(--ant-border-radius-lg)', wrapper: {
paddingInline: 14, borderRadius: 'var(--ant-border-radius-lg)',
marginBottom: 24 paddingInline: 14,
} marginBottom: 24
}} }
value={enabled} }}
onChange={handleToggle} value={enabled}
label={intl.formatMessage({ id: 'clusters.gpuInstances.title' })} onChange={handleToggle}
description={intl.formatMessage({ id: 'clusters.gpuInstances.tip' })} label={intl.formatMessage({ id: 'clusters.gpuInstances.title' })}
> description={intl.formatMessage({ id: 'clusters.gpuInstances.tip' })}
/>
</SwitchCardWrap>
);
};
// Static access address for GPU instances. Only shown while the GPU instance
// service switch is on, mirroring the previous in-card behaviour. Rendered in
// the advanced section, between the default container registry and the worker
// config (节点配置).
export const GpuInstancesStaticAddressForm: React.FC = () => {
const intl = useIntl();
// See note in GpuInstanceServiceSwitch: watch the full store so this field's
// visibility tracks the switch even before it has mounted its own Form.Item.
const enabled = !!Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
preserve: true
});
if (!enabled) {
return null;
}
return (
<SectionWrap>
<Form.Item <Form.Item
name={[ name={[
'k8s_options', 'k8s_options',
'gpuInstanceOptions', 'gpuInstanceOptions',
'gpuInstancesAccessStaticAddress' 'gpuInstancesAccessStaticAddress'
]} ]}
style={{ marginBottom: 0 }}
normalize={(value) => value || null} normalize={(value) => value || null}
noStyle
> >
<CInput.Input <CInput.Input
label={intl.formatMessage({ label={intl.formatMessage({
@@ -135,23 +166,22 @@ const GpuInstanceOptionsForm: React.FC = () => {
})} })}
></CInput.Input> ></CInput.Input>
</Form.Item> </Form.Item>
</SwitchCard> </SectionWrap>
); );
}; };
const K8sPodSpec: React.FC<{ // Kubernetes-specific options that live inside the cluster's advanced section.
const K8sAdvancedOptions: React.FC<{
action: PageActionType; action: PageActionType;
}> = ({ action }) => { }> = ({ action }) => {
return ( return (
<> <>
<GpuInstanceOptionsForm />
<NamespaceForm /> <NamespaceForm />
<K8SVolumeMount action={action}></K8SVolumeMount> <K8SVolumeMount action={action}></K8SVolumeMount>
<ImageCredential /> <ImageCredential />
<NodeSelectorForm /> <NodeSelectorForm />
<OperatorImageForm />
</> </>
); );
}; };
export default K8sPodSpec; export default K8sAdvancedOptions;
@@ -40,7 +40,7 @@ const VolumeMountsForm: React.FC<{ action: PageActionType }> = ({ action }) => {
const k8sVolumeMounts = Form.useWatch(['k8s_options', 'volumeMounts'], form); const k8sVolumeMounts = Form.useWatch(['k8s_options', 'volumeMounts'], form);
const [collapseKey, setCollapseKey] = useState<Set<number | string>>( const [collapseKey, setCollapseKey] = useState<Set<number | string>>(
new Set([0]) new Set()
); );
const volumeList = [ const volumeList = [
@@ -7,6 +7,10 @@ import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd'; import { Button, Form } from 'antd';
import React, { forwardRef, useEffect, useImperativeHandle } from 'react'; import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import {
GpuInstancesStaticAddressForm,
OperatorImageForm
} from '../components/k8s-pod-spec';
import { ProviderType, ProviderValueMap } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
import { import {
ClusterFormData as FormData, ClusterFormData as FormData,
@@ -107,6 +111,12 @@ const ClusterAdvanceConfig: React.FC<{
placeholder="docker.io" placeholder="docker.io"
></CInput.Input> ></CInput.Input>
</Form.Item> </Form.Item>
{provider === ProviderValueMap.Kubernetes && (
<>
<OperatorImageForm />
<GpuInstancesStaticAddressForm />
</>
)}
<Title> <Title>
{intl.formatMessage({ id: 'clusters.create.workerConfig' })} {intl.formatMessage({ id: 'clusters.create.workerConfig' })}
</Title> </Title>