fix: align instance type fields to api

This commit is contained in:
jialin
2026-06-05 23:08:45 +08:00
committed by Lawrence Li
parent 0cabbeb81c
commit 9f5d95f565
23 changed files with 733 additions and 457 deletions
+219 -194
View File
@@ -36,6 +36,7 @@ import TemplateBasicForm, {
BasicResourceMax
} from '../../templates/forms/basic';
import { pickCandidateForAccelerator, StorageModeValueMap } from '../config';
import { FormContext } from '../config/form-context';
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
import instanceStyles from '../styles/instances.module.less';
import Basic from './basic';
@@ -121,6 +122,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const formAction =
realAction === PageAction.CREATE ? PageAction.CREATE : action;
const sshEnabled = Form.useWatch('enable_ssh', form);
const description = Form.useWatch(['spec', 'description'], form);
// `organization_id` is owned by the create-scope picker slot; it only
// exists/changes when a platform admin retargets the form. Watch it
// so the parent can re-scope offerings (see onScopeChange).
@@ -147,6 +149,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
: [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
});
const isGPUType = useMemo(() => {
const spec = parseJsonSafe(description || '{}', {} as any)?.spec;
return spec?.acceleratable;
}, [description]);
useEffect(() => {
if (open) {
fetchSSHData({ page: 1, perPage: 100 });
@@ -236,44 +243,42 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}
) => {
const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed;
const acceleratable = instanceType?.spec?.acceleratable;
const { count = 0 } = options;
if (acceleratable) {
return {
accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores
: undefined,
ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value
: undefined
};
}
return {};
return {
accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores
: null,
ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value
: null
};
};
const buildResourcesDataForSubmit = (values: FormData) => {
const unitResourcesParsed = getUnitResources();
const accelerator = _.toNumber(values.spec?.resources?.accelerator) || 0;
const cpuCount = _.toNumber(values.spec?.resources?.cpu) || 0;
const cpuNum = unitResourcesParsed?.cpu?.num;
const ramNum = unitResourcesParsed?.ram?.num;
const fallbackCpu = values.spec?.resources?.cpu;
const factor = isGPUType ? accelerator : cpuCount;
return {
cpu:
accelerator > 0 && cpuNum
? `${accelerator * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}`
: // Don't stringify an unset value — `${undefined}` becomes the
// literal "undefined", which fails k8s quantity validation.
fallbackCpu != null && fallbackCpu !== ''
? `${fallbackCpu}`
: undefined,
ram:
accelerator > 0 && ramNum
? `${accelerator * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
: values.spec?.resources?.ram
cpu: cpuNum
? `${factor * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}`
: // Don't stringify an unset value — `${undefined}` becomes the
// literal "undefined", which fails k8s quantity validation.
fallbackCpu
? `${fallbackCpu}`
: undefined,
ram: ramNum
? `${factor * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
: values.spec?.resources?.ram
};
};
@@ -284,31 +289,36 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
if (!instanceType) {
setSelectedInstanceType(undefined);
setOnceMaxRequest({ cpu: null, memory: null, localStorage: null });
// Also reset the instance-type-derived form fields (cluster, type and
// the CPU / memory / GPU-count resources) so the previous scope's
// numbers don't linger when the new org has no clusters.
const spec = form.getFieldValue('spec') || {};
form.setFieldsValue({
clusterId: null,
spec: {
...spec,
type: undefined,
type: null,
resources: {
...spec.resources,
accelerator: undefined,
cpu: undefined,
ram: undefined
accelerator: null,
cpu: null,
ram: null
}
}
} as any);
});
return;
}
const candidate = pickCandidateForAccelerator(
instanceType.status?.acceleratorTiers,
count
);
setSelectedInstanceType(instanceType);
const candidate = pickCandidateForAccelerator(
instanceType.status?.tiers,
{
count,
acceleratable: instanceType.spec?.acceleratable
}
);
console.log('picked candidate', candidate, instanceType, count);
setOnceMaxRequest({
cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores,
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest)?.value,
@@ -344,6 +354,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
});
const handleOnFinishFailed = (errorInfo: any) => {
console.log('formvalues===', form.getFieldsValue());
const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({
...field,
name: [
@@ -371,6 +382,15 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
realAction === PageAction.CREATE
) {
console.log('currentData', currentData);
const currentSpec = parseJsonSafe(
currentData?.description || '{}',
{} as any
)?.spec;
const count = currentSpec?.acceleratable
? _.toNumber(currentData?.spec?.resources?.accelerator)
: _.toNumber(currentData?.spec?.resources?.cpu) || 0;
form.setFieldsValue({
...currentData,
spec: {
@@ -380,8 +400,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
...buildResourcesData(
parseJsonSafe<any>(currentData?.description || '{}', {}),
{
count:
_.toNumber(currentData?.spec?.resources?.accelerator) || 0
count
}
)
}
@@ -456,15 +475,14 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
},
getFieldsValue: () => form.getFieldsValue(),
applyInstanceType: (instanceType?: InstanceTypeItem) => {
console.log('applying instance type', instanceType);
if (!instanceType) {
// Clear the current instance-type selection (e.g. the create scope
// switched to an org with no clusters) so its card/limits don't
// linger.
resolveAndApply(undefined, 0);
return;
}
const count = instanceType.spec?.acceleratable ? 1 : 0;
resolveAndApply(instanceType, count);
// set default to 1, for all instance types: GPU or non-GPU
resolveAndApply(instanceType, 1);
}
}));
@@ -490,7 +508,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const handleOnEnableSSHChange = (e: any) => {
const checked = e.target.checked;
console.log('enable ssh change', checked);
if (!checked) {
form.setFieldValue(['spec', 'sshPublicKeys'], []);
}
@@ -506,164 +523,172 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
segmentedTop={segmentedTop}
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
>
<Form
name="gpuServiceInstanceForm"
form={form}
disabled={disabled}
onFinish={handleFinish}
onFinishFailed={handleOnFinishFailed}
preserve={false}
initialValues={{
spec: {
description: '',
displayName: '',
image: '',
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [],
env: [],
volumeMount: '',
resources: {
accelerator: 1
},
volume: {
ephemeral: {
capacity: '50Gi'
},
persistent: {
name: ''
}
}
},
enable_ssh: false,
storageMode: StorageModeValueMap.Temporary
<FormContext.Provider
value={{
action: formAction,
currentData: currentData,
isGPUType: isGPUType
}}
>
<Basic action={formAction} disabled={disabled} />
<Form.Item name="clusterId" hidden>
<CInput.Input />
</Form.Item>
<CollapsePanel
activeKey={collapseKeys}
accordion={false}
onChange={handleOnCollapseChange}
items={[
{
key: TABKeysMap.INSTANCE_TYPE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.type'
}),
forceRender: true,
children: (
<InstanceTypeFormItem
action={formAction}
disabled={disabled}
selectedInstanceType={selectedInstanceType}
currentData={currentData as any}
onceMaxRequest={onceMaxRequest}
noAvailableTypes={noAvailableInstanceTypes}
onGPUCountChange={handleAcceleratorChange}
/>
)
<Form
name="gpuServiceInstanceForm"
form={form}
disabled={disabled}
onFinish={handleFinish}
onFinishFailed={handleOnFinishFailed}
preserve={false}
initialValues={{
spec: {
description: '',
displayName: '',
image: '',
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [],
env: [],
volumeMount: '',
resources: {
accelerator: 1
},
volume: {
ephemeral: {
capacity: '50Gi'
},
persistent: {
name: ''
}
}
},
{
key: TABKeysMap.TEMPLATE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
forceRender: true,
children: (
<TemplateBasicForm
page="instance"
disabled={disabled || formAction === PageAction.EDIT}
onceMaxRequest={onceMaxRequest}
/>
)
},
{
key: TABKeysMap.STORAGE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
forceRender: true,
children: (
<StorageVolume
disabled={disabled || formAction === PageAction.EDIT}
action={formAction}
/>
)
}
]}
/>
<Flex align="center" justify="space-between">
<Form.Item<FormData>
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
onChange={handleOnEnableSSHChange}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.enable'
})}
></CheckboxField>
enable_ssh: false,
storageMode: StorageModeValueMap.Temporary
}}
>
<Basic action={formAction} disabled={disabled} />
<Form.Item name="clusterId" hidden>
<CInput.Input />
</Form.Item>
<Button
size="small"
type="link"
icon={<PlusOutlined />}
onClick={handleAddSSHKey}
>
{intl.formatMessage({
id: 'gpuservice.instance.ssh.addKey'
})}
</Button>
</Flex>
<div className={instanceStyles.sshkeySelection}>
<Form.Item<FormData>
name={['spec', 'sshPublicKeys']}
style={{
marginBottom: 12
}}
hidden={!sshEnabled}
normalize={(value) =>
Array.isArray(value)
? value?.map((item) => ({ name: item }))
: []
}
getValueProps={(value) => ({
value: Array.isArray(value)
? value.map((item) => item?.name ?? item)
: []
})}
rules={[
<CollapsePanel
activeKey={collapseKeys}
accordion={false}
onChange={handleOnCollapseChange}
items={[
{
required: sshEnabled,
message: getRuleMessage('select', 'gpuservice.publicKey')
key: TABKeysMap.INSTANCE_TYPE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.type'
}),
forceRender: true,
children: (
<InstanceTypeFormItem
action={formAction}
disabled={disabled}
selectedInstanceType={selectedInstanceType}
currentData={currentData as any}
onceMaxRequest={onceMaxRequest}
noAvailableTypes={noAvailableInstanceTypes}
onGPUCountChange={handleAcceleratorChange}
/>
)
},
{
key: TABKeysMap.TEMPLATE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
forceRender: true,
children: (
<TemplateBasicForm
page="instance"
disabled={disabled || formAction === PageAction.EDIT}
onceMaxRequest={onceMaxRequest}
/>
)
},
{
key: TABKeysMap.STORAGE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
forceRender: true,
children: (
<StorageVolume
disabled={disabled || formAction === PageAction.EDIT}
action={formAction}
/>
)
}
]}
>
<MultipleSelect
required
disabled={disabled}
mode="multiple"
maxTagCount={'responsive'}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.assignKey'
/>
<Flex align="center" justify="space-between">
<Form.Item<FormData>
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
onChange={handleOnEnableSSHChange}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.enable'
})}
></CheckboxField>
</Form.Item>
<Button
size="small"
type="link"
icon={<PlusOutlined />}
onClick={handleAddSSHKey}
>
{intl.formatMessage({
id: 'gpuservice.instance.ssh.addKey'
})}
styles={{
wrapper: {
width: '100%'
}
</Button>
</Flex>
<div className={instanceStyles.sshkeySelection}>
<Form.Item<FormData>
name={['spec', 'sshPublicKeys']}
style={{
marginBottom: 12
}}
options={sshkeyOptions}
></MultipleSelect>
</Form.Item>
</div>
</Form>
hidden={!sshEnabled}
normalize={(value) =>
Array.isArray(value)
? value?.map((item) => ({ name: item }))
: []
}
getValueProps={(value) => ({
value: Array.isArray(value)
? value.map((item) => item?.name ?? item)
: []
})}
rules={[
{
required: sshEnabled,
message: getRuleMessage('select', 'gpuservice.publicKey')
}
]}
>
<MultipleSelect
required
disabled={disabled}
mode="multiple"
maxTagCount={'responsive'}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.assignKey'
})}
styles={{
wrapper: {
width: '100%'
}
}}
options={sshkeyOptions}
></MultipleSelect>
</Form.Item>
</div>
</Form>
</FormContext.Provider>
<PublicKeyOverlay
open={sshOverlayOpen}
onCancel={() => setSshOverlayOpen(false)}