diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 60ca329e..20e3e7df 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -225,7 +225,6 @@ export default { 'common.text.latest': 'Latest', 'common.text.new': 'New', 'common.text.changelog': 'Release Notes', - 'common.button.recreate': 'Recreate', 'common.button.delrecreate': 'Delete (Recreate)', 'common.options.all': 'All', 'common.options.none': 'None', @@ -283,5 +282,6 @@ export default { 'common.file.format.limit': 'Invalid file format. Allowed: {formats}.', 'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.height': 'Image height must be {height}.', - 'common.remaining': 'Remaining {count}' + 'common.remaining': 'Remaining {count}', + 'common.validate.group': 'Please complete the {group} configuration' }; diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index 88f2df4e..b922b3a1 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -15,6 +15,9 @@ export default { 'gpuservice.template.mountPath': 'Mount Path', 'gpuservice.template.containerDisk': 'Container Disk (GB)', 'gpuservice.template.memory': 'Memory (GB)', + 'gpuservice.instance.containerDisk.remaining': + 'Container Disk (Remaining {count} GB)', + 'gpuservice.instance.memory.remaining': 'Memory (Remaining {count} GB)', 'gpuservice.template.displayName': 'Display Name', 'gpuservice.template.displayName.max': 'Display name cannot exceed 63 characters.', @@ -58,6 +61,8 @@ export default { 'Please select at most {count} GPU card(s)', 'gpuservice.instance.gpuCount.min': 'Please select at least {count} GPU card(s)', + 'gpuservice.instance.gpuCount.noAvailable': + 'No available GPU resources, please choose another instance type.', 'gpuservice.instance.stock': 'Stock', 'gpuservice.instance.sliced': 'Sliced', 'gpuservice.instance.memory': 'Memory', diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index cddd73fa..a0448019 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -225,7 +225,6 @@ export default { 'common.text.latest': '最新', 'common.text.new': '新規', 'common.text.changelog': 'リリースノート', - 'common.button.recreate': '再作成', 'common.button.delrecreate': '削除(再作成)', 'common.options.all': 'すべて', 'common.options.none': 'なし', @@ -283,7 +282,8 @@ export default { 'common.file.format.limit': 'Invalid file format. Allowed: {formats}.', 'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.height': 'Image height must be {height}.', - 'common.remaining': '残り {count}' + 'common.remaining': '残り {count}', + 'common.validate.group': 'Please complete the {group} configuration' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index 50887b7e..c022e1a4 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -15,6 +15,9 @@ export default { 'gpuservice.template.mountPath': 'マウントパス', 'gpuservice.template.containerDisk': 'コンテナディスク (GB)', 'gpuservice.template.memory': 'メモリ (GB)', + 'gpuservice.instance.containerDisk.remaining': + 'コンテナディスク (残り {count} GB)', + 'gpuservice.instance.memory.remaining': 'メモリ (残り {count} GB)', 'gpuservice.template.displayName': '表示名', 'gpuservice.template.displayName.max': '表示名は 63 文字以内で入力してください。', @@ -56,6 +59,8 @@ export default { '最大 {count} 枚の GPU カードを選択してください', 'gpuservice.instance.gpuCount.min': '少なくとも {count} 枚の GPU カードを選択してください', + 'gpuservice.instance.gpuCount.noAvailable': + '利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。', 'gpuservice.instance.stock': '在庫', 'gpuservice.instance.sliced': '分割', 'gpuservice.instance.memory': 'Memory', diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index 38a5f1d5..937a6caf 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -223,7 +223,6 @@ export default { 'common.text.latest': 'Последняя', 'common.text.new': 'Новая', 'common.text.changelog': 'История изменений', - 'common.button.recreate': 'Пересоздать', 'common.button.delrecreate': 'Удалить (Пересоздать)', 'common.options.all': 'Все', 'common.options.none': 'Нет', @@ -282,7 +281,8 @@ export default { 'common.file.format.limit': 'Invalid file format. Allowed: {formats}.', 'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.height': 'Image height must be {height}.', - 'common.remaining': 'Остаток {count}' + 'common.remaining': 'Остаток {count}', + 'common.validate.group': 'Please complete the {group} configuration' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index 8c0d9fa1..d2358c18 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -16,6 +16,9 @@ export default { 'gpuservice.template.mountPath': 'Путь монтирования', 'gpuservice.template.containerDisk': 'Диск контейнера (GB)', 'gpuservice.template.memory': 'Память (GB)', + 'gpuservice.instance.containerDisk.remaining': + 'Диск контейнера (Остаток {count} GB)', + 'gpuservice.instance.memory.remaining': 'Память (Остаток {count} GB)', 'gpuservice.template.displayName': 'Отображаемое имя', 'gpuservice.template.displayName.max': 'Отображаемое имя не должно превышать 63 символа.', @@ -57,6 +60,8 @@ export default { 'gpuservice.instance.gpuCount.required': 'Введите количество GPU', 'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт', 'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт', + 'gpuservice.instance.gpuCount.noAvailable': + 'Нет доступных ресурсов GPU, выберите другой тип экземпляра.', 'gpuservice.instance.stock': 'Остаток', 'gpuservice.instance.sliced': 'Разделено', 'gpuservice.instance.memory': 'Память', diff --git a/src/locales/tr-TR/common.ts b/src/locales/tr-TR/common.ts index 82ec9b71..e98a22d2 100644 --- a/src/locales/tr-TR/common.ts +++ b/src/locales/tr-TR/common.ts @@ -228,7 +228,6 @@ export default { 'common.text.latest': 'En Son', 'common.text.new': 'Yeni', 'common.text.changelog': 'Sürüm Notları', - 'common.button.recreate': 'Yeniden oluştur', 'common.button.delrecreate': 'Sil (Yeniden oluştur)', 'common.options.all': 'Tümü', 'common.options.none': 'Hiçbiri', @@ -286,5 +285,6 @@ export default { 'common.file.format.limit': 'Invalid file format. Allowed: {formats}.', 'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.height': 'Image height must be {height}.', - 'common.remaining': 'Kalan {count}' + 'common.remaining': 'Kalan {count}', + 'common.validate.group': 'Please complete the {group} configuration' }; diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index b4067925..945067e4 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -15,6 +15,9 @@ export default { 'gpuservice.template.mountPath': 'Bağlama Yolu', 'gpuservice.template.containerDisk': 'Konteyner Diski (GB)', 'gpuservice.template.memory': 'Bellek (GB)', + 'gpuservice.instance.containerDisk.remaining': + 'Konteyner Diski (Kalan {count} GB)', + 'gpuservice.instance.memory.remaining': 'Bellek (Kalan {count} GB)', 'gpuservice.template.displayName': 'Görünen Ad', 'gpuservice.template.displayName.max': 'Görünen ad 63 karakterden uzun olamaz.', @@ -56,6 +59,8 @@ export default { 'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin', 'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin', 'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı seçin', + 'gpuservice.instance.gpuCount.noAvailable': + 'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.', 'gpuservice.instance.stock': 'Stok', 'gpuservice.instance.sliced': 'Bölünmüş', 'gpuservice.instance.memory': 'Bellek', diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 38b54753..9027672f 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -275,5 +275,6 @@ export default { 'common.file.format.limit': '文件格式不正确,仅支持{formats}。', 'common.image.limit.width': '图片宽度须为{width}。', 'common.image.limit.height': '图片高度须为{height}。', - 'common.remaining': '剩余 {count}' + 'common.remaining': '剩余 {count}', + 'common.validate.group': '请填写完整的{group}配置' }; diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index 9f436751..42eb0746 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -15,6 +15,8 @@ export default { 'gpuservice.template.mountPath': '挂载路径', 'gpuservice.template.containerDisk': '容器磁盘(GB)', 'gpuservice.template.memory': '内存(GB)', + 'gpuservice.instance.containerDisk.remaining': '容器磁盘(剩余{count}GB)', + 'gpuservice.instance.memory.remaining': '内存(剩余{count}GB)', 'gpuservice.template.displayName': '显示名称', 'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符', 'gpuservice.template.ports': '端口', @@ -52,6 +54,8 @@ export default { 'gpuservice.instance.gpuCount.required': '请输入 GPU 数量', 'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡', 'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡', + 'gpuservice.instance.gpuCount.noAvailable': + '没有可用的 GPU 资源,请选择其他实例类型。', 'gpuservice.instance.stock': '库存', 'gpuservice.instance.sliced': '切分', 'gpuservice.instance.memory': '显存', diff --git a/src/pages/_components/number-selection/index.tsx b/src/pages/_components/number-selection/index.tsx index f4eca299..58630913 100644 --- a/src/pages/_components/number-selection/index.tsx +++ b/src/pages/_components/number-selection/index.tsx @@ -1,6 +1,6 @@ import { LabelInfo } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { InputNumber } from 'antd'; +import { Flex, InputNumber } from 'antd'; import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; import styles from './styles.less'; @@ -19,6 +19,7 @@ interface NumberSelectionProps { styles?: { input?: React.CSSProperties; }; + labelExtra?: React.ReactNode; onChange?: (value: number) => void; } @@ -33,6 +34,7 @@ const NumberSelection: React.FC = ({ disabled, label, required, + labelExtra, className, style, onChange @@ -93,70 +95,77 @@ const NumberSelection: React.FC = ({ > {label !== undefined && label !== null && (
- +
)} -
- {items.map((num) => { - return ( + +
+ {items.map((num) => { + return ( +
handleSelect(num)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSelect(num); + } + }} + > + {num} +
+ ); + })} +
+ {showCustomInput && ( +
+
handleSelect(num)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleSelect(num); - } - }} > - {num} +
- ); - })} -
- {showCustomInput && ( -
-
-
-
-
- )} + )} +
); }; diff --git a/src/pages/_components/number-selection/styles.less b/src/pages/_components/number-selection/styles.less index 4694f712..ae69881f 100644 --- a/src/pages/_components/number-selection/styles.less +++ b/src/pages/_components/number-selection/styles.less @@ -4,7 +4,10 @@ border-radius: var(--ant-border-radius-lg); border: 1px solid var(--ant-color-border); overflow: hidden; - background-color: var(--ant-color-fill-quaternary); + flex-direction: column; + align-items: flex-start; + padding: 4px; + .inputContainer { &.hasValue { background-color: var(--ant-color-bg-container); @@ -28,6 +31,7 @@ display: flex; height: 22px; border-left: 1px solid var(--ant-color-split); + margin-inline: 4px; } } @@ -37,14 +41,21 @@ padding: 0 12px; color: var(--ant-color-text-tertiary); white-space: nowrap; - border-right: 1px solid var(--ant-color-split); + padding-block: 6px; + } + .contentWrapper { + width: 100%; + background-color: var(--ant-color-fill-quaternary); + border-radius: 4px; + height: 36px; + padding-left: 4px; } .content { display: flex; align-items: center; flex: 1; gap: 4px; - padding: 4px; + padding: 0px; } .numberItem { position: relative; @@ -53,7 +64,7 @@ display: flex; align-items: center; justify-content: center; - height: 40px; + height: 32px; border-radius: 4px; cursor: pointer; border: 1px solid transparent; diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 139270eb..29804ab4 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -370,9 +370,11 @@ const AddModal: React.FC = ({ } > <> - - {intl.formatMessage({ id: 'common.title.config' })} - + {action !== PageAction.VIEW && ( + + {intl.formatMessage({ id: 'common.title.config' })} + + )} = ({ const intl = useIntl(); const name = item.metadata?.name; const acceleratable = item.spec?.acceleratable; - const product = item.spec?.product; - const displayName = product || (!acceleratable ? 'CPU' : name); + const manufacturer = item.spec?.manufacturer?.toUpperCase(); + const renderName = () => { + const product = item.spec?.product; + const displayName = + product || + (!acceleratable ? `${item.status?.cpu?.capacity} vCPUs` : name); + return displayName; + }; + return ( <> <Flex gap={8} align="center"> <AutoTooltip ghost minWidth={20} maxWidth={180}> - {item.spec ? displayName : '-'} + {item.spec ? renderName() : '-'} </AutoTooltip> {acceleratable && manufacturer && ( <span @@ -165,14 +172,18 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ <span>{toDisplayUnit(item.status?.ram?.capacity) ?? '-'}</span> </Flex> </span> - <span className="dot"></span> - <span className="meta-item"> - <IconFont type="icon-cpu" className="meta-icon" /> - <Flex align="center" gap={4}> - <span>vCPU</span> - <span>{item.status?.cpu?.capacity ?? '-'}</span> - </Flex> - </span> + {acceleratable && ( + <> + <span className="dot"></span> + <span className="meta-item"> + <IconFont type="icon-cpu" className="meta-icon" /> + <Flex align="center" gap={4}> + <span>vCPU</span> + <span>{item.status?.cpu?.capacity ?? '-'}</span> + </Flex> + </span> + </> + )} </span> </Meta> </> diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 41c3df4a..6fa2f02c 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -160,11 +160,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef( ); const ports = Form.useWatch(['spec', 'ports'], form) || []; - const hasSshPort = useMemo( - () => - ports.some((p) => Number(p?.port) === SSH_PORT && p.protocol === 'TCP'), - [ports] - ); const selectedTypeName = Form.useWatch(['spec', 'type'], form) as | string @@ -242,6 +237,17 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef( }); }; + const segmentedTop = + action === PageAction.VIEW + ? { + top: 0, + offsetTop: 100 + } + : { + top: 40, + offsetTop: 130 + }; + useImperativeHandle(ref, () => ({ submit: () => { form.submit(); @@ -262,10 +268,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef( segmentOptions={segmentOptions} activeKey={activeKey} setActiveKey={handleActiveChange} - segmentedTop={{ - top: 40, - offsetTop: 130 - }} + segmentedTop={segmentedTop} getScrollElementScrollableHeight={getScrollElementScrollableHeight} > <Form diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index 149a374a..531e9c63 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -2,7 +2,7 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import NumberSelection from '@/pages/_components/number-selection'; import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; +import { Alert, Form } from 'antd'; import { useEffect, useMemo } from 'react'; import styled from 'styled-components'; import InstanceTypeItem from '../components/instance-type-item'; @@ -118,17 +118,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({ <InstanceTypePicker dataList={dataList} value={typeName} /> </Form.Item> </FieldBlock> - {selectedInstanceType?.spec?.acceleratable && ( + {!selectedInstanceType?.spec?.acceleratable && ( <Form.Item name={['spec', 'resources', 'accelerator']} rules={[ { required: true, - message: intl.formatMessage({ - id: 'gpuservice.instance.gpuCount.required' - }) - }, - { validator: (_, value) => { if (value > maxGpuCount) { return Promise.reject( @@ -143,9 +138,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({ if (value < 1) { return Promise.reject( new Error( - intl.formatMessage({ - id: 'gpuservice.instance.gpuCount.min' - }) + intl.formatMessage( + { + id: 'gpuservice.instance.gpuCount.min' + }, + { + count: 1 + } + ) ) ); } @@ -160,6 +160,23 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({ step={1} required disabled={disabled || action === PageAction.EDIT} + labelExtra={ + !maxGpuCount && ( + <Alert + showIcon + type="warning" + title={intl.formatMessage({ + id: 'gpuservice.instance.gpuCount.noAvailable' + })} + styles={{ + root: { + marginLeft: 4, + paddingBlock: 0 + } + }} + ></Alert> + ) + } label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} /> </Form.Item> diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index b1e4b56d..4c037642 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -2,9 +2,11 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; import { getCurrentOrgNamespace } from '@/atoms/user'; import { PageAction } from '@/config'; import { + Input as CInput, InputNumber as CInputNumber, LabelInfo, - Select + Select, + useAppUtils } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Button, Flex, Form, Radio } from 'antd'; @@ -32,6 +34,7 @@ const StorageVolume = ({ action: PageActionType; }) => { const intl = useIntl(); + const { getRuleMessage } = useAppUtils(); const { fetchData: createStorage } = useCreateStorage(); const { detailData: storageData, fetchData: fetchStorage } = useQueryStorage(); @@ -84,7 +87,7 @@ const StorageVolume = ({ }; return ( - <FieldBlock data-field="storage"> + <FieldBlock> <Flex style={{ justifyContent: 'space-between', @@ -146,6 +149,7 @@ const StorageVolume = ({ </Button> )} </Flex> + <div data-field="storage"></div> {storageMode === StorageModeValueMap.Existing && ( <Form.Item @@ -199,6 +203,26 @@ const StorageVolume = ({ /> </Form.Item> )} + <Form.Item<FormData> + name={['spec', 'volumeMount']} + rules={[ + { + required: true, + message: getRuleMessage('input', 'gpuservice.template.mountPath') + } + ]} + > + <CInput.Input + required + label={intl.formatMessage({ + id: 'gpuservice.template.mountPath' + })} + placeholder={intl.formatMessage({ + id: 'clusters.volume.mountPath.format' + })} + disabled={disabled} + /> + </Form.Item> <StorageOverlay open={overlayOpen} diff --git a/src/pages/gpu-service/templates/forms/basic.tsx b/src/pages/gpu-service/templates/forms/basic.tsx index 3dda3a6f..661954bb 100644 --- a/src/pages/gpu-service/templates/forms/basic.tsx +++ b/src/pages/gpu-service/templates/forms/basic.tsx @@ -64,6 +64,26 @@ const Basic: React.FC<BasicProps> = ({ ); }; + const renderStorageLabel = (): React.ReactNode => { + if (page === 'instance' && onceMaxRequest?.localStorage != null) { + return intl.formatMessage( + { id: 'gpuservice.instance.containerDisk.remaining' }, + { count: onceMaxRequest.localStorage } + ); + } + return intl.formatMessage({ id: 'gpuservice.template.containerDisk' }); + }; + + const renderMemoryLabel = (): React.ReactNode => { + if (page === 'instance' && onceMaxRequest?.memory != null) { + return intl.formatMessage( + { id: 'gpuservice.instance.memory.remaining' }, + { count: onceMaxRequest.memory } + ); + } + return intl.formatMessage({ id: 'gpuservice.template.memory' }); + }; + return ( <> <div data-field="template"></div> @@ -174,20 +194,22 @@ const Basic: React.FC<BasicProps> = ({ </Form.Item> </div> - <Flex gap={16}> - <div style={{ flex: 1 }}> - <Form.Item<FormData> name={['spec', 'volumeMount']}> - <CInput.Input - label={intl.formatMessage({ - id: 'gpuservice.template.mountPath' - })} - placeholder={intl.formatMessage({ - id: 'clusters.volume.mountPath.format' - })} - disabled={disabled} - /> - </Form.Item> - </div> + <Flex gap={12}> + {page === 'template' && ( + <div style={{ flex: 1 }}> + <Form.Item<FormData> name={['spec', 'volumeMount']}> + <CInput.Input + label={intl.formatMessage({ + id: 'gpuservice.template.mountPath' + })} + placeholder={intl.formatMessage({ + id: 'clusters.volume.mountPath.format' + })} + disabled={disabled} + /> + </Form.Item> + </div> + )} <div style={{ flex: 1 }}> <Form.Item<FormData> name={['spec', 'resources', 'localStorage']} @@ -197,17 +219,14 @@ const Basic: React.FC<BasicProps> = ({ })} > <InputNumber - label={renderMaxLabel( - intl.formatMessage({ id: 'gpuservice.template.containerDisk' }), - onceMaxRequest?.localStorage - )} + label={renderStorageLabel()} max={onceMaxRequest?.localStorage ?? undefined} disabled={disabled} /> </Form.Item> </div> </Flex> - <Flex gap={16}> + <Flex gap={12}> <div style={{ flex: 1 }}> <Form.Item<FormData> name={['spec', 'resources', 'cpu']} @@ -230,10 +249,7 @@ const Basic: React.FC<BasicProps> = ({ })} > <InputNumber - label={renderMaxLabel( - intl.formatMessage({ id: 'gpuservice.template.memory' }), - onceMaxRequest?.memory - )} + label={renderMemoryLabel()} max={onceMaxRequest?.memory ?? undefined} disabled={disabled} /> diff --git a/src/pages/gpu-service/templates/forms/ports.tsx b/src/pages/gpu-service/templates/forms/ports.tsx index 450f8667..d85f3d4d 100644 --- a/src/pages/gpu-service/templates/forms/ports.tsx +++ b/src/pages/gpu-service/templates/forms/ports.tsx @@ -9,6 +9,7 @@ import { Form } from 'antd'; import _ from 'lodash'; import { useEffect, useRef, useState } from 'react'; import { FormData, PortItem as PortItemType } from '../config/types'; +import styles from '../styles/template.less'; type PortProtocol = PortItemType['protocol']; @@ -115,11 +116,16 @@ const PortNameInput: React.FC<PortNameInputProps & { disabled?: boolean }> = ({ ); }; -const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ item, index, disabled = false, onChange }) => { +const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ + item, + index, + disabled = false, + onChange +}) => { const intl = useIntl(); return ( - <div style={{ display: 'flex', gap: 12, width: '100%' }}> - <div style={{ width: 120 }}> + <div className={styles.portItem}> + <div className={styles.select}> <SealSelect disabled={disabled} value={item.protocol} @@ -141,10 +147,9 @@ const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ item, inde style={{ width: '100%' }} ></SealSelect> </div> - <div style={{ width: 120 }}> + <div className={styles.port}> <CInputNumber min={PORT_MIN} - max={PORT_MAX} disabled={disabled} precision={0} value={item.port} @@ -166,7 +171,7 @@ const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ item, inde style={{ width: '100%' }} /> </div> - <div style={{ flex: 1 }}> + <div className={styles.name}> <PortNameInput disabled={disabled} value={item.name} @@ -192,8 +197,6 @@ const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => { const updatePorts = (list: PortItemType[]) => { form.setFieldValue(['spec', 'ports'], list); - // setFieldValue doesn't run validators — re-run them so users see errors live. - form.validateFields([['spec', 'ports']]).catch(() => {}); }; const handleAdd = () => { @@ -231,13 +234,19 @@ const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => { const items = value as PortItemType[]; - if (_.some(items, (item) => !item.protocol || !item.port)) { + if ( + _.some( + items, + (item: PortItemType) => + !item.protocol || !item.port || !item.name + ) + ) { return Promise.reject( new Error( intl.formatMessage( - { id: 'common.validate.value' }, + { id: 'common.validate.group' }, { - name: intl.formatMessage({ + group: intl.formatMessage({ id: 'gpuservice.template.ports' }) } @@ -248,16 +257,6 @@ const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => { const namedItems = items.filter((item) => !!item.name); - if (_.some(namedItems, (item) => item.name!.length > PORT_NAME_MAX)) { - return Promise.reject( - new Error( - intl.formatMessage({ - id: 'gpuservice.template.ports.name.max' - }) - ) - ); - } - const names = namedItems.map((item) => item.name); if (_.uniq(names).length !== names.length) { return Promise.reject( diff --git a/src/pages/gpu-service/templates/styles/template.less b/src/pages/gpu-service/templates/styles/template.less new file mode 100644 index 00000000..caef2410 --- /dev/null +++ b/src/pages/gpu-service/templates/styles/template.less @@ -0,0 +1,18 @@ +.portItem { + display: flex; + gap: 12px; + width: 100%; + flex-wrap: wrap; + .select { + flex: 1 1 120px; + min-width: 120px; + } + .port { + flex: 1 1 120px; + min-width: 120px; + } + .name { + flex: 100 1 180px; + min-width: 180px; + } +}