feat(gpu-service): allow re-typing stopped instances on edit
- Add instance-type column to edit drawer for stopped instances - Extract saveInstanceDataInDescription into shared util - Floor sliced unit resources; keep CPU >= 1 core - Expose applyInstanceType for both create and edit flows
This commit is contained in:
@@ -13,15 +13,16 @@ import {
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl, useModel } from '@umijs/max';
|
||||
import { Input, Typography } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ListItem as TemplateItem } from '../../templates/config/types';
|
||||
import useQueryTemplates from '../../templates/services/use-query-templates';
|
||||
import { InstanceStatusValueMap } from '../config';
|
||||
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
|
||||
import GPUServiceInstanceForm from '../forms';
|
||||
import TemplateSelector, { TemplateGroup } from '../forms/template-selector';
|
||||
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
||||
import styles from '../styles/instances.module.less';
|
||||
import { saveInstanceDataInDescription } from '../utils/instance-description';
|
||||
import InstanceTypeList from './instance-type-list';
|
||||
|
||||
type AddModalProps = {
|
||||
@@ -103,6 +104,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
manufacturer: undefined
|
||||
});
|
||||
const [templateId, setTemplateId] = useState<number | undefined>();
|
||||
// Re-selected instance type on a stopped-instance edit. Kept separate from
|
||||
// `instanceTypeSelection` (the create card selection) so the two flows don't
|
||||
// couple; starts empty each open (no default highlight).
|
||||
const [editSelectedType, setEditSelectedType] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||
const { loading, guard, run, release } = useSubmitLock();
|
||||
@@ -169,6 +176,13 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
const isRecreate = realAction === PageAction.CREATE;
|
||||
const showResourceSelectors = action === PageAction.CREATE || isRecreate;
|
||||
const shouldAutoSelectResource = action === PageAction.CREATE && !isRecreate;
|
||||
// Only a stopped instance can be re-typed on edit. It shows the instance-type
|
||||
// column (but not the template column) beside the form; the create card
|
||||
// columns render for CREATE / recreate.
|
||||
const isStoppedEdit =
|
||||
action === PageAction.EDIT &&
|
||||
data?.status?.phase === InstanceStatusValueMap.Stopped;
|
||||
const showInstanceTypeColumn = showResourceSelectors || isStoppedEdit;
|
||||
|
||||
const findTemplateByManufacturer = (
|
||||
manufacturer: string | undefined,
|
||||
@@ -179,20 +193,6 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const saveInstanceDataInDescription = (instanceType: InstanceTypeItem) => {
|
||||
return JSON.stringify({
|
||||
name: instanceType.name,
|
||||
spec: {
|
||||
..._.omit(instanceType.spec, ['cache', 'cpu']),
|
||||
cpu: _.pick(instanceType.spec?.cpu, [
|
||||
'manufacturer',
|
||||
'product',
|
||||
'family'
|
||||
])
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// GPU types carry their accelerator vendor; non-acceleratable (CPU) types
|
||||
// all map to the single 'cpu' bucket used to match templates.
|
||||
const manufacturerOf = (instanceType: InstanceTypeItem) =>
|
||||
@@ -359,6 +359,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
manufacturer: undefined
|
||||
});
|
||||
setTemplateId(undefined);
|
||||
setEditSelectedType(undefined);
|
||||
setInstanceKeyword('');
|
||||
setTemplateKeyword('');
|
||||
setScopeOrgId(undefined);
|
||||
@@ -367,6 +368,10 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
|
||||
if (action === PageAction.CREATE) {
|
||||
loadCreateResources();
|
||||
} else if (action === PageAction.EDIT) {
|
||||
// Edit has no card columns, but the change-type overlay still needs the
|
||||
// full instance-type list to re-type a stopped instance.
|
||||
fetchData({ page: -1 });
|
||||
}
|
||||
}, [open, shouldAutoSelectResource, action]);
|
||||
|
||||
@@ -506,6 +511,17 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
applySelection(item, template);
|
||||
};
|
||||
|
||||
// Stopped-edit re-type. Decoupled from applySelection (the create flow): it
|
||||
// only snapshots the type into `description` and applies it to the form — no
|
||||
// template selection or filtering.
|
||||
const handleEditInstanceTypeChange = (item: InstanceTypeItem) => {
|
||||
setEditSelectedType(item.name);
|
||||
form.current?.setFieldsValue({
|
||||
description: saveInstanceDataInDescription(item)
|
||||
});
|
||||
form.current?.applyInstanceType?.(item);
|
||||
};
|
||||
|
||||
const handleTemplateChange = (id: number, item: TemplateItem) => {
|
||||
setTemplateId(id);
|
||||
const formValues = form.current?.getFieldsValue();
|
||||
@@ -543,87 +559,102 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
footer={false}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
{showInstanceTypeColumn && (
|
||||
<div
|
||||
className={styles.colWrapper}
|
||||
// The 33% cap suits the 3-column create layout; in the 2-column
|
||||
// stopped-edit layout, split the space evenly with the form column.
|
||||
style={isStoppedEdit ? { flex: 1, maxWidth: 'none' } : undefined}
|
||||
>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.types'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.type.placeholder'
|
||||
})}
|
||||
value={instanceKeyword}
|
||||
onChange={(e) => setInstanceKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<InstanceTypeList
|
||||
// Edit (stopped) re-selection is decoupled from create's
|
||||
// card selection: separate highlight state + apply handler.
|
||||
value={
|
||||
isStoppedEdit
|
||||
? editSelectedType
|
||||
: instanceTypeSelection.instanceType
|
||||
}
|
||||
dataList={filteredInstanceTypes}
|
||||
loading={instanceTypesLoading}
|
||||
onChange={
|
||||
isStoppedEdit
|
||||
? handleEditInstanceTypeChange
|
||||
: handleInstanceTypeChange
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
)}
|
||||
{showResourceSelectors && (
|
||||
<>
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.types'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.type.placeholder'
|
||||
})}
|
||||
value={instanceKeyword}
|
||||
onChange={(e) => setInstanceKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<InstanceTypeList
|
||||
value={instanceTypeSelection.instanceType}
|
||||
dataList={filteredInstanceTypes}
|
||||
loading={instanceTypesLoading}
|
||||
onChange={handleInstanceTypeChange}
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.templates'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.template.placeholder'
|
||||
})}
|
||||
value={templateKeyword}
|
||||
onChange={(e) => setTemplateKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
<div className={styles.colWrapper}>
|
||||
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
|
||||
<div className={styles.panelBody}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'var(--ant-color-bg-elevated)'
|
||||
}}
|
||||
>
|
||||
<ColTitle style={{ paddingBottom: 0 }}>
|
||||
{intl.formatMessage({
|
||||
id: 'gpuservice.instance.templates'
|
||||
})}
|
||||
</ColTitle>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined className="text-tertiary" />}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'gpuservice.instance.search.template.placeholder'
|
||||
})}
|
||||
value={templateKeyword}
|
||||
onChange={(e) => setTemplateKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<TemplateSelector
|
||||
value={templateId}
|
||||
loading={templateLoading || !initialized}
|
||||
groups={templateGroups}
|
||||
onChange={handleTemplateChange}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
</>
|
||||
<TemplateSelector
|
||||
value={templateId}
|
||||
loading={templateLoading || !initialized}
|
||||
groups={templateGroups}
|
||||
onChange={handleTemplateChange}
|
||||
/>
|
||||
</div>
|
||||
</ColumnWrapper>
|
||||
<Separator></Separator>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.formWrapper}>
|
||||
<ColumnWrapper
|
||||
|
||||
@@ -54,6 +54,7 @@ const Meta = styled.div<{ $columns?: number }>`
|
||||
|
||||
interface InstanceTypeItemProps {
|
||||
item: InstanceTypeItemModel;
|
||||
action?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface MetadataSectionProps {
|
||||
@@ -212,7 +213,10 @@ export const InstanceMetadataSection: React.FC<MetadataSectionProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||
item,
|
||||
action
|
||||
}) => {
|
||||
const specData = item.spec || {};
|
||||
|
||||
const { acceleratable, manufacturer, displayName, cpuManufacturer } =
|
||||
@@ -264,6 +268,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
name="InstanceTypeBillingBadge"
|
||||
context={{ instanceType: item }}
|
||||
/>
|
||||
{action && <div style={{ marginLeft: 8 }}>{action}</div>}
|
||||
</Flex>
|
||||
</Title>
|
||||
<InstanceMetadataSection
|
||||
|
||||
@@ -335,16 +335,15 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
form.setFieldsValue({
|
||||
spec: {
|
||||
resources: {
|
||||
// Display the precise (rounded) fractional values — the inputs are
|
||||
// disabled, so decimals are fine and match the submitted
|
||||
// millicore / MiB allocation better than a floored integer.
|
||||
// Floor the scaled unit resources to whole units; CPU never drops
|
||||
// below 1 core so a small slice still gets a usable vCPU.
|
||||
cpu:
|
||||
cpuCores != null && percentage > 0
|
||||
? _.round((cpuCores * percentage) / 100, 2)
|
||||
? Math.max(1, _.floor((cpuCores * percentage) / 100))
|
||||
: null,
|
||||
ram:
|
||||
ramValue != null && percentage > 0
|
||||
? _.round((ramValue * percentage) / 100, 2)
|
||||
? _.floor((ramValue * percentage) / 100)
|
||||
: null
|
||||
}
|
||||
}
|
||||
@@ -468,6 +467,38 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}
|
||||
};
|
||||
|
||||
// Apply a chosen instance type to the form: default to sliced mode for a
|
||||
// sliceable type with no whole-card capacity, otherwise whole-card with a
|
||||
// count of 1. Shared by the create card selection (imperative handle) and
|
||||
// the edit change-type overlay.
|
||||
const applyInstanceType = (instanceType?: InstanceTypeItem) => {
|
||||
if (!instanceType) {
|
||||
setSliceMode('whole');
|
||||
resolveAndApply(undefined, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// A sliceable type with no whole-card capacity (Max < 1) defaults to
|
||||
// sliced mode — whole mode would have nothing selectable.
|
||||
const wholeMax = instanceType.spec?.maxComputeUnitCount ?? 0;
|
||||
const slicedMax =
|
||||
_.toNumber(instanceType.status?.onceMaxRequest?.acceleratorSliced) || 0;
|
||||
const defaultSliced =
|
||||
!!instanceType.spec?.sliceable && wholeMax < 1 && slicedMax > 0;
|
||||
|
||||
if (defaultSliced) {
|
||||
setSliceMode('sliced');
|
||||
resolveAndApply(instanceType, 1, true);
|
||||
applySlicedDefaults(instanceType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise default to whole-card mode (a new type may not be
|
||||
// sliceable); set count to 1 for all instance types: GPU or non-GPU.
|
||||
setSliceMode('whole');
|
||||
resolveAndApply(instanceType, 1);
|
||||
};
|
||||
|
||||
const onTargetChange = (key: string) => {
|
||||
scrollTabsRef.current?.handleTargetChange(key);
|
||||
};
|
||||
@@ -614,34 +645,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
form.setFieldsValue(values as any);
|
||||
},
|
||||
getFieldsValue: () => form.getFieldsValue(),
|
||||
applyInstanceType: (instanceType?: InstanceTypeItem) => {
|
||||
if (!instanceType) {
|
||||
setSliceMode('whole');
|
||||
resolveAndApply(undefined, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// A sliceable type with no whole-card capacity (Max < 1) defaults to
|
||||
// sliced mode — whole mode would have nothing selectable.
|
||||
const wholeMax = instanceType.spec?.maxComputeUnitCount ?? 0;
|
||||
const slicedMax =
|
||||
_.toNumber(instanceType.status?.onceMaxRequest?.acceleratorSliced) ||
|
||||
0;
|
||||
const defaultSliced =
|
||||
!!instanceType.spec?.sliceable && wholeMax < 1 && slicedMax > 0;
|
||||
|
||||
if (defaultSliced) {
|
||||
setSliceMode('sliced');
|
||||
resolveAndApply(instanceType, 1, true);
|
||||
applySlicedDefaults(instanceType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise default to whole-card mode (a new type may not be
|
||||
// sliceable); set count to 1 for all instance types: GPU or non-GPU.
|
||||
setSliceMode('whole');
|
||||
resolveAndApply(instanceType, 1);
|
||||
}
|
||||
applyInstanceType
|
||||
}));
|
||||
|
||||
const handleAddSSHKey = () => {
|
||||
@@ -766,7 +770,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
children: (
|
||||
<TemplateBasicForm
|
||||
page="instance"
|
||||
disabled={disabled || formAction === PageAction.EDIT}
|
||||
disabled={disabled}
|
||||
onceMaxRequest={onceMaxRequest}
|
||||
/>
|
||||
)
|
||||
@@ -778,10 +782,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}),
|
||||
forceRender: true,
|
||||
children: (
|
||||
<StorageVolume
|
||||
disabled={disabled || formAction === PageAction.EDIT}
|
||||
action={formAction}
|
||||
/>
|
||||
<StorageVolume disabled={disabled} action={formAction} />
|
||||
)
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -95,8 +95,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
const form = Form.useFormInstance();
|
||||
const { isGPUType } = useContext(FormContext);
|
||||
|
||||
// In edit mode the type card is read-only until a type is re-picked from the
|
||||
// instance-type column (stopped instances only); once selected the section
|
||||
// behaves like create (editable count / slice controls, live capacity
|
||||
// labels).
|
||||
const readonlyType = action === PageAction.EDIT && !selectedInstanceType;
|
||||
|
||||
const maxComputeUnitCount = useMemo(() => {
|
||||
if (action === PageAction.EDIT) {
|
||||
if (readonlyType) {
|
||||
const description = parseJsonSafe(
|
||||
currentData?.description || '{}',
|
||||
{} as any
|
||||
@@ -104,25 +110,24 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
return description.spec?.maxComputeUnitCount || 0;
|
||||
}
|
||||
return selectedInstanceType?.spec?.maxComputeUnitCount || 0;
|
||||
}, [action, currentData, selectedInstanceType]);
|
||||
}, [readonlyType, currentData, selectedInstanceType]);
|
||||
|
||||
const isGPU = useMemo(() => {
|
||||
if (action === PageAction.EDIT) {
|
||||
if (readonlyType) {
|
||||
return _.toNumber(currentData?.spec?.resources?.accelerator) > 0;
|
||||
}
|
||||
return selectedInstanceType?.spec?.acceleratable;
|
||||
}, [selectedInstanceType, action]);
|
||||
}, [selectedInstanceType, readonlyType, currentData]);
|
||||
|
||||
const handleOnGPUCountChange = (value: number) => {
|
||||
onGPUCountChange?.(value);
|
||||
};
|
||||
|
||||
// Sliced mode is only offered for sliceable accelerator types, and only when
|
||||
// the section is editable (create / recreate; edit renders a readonly card).
|
||||
// the section is editable (create / recreate, or edit after re-picking a
|
||||
// type; a not-yet-re-typed edit renders a readonly card).
|
||||
const showModeSwitch =
|
||||
action !== PageAction.EDIT &&
|
||||
isGPUType &&
|
||||
!!selectedInstanceType?.spec?.sliceable;
|
||||
!readonlyType && isGPUType && !!selectedInstanceType?.spec?.sliceable;
|
||||
|
||||
const handleModeChange = (value: string) => {
|
||||
onSliceModeChange?.(value as 'whole' | 'sliced');
|
||||
@@ -191,7 +196,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
};
|
||||
|
||||
const renderMemoryLabel = (): React.ReactNode => {
|
||||
if (isGPUType || action === PageAction.EDIT || !onceMaxRequest?.memory) {
|
||||
if (isGPUType || readonlyType || !onceMaxRequest?.memory) {
|
||||
return intl.formatMessage({ id: 'gpuservice.template.memory' });
|
||||
}
|
||||
|
||||
@@ -257,13 +262,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
}
|
||||
]}
|
||||
>
|
||||
{action === PageAction.CREATE && (
|
||||
{readonlyType ? (
|
||||
renderInstanceType()
|
||||
) : (
|
||||
<InstanceTypePicker
|
||||
selectedInstanceType={selectedInstanceType}
|
||||
noAvailable={noAvailableTypes}
|
||||
/>
|
||||
)}
|
||||
{action === PageAction.EDIT && renderInstanceType()}
|
||||
</Form.Item>
|
||||
</FieldBlock>
|
||||
{!noAvailableTypes && (
|
||||
@@ -275,7 +281,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
: ['spec', 'resources', 'cpu']
|
||||
}
|
||||
preserve
|
||||
hidden={action === PageAction.EDIT || isSliced}
|
||||
hidden={readonlyType || isSliced}
|
||||
normalize={(value) => (value != null ? _.toString(value) : undefined)}
|
||||
getValueProps={(value) => ({
|
||||
value: value != null ? _.toNumber(value) : undefined
|
||||
@@ -317,7 +323,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
step={1}
|
||||
required
|
||||
labelExtra={sliceMode === 'whole' ? modeSegmented : undefined}
|
||||
disabled={disabled || action === PageAction.EDIT}
|
||||
disabled={disabled || readonlyType}
|
||||
label={`${intl.formatMessage({ id: 'common.max.count' }, { label: numberSelectionLabel.label })} (${intl.formatMessage(
|
||||
{
|
||||
id: 'common.max'
|
||||
@@ -391,10 +397,10 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
</Form.Item>
|
||||
</FieldBlock>
|
||||
)}
|
||||
{/* Edit renders a readonly card (no sliced UI), so register the slice
|
||||
percentages as hidden fields — otherwise their persisted values are
|
||||
dropped from the submit payload. */}
|
||||
{action === PageAction.EDIT && (
|
||||
{/* A not-yet-re-typed edit renders a readonly card (no sliced UI), so
|
||||
register the slice percentages as hidden fields — otherwise their
|
||||
persisted values are dropped from the submit payload. */}
|
||||
{readonlyType && (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'resources', 'acceleratorSlicedMemoryPercentage']}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PageActionType } from '@/config/types';
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useState } from 'react';
|
||||
import { InstanceStatusValueMap } from '../config';
|
||||
import type { ListItem } from '../config/types';
|
||||
|
||||
const useCreateInstance = () => {
|
||||
@@ -52,11 +53,14 @@ const useCreateInstance = () => {
|
||||
};
|
||||
|
||||
const openEditInstanceModal = (row: ListItem) => {
|
||||
// A stopped instance can be re-typed, so it needs the two-column layout
|
||||
// (instance-type list + form); other statuses edit in a single column.
|
||||
const isStopped = row.status?.phase === InstanceStatusValueMap.Stopped;
|
||||
openModal(
|
||||
PageAction.EDIT,
|
||||
intl.formatMessage({ id: 'gpuservice.instance.edit' }),
|
||||
row,
|
||||
600
|
||||
isStopped ? 'min(1040px, calc(100vw - 220px))' : 600
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import _ from 'lodash';
|
||||
import { InstanceTypeItem } from '../config/types';
|
||||
|
||||
// Serialize the chosen instance type into the instance's `description` field —
|
||||
// a persisted spec snapshot the form reads back to render the type card and
|
||||
// derive unit resources. Shared by the create/recreate flow (card selection)
|
||||
// and the edit flow (change-type overlay).
|
||||
export const saveInstanceDataInDescription = (
|
||||
instanceType: InstanceTypeItem
|
||||
): string => {
|
||||
return JSON.stringify({
|
||||
name: instanceType.name,
|
||||
spec: {
|
||||
..._.omit(instanceType.spec, ['cache', 'cpu']),
|
||||
cpu: _.pick(instanceType.spec?.cpu, ['manufacturer', 'product', 'family'])
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user