fix: align instance type fields to api
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user