feat(gpu-service): restrict non-stopped instance edit, drop recreate

- non-stopped edit: only displayName and SSH public keys editable;
  type/template/storage sections disabled (stopped edit keeps all editable)
- remove recreate feature and the realAction prop it required:
  entry, delete-then-create submit path, warning banner, locale keys
This commit is contained in:
jialin
2026-07-23 10:24:54 +08:00
parent 89a74ed26a
commit e919b22d98
11 changed files with 41 additions and 129 deletions
-3
View File
@@ -178,9 +178,6 @@ export default {
'Only events from the last hour are shown', 'Only events from the last hour are shown',
'gpuservice.instance.event.tab.instance': 'Instance Events', 'gpuservice.instance.event.tab.instance': 'Instance Events',
'gpuservice.instance.event.tab.volume': 'Volume Events', 'gpuservice.instance.event.tab.volume': 'Volume Events',
'gpuservice.instance.recreate.confirm.title': 'Confirm recreation',
'gpuservice.instance.recreate.confirm.content':
'The current instance will be deleted first, then recreated with the current configuration.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'gpuservice.storage': 'Storage', 'gpuservice.storage': 'Storage',
'gpuservice.storage.add': 'Add Storage', 'gpuservice.storage.add': 'Add Storage',
'gpuservice.storage.edit': 'Edit Storage', 'gpuservice.storage.edit': 'Edit Storage',
-3
View File
@@ -177,9 +177,6 @@ export default {
'直近 1 時間のイベントのみ表示されます', '直近 1 時間のイベントのみ表示されます',
'gpuservice.instance.event.tab.instance': 'インスタンスイベント', 'gpuservice.instance.event.tab.instance': 'インスタンスイベント',
'gpuservice.instance.event.tab.volume': 'ボリュームイベント', 'gpuservice.instance.event.tab.volume': 'ボリュームイベント',
'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか',
'gpuservice.instance.recreate.confirm.content':
'現在のインスタンスを削除した後、現在の構成で再作成します。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'gpuservice.storage': 'ストレージ', 'gpuservice.storage': 'ストレージ',
'gpuservice.storage.add': 'ストレージを追加', 'gpuservice.storage.add': 'ストレージを追加',
'gpuservice.storage.edit': 'ストレージを編集', 'gpuservice.storage.edit': 'ストレージを編集',
-3
View File
@@ -176,9 +176,6 @@ export default {
'Отображаются только события за последний час', 'Отображаются только события за последний час',
'gpuservice.instance.event.tab.instance': 'События экземпляра', 'gpuservice.instance.event.tab.instance': 'События экземпляра',
'gpuservice.instance.event.tab.volume': 'События тома', 'gpuservice.instance.event.tab.volume': 'События тома',
'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание',
'gpuservice.instance.recreate.confirm.content':
'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'gpuservice.storage': 'Хранилище', 'gpuservice.storage': 'Хранилище',
'gpuservice.storage.add': 'Добавить хранилище', 'gpuservice.storage.add': 'Добавить хранилище',
'gpuservice.storage.edit': 'Редактировать хранилище', 'gpuservice.storage.edit': 'Редактировать хранилище',
-4
View File
@@ -172,10 +172,6 @@ export default {
'Yalnızca son bir saatteki olaylar gösterilir', 'Yalnızca son bir saatteki olaylar gösterilir',
'gpuservice.instance.event.tab.instance': 'Örnek Olayları', 'gpuservice.instance.event.tab.instance': 'Örnek Olayları',
'gpuservice.instance.event.tab.volume': 'Birim Olayları', 'gpuservice.instance.event.tab.volume': 'Birim Olayları',
'gpuservice.instance.recreate.confirm.title':
'Yeniden oluşturma onaylansın mı',
'gpuservice.instance.recreate.confirm.content':
'Mevcut örnek önce silinecek, ardından mevcut yapılandırmayla yeniden oluşturulacaktır.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'gpuservice.storage': 'Depolama', 'gpuservice.storage': 'Depolama',
'gpuservice.storage.add': 'Depolama Ekle', 'gpuservice.storage.add': 'Depolama Ekle',
'gpuservice.storage.edit': 'Depolamayı Düzenle', 'gpuservice.storage.edit': 'Depolamayı Düzenle',
-3
View File
@@ -162,9 +162,6 @@ export default {
'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。', 'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。',
'gpuservice.instance.event.tab.instance': '实例事件', 'gpuservice.instance.event.tab.instance': '实例事件',
'gpuservice.instance.event.tab.volume': '存储卷事件', 'gpuservice.instance.event.tab.volume': '存储卷事件',
'gpuservice.instance.recreate.confirm.title': '确认重新创建',
'gpuservice.instance.recreate.confirm.content':
'系统将先删除当前实例,然后使用当前配置重新创建。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
'gpuservice.storage': '存储', 'gpuservice.storage': '存储',
'gpuservice.storage.add': '添加存储', 'gpuservice.storage.add': '添加存储',
'gpuservice.storage.edit': '编辑存储', 'gpuservice.storage.edit': '编辑存储',
@@ -5,12 +5,7 @@ import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory';
import Separator from '@/pages/llmodels/components/separator'; import Separator from '@/pages/llmodels/components/separator';
import { getGPUStackPlugin } from '@/plugins'; import { getGPUStackPlugin } from '@/plugins';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
AlertBlockInfo,
ColumnWrapper,
GSDrawer,
ModalFooter
} from '@gpustack/core-ui';
import { useIntl, useModel } from '@umijs/max'; import { useIntl, useModel } from '@umijs/max';
import { Input, Typography } from 'antd'; import { Input, Typography } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
@@ -30,7 +25,6 @@ type AddModalProps = {
action: PageActionType; action: PageActionType;
open: boolean; open: boolean;
width?: number | string; width?: number | string;
realAction?: string;
clusterList?: Array<{ clusterList?: Array<{
label: string; label: string;
value: number; value: number;
@@ -84,8 +78,7 @@ const AddModal: React.FC<AddModalProps> = ({
data, data,
onCancel, onCancel,
width, width,
clusterList = [], clusterList = []
realAction
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { initialState } = useModel('@@initialState') || {}; const { initialState } = useModel('@@initialState') || {};
@@ -173,16 +166,18 @@ const AddModal: React.FC<AddModalProps> = ({
); );
// const readonly = action === PageAction.VIEW; // const readonly = action === PageAction.VIEW;
const readonly = false; const readonly = false;
const isRecreate = realAction === PageAction.CREATE; const showResourceSelectors = action === 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 // 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 // column (but not the template column) beside the form; the create card
// columns render for CREATE / recreate. // columns render for CREATE.
const isStoppedEdit = const isStoppedEdit =
action === PageAction.EDIT && action === PageAction.EDIT &&
data?.status?.phase === InstanceStatusValueMap.Stopped; data?.status?.phase === InstanceStatusValueMap.Stopped;
const showInstanceTypeColumn = showResourceSelectors || isStoppedEdit; const showInstanceTypeColumn = showResourceSelectors || isStoppedEdit;
// Editing a non-stopped instance is restricted: only displayName and the
// SSH public keys stay editable; the type / template / storage sections
// render disabled. A stopped instance edits everything.
const isRestrictedEdit = action === PageAction.EDIT && !isStoppedEdit;
const findTemplateByManufacturer = ( const findTemplateByManufacturer = (
manufacturer: string | undefined, manufacturer: string | undefined,
@@ -266,43 +261,12 @@ const AddModal: React.FC<AddModalProps> = ({
); );
}; };
const findAggregateOf = (
candidateName: string | undefined,
clusterId: number | null | undefined,
instanceTypes: InstanceTypeItem[]
): InstanceTypeItem | undefined => {
if (!candidateName) return undefined;
return instanceTypes.find((item) =>
(item.status?.tiers ?? []).some((tier) =>
(tier.candidates ?? []).some(
(c) => c.name === candidateName && Number(c.cluster) === clusterId
)
)
);
};
// initial for first // initial for first
const applyAutoSelection = ( const applyAutoSelection = (
instanceTypes: InstanceTypeItem[], instanceTypes: InstanceTypeItem[],
templates: TemplateItem[], templates: TemplateItem[],
orgId?: number | null orgId?: number | null
) => { ) => {
// On edit / view, surface the persisted selection in the card list.
if (!shouldAutoSelectResource) {
const aggregate = findAggregateOf(
data?.spec?.type,
data?.clusterId,
instanceTypes
);
if (aggregate) {
setInstanceTypeSelection({
instanceType: aggregate.name,
manufacturer: manufacturerOf(aggregate)
});
}
return;
}
// Scope to clusters the chosen org owns (admin "All" view). // Scope to clusters the chosen org owns (admin "All" view).
const owned = filterTypesByOwner(instanceTypes, orgId); const owned = filterTypesByOwner(instanceTypes, orgId);
@@ -376,7 +340,7 @@ const AddModal: React.FC<AddModalProps> = ({
// full instance-type list to re-type a stopped instance. // full instance-type list to re-type a stopped instance.
fetchData({ page: -1 }); fetchData({ page: -1 });
} }
}, [open, shouldAutoSelectResource, action]); }, [open, action]);
// filter instance types (already scoped to the chosen org's clusters) // filter instance types (already scoped to the chosen org's clusters)
const filteredInstanceTypes = ownedInstanceTypes.filter((item) => const filteredInstanceTypes = ownedInstanceTypes.filter((item) =>
@@ -664,17 +628,6 @@ const AddModal: React.FC<AddModalProps> = ({
styles={{ container: { paddingBlock: 0 } }} styles={{ container: { paddingBlock: 0 } }}
footer={ footer={
<> <>
{isRecreate && open && (
<div style={{ marginInline: 24, paddingTop: 8 }}>
<AlertBlockInfo
type="warning"
contentStyle={{ paddingInline: 0 }}
message={intl.formatMessage({
id: 'gpuservice.instance.recreate.confirm.content'
})}
/>
</div>
)}
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
@@ -698,9 +651,9 @@ const AddModal: React.FC<AddModalProps> = ({
<GPUServiceInstanceForm <GPUServiceInstanceForm
ref={form} ref={form}
action={action} action={action}
realAction={realAction}
currentData={data} currentData={data}
disabled={readonly} disabled={readonly}
restrictedEdit={isRestrictedEdit}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release} onFinishFailed={release}
onScopeChange={handleScopeChange} onScopeChange={handleScopeChange}
+22 -21
View File
@@ -59,7 +59,7 @@ interface InstanceFormProps {
ref?: any; ref?: any;
open: boolean; open: boolean;
action: PageActionType; action: PageActionType;
realAction?: PageActionType | string; // Present on edit / view.
currentData?: ListItem | null; currentData?: ListItem | null;
namespace?: string; namespace?: string;
instanceTypeList?: InstanceTypeItem[]; instanceTypeList?: InstanceTypeItem[];
@@ -67,6 +67,9 @@ interface InstanceFormProps {
// surfaces a "no available instance type" message in the scheduling tab. // surfaces a "no available instance type" message in the scheduling tab.
noAvailableInstanceTypes?: boolean; noAvailableInstanceTypes?: boolean;
disabled?: boolean; disabled?: boolean;
// Editing a non-stopped instance: only displayName and the SSH public keys
// stay editable; the type / template / storage sections render disabled.
restrictedEdit?: boolean;
// Fired when the create-scope picker retargets the form to another // Fired when the create-scope picker retargets the form to another
// org (or Global). Only emitted on genuine changes — never on the // org (or Global). Only emitted on genuine changes — never on the
// initial mount, and never in builds where the picker isn't mounted // initial mount, and never in builds where the picker isn't mounted
@@ -111,9 +114,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
(props, ref) => { (props, ref) => {
const { const {
action, action,
realAction,
currentData, currentData,
disabled, disabled,
restrictedEdit,
open, open,
instanceTypeList = [], instanceTypeList = [],
noAvailableInstanceTypes, noAvailableInstanceTypes,
@@ -125,8 +128,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const [form] = Form.useForm<InstanceFormValues>(); const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null); const scrollTabsRef = useRef<any>(null);
const formAction = // Restricted (non-stopped) edit disables the type / template / storage
realAction === PageAction.CREATE ? PageAction.CREATE : action; // sections; displayName and the SSH public keys keep following `disabled`.
const sectionDisabled = disabled || restrictedEdit;
const sshEnabled = Form.useWatch('enable_ssh', form); const sshEnabled = Form.useWatch('enable_ssh', form);
const description = Form.useWatch(['description'], form); const description = Form.useWatch(['description'], form);
// `organization_id` is owned by the create-scope picker slot; it only // `organization_id` is owned by the create-scope picker slot; it only
@@ -464,8 +468,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}; };
// Whole-card (exclusive) vs sliced (percentage) mode. Only meaningful for // Whole-card (exclusive) vs sliced (percentage) mode. Only meaningful for
// sliceable accelerator types; derived (no persisted field) — on edit/ // sliceable accelerator types; derived (no persisted field) — on edit it
// recreate it is inferred from acceleratorSlicedMemoryPercentage > 0. // is inferred from acceleratorSlicedMemoryPercentage > 0.
const [sliceMode, setSliceMode] = useState<'whole' | 'sliced'>('whole'); const [sliceMode, setSliceMode] = useState<'whole' | 'sliced'>('whole');
const handleAcceleratorChange = (count: number) => { const handleAcceleratorChange = (count: number) => {
@@ -582,11 +586,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
return; return;
} }
if ( // Prefill from the source row on edit / view.
action === PageAction.EDIT || if (currentData) {
action === PageAction.VIEW ||
realAction === PageAction.CREATE
) {
console.log('currentData', currentData); console.log('currentData', currentData);
const currentSpec = parseJsonSafe( const currentSpec = parseJsonSafe(
currentData?.description || '{}', currentData?.description || '{}',
@@ -597,8 +598,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
? _.toNumber(currentData?.spec?.resources?.accelerator) ? _.toNumber(currentData?.spec?.resources?.accelerator)
: _.toNumber(currentData?.spec?.resources?.cpu) || 0; : _.toNumber(currentData?.spec?.resources?.cpu) || 0;
// Infer the mode from the persisted slice percentage (recreate keeps // Infer the mode from the persisted slice percentage (edit/view
// the section editable; edit/view render a readonly card). // render a readonly card).
const persistedSliced = const persistedSliced =
_.toNumber( _.toNumber(
currentData?.spec?.resources?.acceleratorSlicedMemoryPercentage currentData?.spec?.resources?.acceleratorSlicedMemoryPercentage
@@ -624,12 +625,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}); });
// buildResourcesData above filled CPU / RAM for the whole card; rescale // buildResourcesData above filled CPU / RAM for the whole card; rescale
// them off the persisted percentages when recreating a sliced instance. // them off the persisted percentages for a sliced instance.
if (persistedSliced) { if (persistedSliced) {
applySlicedResourceScaling(); applySlicedResourceScaling();
} }
} }
}, [action, currentData, form, open, realAction, instanceTypeList]); }, [action, currentData, form, open, instanceTypeList]);
const getUnitResources = () => { const getUnitResources = () => {
if (selectedInstanceType?.spec?.unitResourcesParsed) { if (selectedInstanceType?.spec?.unitResourcesParsed) {
@@ -736,7 +737,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
> >
<FormContext.Provider <FormContext.Provider
value={{ value={{
action: formAction, action: action,
currentData: currentData, currentData: currentData,
isGPUType: isGPUType isGPUType: isGPUType
}} }}
@@ -780,7 +781,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
storageMode: StorageModeValueMap.Temporary storageMode: StorageModeValueMap.Temporary
}} }}
> >
<Basic action={formAction} disabled={disabled} /> <Basic action={action} disabled={disabled} />
<Form.Item name="clusterId" hidden> <Form.Item name="clusterId" hidden>
<CInput.Input /> <CInput.Input />
</Form.Item> </Form.Item>
@@ -797,8 +798,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
forceRender: true, forceRender: true,
children: ( children: (
<InstanceTypeFormItem <InstanceTypeFormItem
action={formAction} action={action}
disabled={disabled} disabled={sectionDisabled}
selectedInstanceType={selectedInstanceType} selectedInstanceType={selectedInstanceType}
currentData={currentData as any} currentData={currentData as any}
onceMaxRequest={onceMaxRequest} onceMaxRequest={onceMaxRequest}
@@ -820,7 +821,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
children: ( children: (
<TemplateBasicForm <TemplateBasicForm
page="instance" page="instance"
disabled={disabled} disabled={sectionDisabled}
onceMaxRequest={onceMaxRequest} onceMaxRequest={onceMaxRequest}
/> />
) )
@@ -832,7 +833,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}), }),
forceRender: true, forceRender: true,
children: ( children: (
<StorageVolume disabled={disabled} action={formAction} /> <StorageVolume disabled={sectionDisabled} action={action} />
) )
} }
]} ]}
@@ -136,8 +136,8 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
}; };
// Sliced mode is only offered for sliceable accelerator types, and only when // Sliced mode is only offered for sliceable accelerator types, and only when
// the section is editable (create / recreate, or edit after re-picking a // the section is editable (create, or edit after re-picking a type; a
// type; a not-yet-re-typed edit renders a readonly card). // not-yet-re-typed edit renders a readonly card).
const showModeSwitch = const showModeSwitch =
!readonlyType && !readonlyType &&
isGPUType && isGPUType &&
@@ -15,30 +15,26 @@ const useCreateInstance = () => {
title: string; title: string;
currentData?: ListItem | null; currentData?: ListItem | null;
width?: number | string; width?: number | string;
realAction?: string;
}>({ }>({
action: PageAction.CREATE, action: PageAction.CREATE,
title: '', title: '',
open: false, open: false,
width: undefined, width: undefined,
currentData: null, currentData: null
realAction: undefined
}); });
const openModal = ( const openModal = (
action: PageActionType, action: PageActionType,
title: string, title: string,
currentData?: ListItem | null, currentData?: ListItem | null,
width?: number | string, width?: number | string
realAction?: string
) => { ) => {
setOpenModalStatus({ setOpenModalStatus({
action, action,
title, title,
open: true, open: true,
currentData, currentData,
width, width
realAction
}); });
saveScrollHeight(); saveScrollHeight();
}; };
@@ -73,23 +69,12 @@ const useCreateInstance = () => {
); );
}; };
const openRecreateInstanceModal = (row: ListItem) => {
openModal(
PageAction.EDIT,
intl.formatMessage({ id: 'common.button.recreate' }),
row,
'calc(100vw - 220px)',
PageAction.CREATE
);
};
const closeModal = () => { const closeModal = () => {
setOpenModalStatus({ setOpenModalStatus({
...openModalStatus, ...openModalStatus,
title: '', title: '',
open: false, open: false,
currentData: null, currentData: null
realAction: undefined
}); });
restoreScrollHeight(); restoreScrollHeight();
}; };
@@ -101,7 +86,6 @@ const useCreateInstance = () => {
openCreateInstanceModal, openCreateInstanceModal,
openEditInstanceModal, openEditInstanceModal,
openViewInstanceModal, openViewInstanceModal,
openRecreateInstanceModal,
closeInstanceModal: closeModal closeInstanceModal: closeModal
}; };
}; };
+1 -11
View File
@@ -86,7 +86,6 @@ const GPUService: React.FC = () => {
openCreateInstanceModal, openCreateInstanceModal,
openEditInstanceModal, openEditInstanceModal,
openViewInstanceModal, openViewInstanceModal,
openRecreateInstanceModal,
closeInstanceModal closeInstanceModal
} = useCreateInstance(); } = useCreateInstance();
const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } = const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } =
@@ -136,13 +135,7 @@ const GPUService: React.FC = () => {
const handleModalOk = async (data: FormData) => { const handleModalOk = async (data: FormData) => {
try { try {
if (openInstanceModalStatus.realAction === PageAction.CREATE) { if (openInstanceModalStatus.action === PageAction.EDIT) {
await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id);
await new Promise((resolve) => {
setTimeout(resolve, 300);
});
await createInstance({ data });
} else if (openInstanceModalStatus.action === PageAction.EDIT) {
await updateInstance({ await updateInstance({
id: openInstanceModalStatus.currentData!.id, id: openInstanceModalStatus.currentData!.id,
data data
@@ -243,8 +236,6 @@ const GPUService: React.FC = () => {
openEditInstanceModal(row); openEditInstanceModal(row);
} else if (val === 'delete') { } else if (val === 'delete') {
handleDelete({ ...row }); handleDelete({ ...row });
} else if (val === 'recreate') {
openRecreateInstanceModal(row);
} else if (val === 'viewlog') { } else if (val === 'viewlog') {
openViewLogsModal(row); openViewLogsModal(row);
} else if (val === 'viewevent') { } else if (val === 'viewevent') {
@@ -391,7 +382,6 @@ const GPUService: React.FC = () => {
title={openInstanceModalStatus.title} title={openInstanceModalStatus.title}
data={openInstanceModalStatus.currentData} data={openInstanceModalStatus.currentData}
width={openInstanceModalStatus.width} width={openInstanceModalStatus.width}
realAction={openInstanceModalStatus.realAction}
clusterList={clusterList} clusterList={clusterList}
onCancel={closeInstanceModal} onCancel={closeInstanceModal}
onOk={handleModalOk} onOk={handleModalOk}
@@ -23,8 +23,8 @@ export const buildInstanceTypeSnapshotSpec = (
// Serialize the chosen instance type into the instance's `description` field — // 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 // 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) // derive unit resources. Shared by the create flow (card selection) and the
// and the edit flow (change-type overlay). // edit flow (change-type overlay).
export const saveInstanceDataInDescription = ( export const saveInstanceDataInDescription = (
instanceType: InstanceTypeItem instanceType: InstanceTypeItem
): string => { ): string => {