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:
@@ -182,7 +182,8 @@ export default {
|
|||||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
'Kubernetes namespace the cluster’s 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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -182,7 +182,8 @@ export default {
|
|||||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
'Kubernetes namespace the cluster’s 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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -183,7 +183,8 @@ export default {
|
|||||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
'Kubernetes namespace the cluster’s 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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -183,7 +183,8 @@ export default {
|
|||||||
'Kubernetes namespace the cluster’s manifests render into. Leave empty to use gpustack-system.',
|
'Kubernetes namespace the cluster’s 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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user