feat(cluster): card-based cluster type selector for K8s
Replace the GPU Service switch (with the small "cannot be used for
model service" caption) with a two-card radio selector that makes the
choice between Model Service and GPU Service explicit at a glance.
- Render two cards styled to match the existing SwitchCard (same
border, radius, 12px/14px padding, and label/description typography)
so the selector blends in with the surrounding form fields.
- Underlying form state is unchanged: picking GPU Service still seeds
k8s_options.gpuInstanceOptions = {}, picking Model Service clears
it — so the static-address field, the EDIT-mode change watcher, and
the API payload all keep working as before.
- Align the GPU Service label with the top-level menu entry
(menu.gpuService) — drop "Instance" / "实例" from the card title
and the static access address label.
This commit is contained in:
@@ -184,11 +184,14 @@ export default {
|
|||||||
'clusters.namespace.title': 'Namespace',
|
'clusters.namespace.title': 'Namespace',
|
||||||
'clusters.namespace.tip':
|
'clusters.namespace.tip':
|
||||||
'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.clusterType.title': 'Cluster Type',
|
||||||
|
'clusters.modelService.title': 'Model Service',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||||
|
'clusters.gpuInstances.title': 'GPU Service',
|
||||||
'clusters.gpuInstances.tip':
|
'clusters.gpuInstances.tip':
|
||||||
'Once enabled, the cluster is dedicated to GPU instance service and cannot be used for model service.',
|
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
|
||||||
'clusters.gpuInstances.staticAddress':
|
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
|
||||||
'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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -184,11 +184,14 @@ export default {
|
|||||||
'clusters.namespace.title': 'Namespace',
|
'clusters.namespace.title': 'Namespace',
|
||||||
'clusters.namespace.tip':
|
'clusters.namespace.tip':
|
||||||
'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.clusterType.title': 'Cluster Type',
|
||||||
|
'clusters.modelService.title': 'Model Service',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||||
|
'clusters.gpuInstances.title': 'GPU Service',
|
||||||
'clusters.gpuInstances.tip':
|
'clusters.gpuInstances.tip':
|
||||||
'Once enabled, the cluster is dedicated to GPU instance service and cannot be used for model service.',
|
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
|
||||||
'clusters.gpuInstances.staticAddress':
|
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
|
||||||
'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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -185,11 +185,14 @@ export default {
|
|||||||
'clusters.namespace.title': 'Namespace',
|
'clusters.namespace.title': 'Namespace',
|
||||||
'clusters.namespace.tip':
|
'clusters.namespace.tip':
|
||||||
'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.clusterType.title': 'Cluster Type',
|
||||||
|
'clusters.modelService.title': 'Model Service',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||||
|
'clusters.gpuInstances.title': 'GPU Service',
|
||||||
'clusters.gpuInstances.tip':
|
'clusters.gpuInstances.tip':
|
||||||
'Once enabled, the cluster is dedicated to GPU instance service and cannot be used for model service.',
|
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
|
||||||
'clusters.gpuInstances.staticAddress':
|
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
|
||||||
'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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -185,11 +185,14 @@ export default {
|
|||||||
'clusters.namespace.title': 'Namespace',
|
'clusters.namespace.title': 'Namespace',
|
||||||
'clusters.namespace.tip':
|
'clusters.namespace.tip':
|
||||||
'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.clusterType.title': 'Cluster Type',
|
||||||
|
'clusters.modelService.title': 'Model Service',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'For LLM inference and API serving — e.g. exposing model APIs and token-based services.',
|
||||||
|
'clusters.gpuInstances.title': 'GPU Service',
|
||||||
'clusters.gpuInstances.tip':
|
'clusters.gpuInstances.tip':
|
||||||
'Once enabled, the cluster is dedicated to GPU instance service and cannot be used for model service.',
|
'For on-demand GPU compute — e.g. interactive development, training jobs, or custom environments.',
|
||||||
'clusters.gpuInstances.staticAddress':
|
'clusters.gpuInstances.staticAddress': 'GPU Service Static Access Address',
|
||||||
'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.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -177,10 +177,14 @@ export default {
|
|||||||
'clusters.namespace.title': '命名空间',
|
'clusters.namespace.title': '命名空间',
|
||||||
'clusters.namespace.tip':
|
'clusters.namespace.tip':
|
||||||
'集群清单渲染所使用的 Kubernetes 命名空间。留空则使用 gpustack-system。',
|
'集群清单渲染所使用的 Kubernetes 命名空间。留空则使用 gpustack-system。',
|
||||||
'clusters.gpuInstances.title': 'GPU 实例服务',
|
'clusters.clusterType.title': '集群类型',
|
||||||
|
'clusters.modelService.title': '模型服务',
|
||||||
|
'clusters.modelService.tip':
|
||||||
|
'适用于大模型推理与 API 服务化场景,例如对外提供模型 API 与 Token 服务能力。',
|
||||||
|
'clusters.gpuInstances.title': 'GPU 服务',
|
||||||
'clusters.gpuInstances.tip':
|
'clusters.gpuInstances.tip':
|
||||||
'启用后该集群将专用于 GPU 实例服务,不可用于模型服务。',
|
'适用于按需分配 GPU 计算资源的场景,例如交互式开发、训练任务或自定义运行环境。',
|
||||||
'clusters.gpuInstances.staticAddress': 'GPU实例服务静态访问地址',
|
'clusters.gpuInstances.staticAddress': 'GPU 服务静态访问地址',
|
||||||
'clusters.gpuInstances.staticAddress.tip':
|
'clusters.gpuInstances.staticAddress.tip':
|
||||||
'Operator 访问该集群 GPU 实例所使用的静态地址(例如 LoadBalancer VIP)。可选。'
|
'Operator 访问该集群 GPU 实例所使用的静态地址(例如 LoadBalancer VIP)。可选。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
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 K8sAdvancedOptions, {
|
import K8sAdvancedOptions, {
|
||||||
GpuInstanceServiceSwitch,
|
ClusterTypeSelector,
|
||||||
K8sOptionsChangeWatcher
|
K8sOptionsChangeWatcher
|
||||||
} from './k8s-pod-spec';
|
} from './k8s-pod-spec';
|
||||||
|
|
||||||
@@ -260,8 +260,8 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="description"
|
name="description"
|
||||||
rules={[{ required: false }]}
|
rules={[{ required: false }]}
|
||||||
// For Kubernetes the GPU instance service switch follows directly,
|
// For Kubernetes the cluster type selector follows directly, so
|
||||||
// so fall back to the default item margin (matching the name field)
|
// fall back to the default item margin (matching the name field)
|
||||||
// to keep the description spacing symmetric; other providers keep
|
// to keep the description spacing symmetric; other providers keep
|
||||||
// the tighter gap before the advanced panel.
|
// the tighter gap before the advanced panel.
|
||||||
style={{
|
style={{
|
||||||
@@ -275,9 +275,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
|
|||||||
></SealTextArea>
|
></SealTextArea>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{provider === ProviderValueMap.Kubernetes && (
|
{provider === ProviderValueMap.Kubernetes && <ClusterTypeSelector />}
|
||||||
<GpuInstanceServiceSwitch />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CollapsePanel
|
<CollapsePanel
|
||||||
accordion={false}
|
accordion={false}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import { Input as CInput, LabelSelector, SwitchCard } from '@gpustack/core-ui';
|
import { Input as CInput, LabelSelector } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useId } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { ClusterListItem as ListItem } from '../config/types';
|
import { ClusterListItem as ListItem } from '../config/types';
|
||||||
import ImageCredential from './image-credential';
|
import ImageCredential from './image-credential';
|
||||||
@@ -14,16 +14,6 @@ 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();
|
||||||
|
|
||||||
@@ -79,32 +69,123 @@ export const OperatorImageForm: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The presence of `gpuInstanceOptions` on `k8s_options` is the source of truth
|
// 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
|
// for whether GPU instances are enabled. Both the cluster-type selector
|
||||||
// the static-address field (rendered in the advanced section) watch this same
|
// (rendered up top) and the static-address field (rendered in the advanced
|
||||||
// path so they stay in sync without sharing local state.
|
// section) watch this same path so they stay in sync without sharing local
|
||||||
|
// state.
|
||||||
const GPU_INSTANCE_OPTIONS_PATH = ['k8s_options', 'gpuInstanceOptions'];
|
const GPU_INSTANCE_OPTIONS_PATH = ['k8s_options', 'gpuInstanceOptions'];
|
||||||
|
|
||||||
// Standalone switch shown directly under the cluster description. Toggling it
|
// Visual parity with @gpustack/core-ui's SwitchCard so the selector blends
|
||||||
// only flips the presence of `gpuInstanceOptions` on the form; the related
|
// in with surrounding form fields: same border, radius, padding, and
|
||||||
// inputs live in the advanced section.
|
// typography. The only differences are the two-column grid layout and an
|
||||||
export const GpuInstanceServiceSwitch: React.FC = () => {
|
// active state (blue border + tinted background) to mark the selection.
|
||||||
|
const ClusterTypeWrap = styled.div`
|
||||||
|
margin-bottom: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ClusterTypeLabel = styled.div`
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
.required {
|
||||||
|
color: var(--ant-color-error);
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ClusterTypeGrid = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ClusterTypeCard = styled.div<{ $active: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: var(--ant-border-radius-lg);
|
||||||
|
border: 1px solid
|
||||||
|
${(p) =>
|
||||||
|
p.$active ? 'var(--ant-color-primary)' : 'var(--ant-color-border)'};
|
||||||
|
background: ${(p) =>
|
||||||
|
p.$active ? 'var(--ant-color-primary-bg)' : 'transparent'};
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
background-color 0.2s;
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: var(--ant-color-primary);
|
||||||
|
}
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--ant-control-outline);
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RadioDot = styled.span<{ $active: boolean }>`
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-top: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1.5px solid
|
||||||
|
${(p) =>
|
||||||
|
p.$active ? 'var(--ant-color-primary)' : 'var(--ant-color-border)'};
|
||||||
|
background: ${(p) =>
|
||||||
|
p.$active ? 'var(--ant-color-primary)' : 'transparent'};
|
||||||
|
transition: all 0.2s;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
opacity: ${(p) => (p.$active ? 1 : 0)};
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Card-based selector for cluster type. The two options are mutually exclusive
|
||||||
|
// and the choice maps directly to the presence/absence of `gpuInstanceOptions`
|
||||||
|
// on the form — "model" clears it, "gpu" seeds it to {} (preserving any
|
||||||
|
// already-entered static address). No standalone form field is registered;
|
||||||
|
// state is read via useWatch with `preserve: true` so it tracks updates made
|
||||||
|
// through setFieldValue.
|
||||||
|
export const ClusterTypeSelector: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
// `preserve: true` makes useWatch read the full form store rather than only
|
const labelId = useId();
|
||||||
// registered fields — required here because `gpuInstanceOptions` is set via
|
|
||||||
// setFieldValue and has no mounted Form.Item of its own while the switch is
|
|
||||||
// off, so a default watch would never see it flip on.
|
|
||||||
const gpuInstanceOptions = Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
const gpuInstanceOptions = Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
||||||
form,
|
form,
|
||||||
preserve: true
|
preserve: true
|
||||||
});
|
});
|
||||||
const enabled = !!gpuInstanceOptions;
|
const value: 'model' | 'gpu' = gpuInstanceOptions ? 'gpu' : 'model';
|
||||||
|
|
||||||
const handleToggle = (checked: boolean) => {
|
const handleSelect = (next: 'model' | 'gpu') => {
|
||||||
if (!form) return;
|
if (!form || next === value) return;
|
||||||
// When on, ensure the object exists (defaulting to {} so it survives even
|
if (next === 'gpu') {
|
||||||
// when the static address is left blank); when off, remove it entirely.
|
|
||||||
if (checked) {
|
|
||||||
form.setFieldValue(
|
form.setFieldValue(
|
||||||
GPU_INSTANCE_OPTIONS_PATH,
|
GPU_INSTANCE_OPTIONS_PATH,
|
||||||
form.getFieldValue(GPU_INSTANCE_OPTIONS_PATH) ?? {}
|
form.getFieldValue(GPU_INSTANCE_OPTIONS_PATH) ?? {}
|
||||||
@@ -114,33 +195,68 @@ export const GpuInstanceServiceSwitch: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const options: {
|
||||||
|
key: 'model' | 'gpu';
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
key: 'model',
|
||||||
|
title: intl.formatMessage({ id: 'clusters.modelService.title' }),
|
||||||
|
description: intl.formatMessage({ id: 'clusters.modelService.tip' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gpu',
|
||||||
|
title: intl.formatMessage({ id: 'clusters.gpuInstances.title' }),
|
||||||
|
description: intl.formatMessage({ id: 'clusters.gpuInstances.tip' })
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SwitchCardWrap>
|
<ClusterTypeWrap>
|
||||||
<SwitchCard
|
<ClusterTypeLabel id={labelId}>
|
||||||
styles={{
|
{intl.formatMessage({ id: 'clusters.clusterType.title' })}
|
||||||
wrapper: {
|
<span className="required">*</span>
|
||||||
borderRadius: 'var(--ant-border-radius-lg)',
|
</ClusterTypeLabel>
|
||||||
paddingInline: 14,
|
<ClusterTypeGrid role="radiogroup" aria-labelledby={labelId}>
|
||||||
marginBottom: 24
|
{options.map((opt) => {
|
||||||
}
|
const active = value === opt.key;
|
||||||
}}
|
return (
|
||||||
value={enabled}
|
<ClusterTypeCard
|
||||||
onChange={handleToggle}
|
key={opt.key}
|
||||||
label={intl.formatMessage({ id: 'clusters.gpuInstances.title' })}
|
$active={active}
|
||||||
description={intl.formatMessage({ id: 'clusters.gpuInstances.tip' })}
|
role="radio"
|
||||||
/>
|
aria-checked={active}
|
||||||
</SwitchCardWrap>
|
tabIndex={0}
|
||||||
|
onClick={() => handleSelect(opt.key)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(opt.key);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RadioDot $active={active} />
|
||||||
|
<div className="body">
|
||||||
|
<div className="title">{opt.title}</div>
|
||||||
|
<div className="description">{opt.description}</div>
|
||||||
|
</div>
|
||||||
|
</ClusterTypeCard>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ClusterTypeGrid>
|
||||||
|
</ClusterTypeWrap>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Static access address for GPU instances. Only shown while the GPU instance
|
// Static access address for GPU instances. Only shown when "GPU 服务" is
|
||||||
// service switch is on, mirroring the previous in-card behaviour. Rendered in
|
// the selected cluster type. Rendered in the advanced section, between the
|
||||||
// the advanced section, between the default container registry and the worker
|
// default container registry and the worker config (节点配置).
|
||||||
// config (节点配置).
|
|
||||||
export const GpuInstancesStaticAddressForm: React.FC = () => {
|
export const GpuInstancesStaticAddressForm: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
// See note in GpuInstanceServiceSwitch: watch the full store so this field's
|
// See note in ClusterTypeSelector: watch the full store so this field's
|
||||||
// visibility tracks the switch even before it has mounted its own Form.Item.
|
// visibility tracks the selector even before it has mounted its own
|
||||||
|
// Form.Item.
|
||||||
const enabled = !!Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
const enabled = !!Form.useWatch(GPU_INSTANCE_OPTIONS_PATH, {
|
||||||
preserve: true
|
preserve: true
|
||||||
});
|
});
|
||||||
@@ -209,7 +325,7 @@ export const K8sOptionsChangeWatcher: React.FC<{
|
|||||||
}> = ({ action, currentData, onChange }) => {
|
}> = ({ action, currentData, onChange }) => {
|
||||||
// `preserve: true` so the watch tracks the full store, including
|
// `preserve: true` so the watch tracks the full store, including
|
||||||
// gpuInstanceOptions which is toggled via setFieldValue without a mounted
|
// gpuInstanceOptions which is toggled via setFieldValue without a mounted
|
||||||
// Form.Item (mirrors GpuInstanceServiceSwitch).
|
// Form.Item (mirrors ClusterTypeSelector).
|
||||||
const k8sOptions = Form.useWatch(['k8s_options'], { preserve: true });
|
const k8sOptions = Form.useWatch(['k8s_options'], { preserve: true });
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
|
|||||||
Reference in New Issue
Block a user