style: number selection
This commit is contained in:
@@ -225,7 +225,6 @@ export default {
|
|||||||
'common.text.latest': 'Latest',
|
'common.text.latest': 'Latest',
|
||||||
'common.text.new': 'New',
|
'common.text.new': 'New',
|
||||||
'common.text.changelog': 'Release Notes',
|
'common.text.changelog': 'Release Notes',
|
||||||
'common.button.recreate': 'Recreate',
|
|
||||||
'common.button.delrecreate': 'Delete (Recreate)',
|
'common.button.delrecreate': 'Delete (Recreate)',
|
||||||
'common.options.all': 'All',
|
'common.options.all': 'All',
|
||||||
'common.options.none': 'None',
|
'common.options.none': 'None',
|
||||||
@@ -283,5 +282,6 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'Mount Path',
|
'gpuservice.template.mountPath': 'Mount Path',
|
||||||
'gpuservice.template.containerDisk': 'Container Disk (GB)',
|
'gpuservice.template.containerDisk': 'Container Disk (GB)',
|
||||||
'gpuservice.template.memory': 'Memory (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': 'Display Name',
|
||||||
'gpuservice.template.displayName.max':
|
'gpuservice.template.displayName.max':
|
||||||
'Display name cannot exceed 63 characters.',
|
'Display name cannot exceed 63 characters.',
|
||||||
@@ -58,6 +61,8 @@ export default {
|
|||||||
'Please select at most {count} GPU card(s)',
|
'Please select at most {count} GPU card(s)',
|
||||||
'gpuservice.instance.gpuCount.min':
|
'gpuservice.instance.gpuCount.min':
|
||||||
'Please select at least {count} GPU card(s)',
|
'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.stock': 'Stock',
|
||||||
'gpuservice.instance.sliced': 'Sliced',
|
'gpuservice.instance.sliced': 'Sliced',
|
||||||
'gpuservice.instance.memory': 'Memory',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
|
|||||||
@@ -225,7 +225,6 @@ export default {
|
|||||||
'common.text.latest': '最新',
|
'common.text.latest': '最新',
|
||||||
'common.text.new': '新規',
|
'common.text.new': '新規',
|
||||||
'common.text.changelog': 'リリースノート',
|
'common.text.changelog': 'リリースノート',
|
||||||
'common.button.recreate': '再作成',
|
|
||||||
'common.button.delrecreate': '削除(再作成)',
|
'common.button.delrecreate': '削除(再作成)',
|
||||||
'common.options.all': 'すべて',
|
'common.options.all': 'すべて',
|
||||||
'common.options.none': 'なし',
|
'common.options.none': 'なし',
|
||||||
@@ -283,7 +282,8 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'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) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'マウントパス',
|
'gpuservice.template.mountPath': 'マウントパス',
|
||||||
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
|
||||||
'gpuservice.template.memory': 'メモリ (GB)',
|
'gpuservice.template.memory': 'メモリ (GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining':
|
||||||
|
'コンテナディスク (残り {count} GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': 'メモリ (残り {count} GB)',
|
||||||
'gpuservice.template.displayName': '表示名',
|
'gpuservice.template.displayName': '表示名',
|
||||||
'gpuservice.template.displayName.max':
|
'gpuservice.template.displayName.max':
|
||||||
'表示名は 63 文字以内で入力してください。',
|
'表示名は 63 文字以内で入力してください。',
|
||||||
@@ -56,6 +59,8 @@ export default {
|
|||||||
'最大 {count} 枚の GPU カードを選択してください',
|
'最大 {count} 枚の GPU カードを選択してください',
|
||||||
'gpuservice.instance.gpuCount.min':
|
'gpuservice.instance.gpuCount.min':
|
||||||
'少なくとも {count} 枚の GPU カードを選択してください',
|
'少なくとも {count} 枚の GPU カードを選択してください',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
|
||||||
'gpuservice.instance.stock': '在庫',
|
'gpuservice.instance.stock': '在庫',
|
||||||
'gpuservice.instance.sliced': '分割',
|
'gpuservice.instance.sliced': '分割',
|
||||||
'gpuservice.instance.memory': 'Memory',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ export default {
|
|||||||
'common.text.latest': 'Последняя',
|
'common.text.latest': 'Последняя',
|
||||||
'common.text.new': 'Новая',
|
'common.text.new': 'Новая',
|
||||||
'common.text.changelog': 'История изменений',
|
'common.text.changelog': 'История изменений',
|
||||||
'common.button.recreate': 'Пересоздать',
|
|
||||||
'common.button.delrecreate': 'Удалить (Пересоздать)',
|
'common.button.delrecreate': 'Удалить (Пересоздать)',
|
||||||
'common.options.all': 'Все',
|
'common.options.all': 'Все',
|
||||||
'common.options.none': 'Нет',
|
'common.options.none': 'Нет',
|
||||||
@@ -282,7 +281,8 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'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) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'Путь монтирования',
|
'gpuservice.template.mountPath': 'Путь монтирования',
|
||||||
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
|
||||||
'gpuservice.template.memory': 'Память (GB)',
|
'gpuservice.template.memory': 'Память (GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining':
|
||||||
|
'Диск контейнера (Остаток {count} GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': 'Память (Остаток {count} GB)',
|
||||||
'gpuservice.template.displayName': 'Отображаемое имя',
|
'gpuservice.template.displayName': 'Отображаемое имя',
|
||||||
'gpuservice.template.displayName.max':
|
'gpuservice.template.displayName.max':
|
||||||
'Отображаемое имя не должно превышать 63 символа.',
|
'Отображаемое имя не должно превышать 63 символа.',
|
||||||
@@ -57,6 +60,8 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
|
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
|
||||||
'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт',
|
'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт',
|
||||||
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
|
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
|
||||||
'gpuservice.instance.stock': 'Остаток',
|
'gpuservice.instance.stock': 'Остаток',
|
||||||
'gpuservice.instance.sliced': 'Разделено',
|
'gpuservice.instance.sliced': 'Разделено',
|
||||||
'gpuservice.instance.memory': 'Память',
|
'gpuservice.instance.memory': 'Память',
|
||||||
|
|||||||
@@ -228,7 +228,6 @@ export default {
|
|||||||
'common.text.latest': 'En Son',
|
'common.text.latest': 'En Son',
|
||||||
'common.text.new': 'Yeni',
|
'common.text.new': 'Yeni',
|
||||||
'common.text.changelog': 'Sürüm Notları',
|
'common.text.changelog': 'Sürüm Notları',
|
||||||
'common.button.recreate': 'Yeniden oluştur',
|
|
||||||
'common.button.delrecreate': 'Sil (Yeniden oluştur)',
|
'common.button.delrecreate': 'Sil (Yeniden oluştur)',
|
||||||
'common.options.all': 'Tümü',
|
'common.options.all': 'Tümü',
|
||||||
'common.options.none': 'Hiçbiri',
|
'common.options.none': 'Hiçbiri',
|
||||||
@@ -286,5 +285,6 @@ export default {
|
|||||||
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
'common.image.limit.width': 'Image width must be {width}.',
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
'common.image.limit.height': 'Image height must be {height}.',
|
'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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export default {
|
|||||||
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
||||||
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
|
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
|
||||||
'gpuservice.template.memory': 'Bellek (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': 'Görünen Ad',
|
||||||
'gpuservice.template.displayName.max':
|
'gpuservice.template.displayName.max':
|
||||||
'Görünen ad 63 karakterden uzun olamaz.',
|
'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.required': 'Lütfen GPU sayısını girin',
|
||||||
'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin',
|
'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.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.stock': 'Stok',
|
||||||
'gpuservice.instance.sliced': 'Bölünmüş',
|
'gpuservice.instance.sliced': 'Bölünmüş',
|
||||||
'gpuservice.instance.memory': 'Bellek',
|
'gpuservice.instance.memory': 'Bellek',
|
||||||
|
|||||||
@@ -275,5 +275,6 @@ export default {
|
|||||||
'common.file.format.limit': '文件格式不正确,仅支持{formats}。',
|
'common.file.format.limit': '文件格式不正确,仅支持{formats}。',
|
||||||
'common.image.limit.width': '图片宽度须为{width}。',
|
'common.image.limit.width': '图片宽度须为{width}。',
|
||||||
'common.image.limit.height': '图片高度须为{height}。',
|
'common.image.limit.height': '图片高度须为{height}。',
|
||||||
'common.remaining': '剩余 {count}'
|
'common.remaining': '剩余 {count}',
|
||||||
|
'common.validate.group': '请填写完整的{group}配置'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export default {
|
|||||||
'gpuservice.template.mountPath': '挂载路径',
|
'gpuservice.template.mountPath': '挂载路径',
|
||||||
'gpuservice.template.containerDisk': '容器磁盘(GB)',
|
'gpuservice.template.containerDisk': '容器磁盘(GB)',
|
||||||
'gpuservice.template.memory': '内存(GB)',
|
'gpuservice.template.memory': '内存(GB)',
|
||||||
|
'gpuservice.instance.containerDisk.remaining': '容器磁盘(剩余{count}GB)',
|
||||||
|
'gpuservice.instance.memory.remaining': '内存(剩余{count}GB)',
|
||||||
'gpuservice.template.displayName': '显示名称',
|
'gpuservice.template.displayName': '显示名称',
|
||||||
'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符',
|
'gpuservice.template.displayName.max': '显示名称不能超过 63 个字符',
|
||||||
'gpuservice.template.ports': '端口',
|
'gpuservice.template.ports': '端口',
|
||||||
@@ -52,6 +54,8 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
|
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
|
||||||
'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡',
|
'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡',
|
||||||
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
|
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
|
||||||
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
|
'没有可用的 GPU 资源,请选择其他实例类型。',
|
||||||
'gpuservice.instance.stock': '库存',
|
'gpuservice.instance.stock': '库存',
|
||||||
'gpuservice.instance.sliced': '切分',
|
'gpuservice.instance.sliced': '切分',
|
||||||
'gpuservice.instance.memory': '显存',
|
'gpuservice.instance.memory': '显存',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LabelInfo } from '@gpustack/core-ui';
|
import { LabelInfo } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { InputNumber } from 'antd';
|
import { Flex, InputNumber } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import styles from './styles.less';
|
import styles from './styles.less';
|
||||||
@@ -19,6 +19,7 @@ interface NumberSelectionProps {
|
|||||||
styles?: {
|
styles?: {
|
||||||
input?: React.CSSProperties;
|
input?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
labelExtra?: React.ReactNode;
|
||||||
onChange?: (value: number) => void;
|
onChange?: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
label,
|
label,
|
||||||
required,
|
required,
|
||||||
|
labelExtra,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
onChange
|
onChange
|
||||||
@@ -93,70 +95,77 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
|||||||
>
|
>
|
||||||
{label !== undefined && label !== null && (
|
{label !== undefined && label !== null && (
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
<LabelInfo label={label} required={required}></LabelInfo>
|
<LabelInfo
|
||||||
|
label={label}
|
||||||
|
required={required}
|
||||||
|
labelExtra={labelExtra}
|
||||||
|
></LabelInfo>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.content}>
|
<Flex className={styles.contentWrapper} align="center">
|
||||||
{items.map((num) => {
|
<div className={styles.content}>
|
||||||
return (
|
{items.map((num) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={num}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={num === value}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
className={classNames(styles.numberItem, {
|
||||||
|
[styles.active]: num === value
|
||||||
|
})}
|
||||||
|
onClick={() => handleSelect(num)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(num);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{showCustomInput && (
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<div className={styles.line}></div>
|
||||||
<div
|
<div
|
||||||
key={num}
|
className={classNames(styles.inputContainer, {
|
||||||
role="radio"
|
[styles.hasValue]: !!inputValue
|
||||||
aria-checked={num === value}
|
|
||||||
aria-disabled={disabled}
|
|
||||||
tabIndex={disabled ? -1 : 0}
|
|
||||||
className={classNames(styles.numberItem, {
|
|
||||||
[styles.active]: num === value
|
|
||||||
})}
|
})}
|
||||||
onClick={() => handleSelect(num)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSelect(num);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{num}
|
<InputNumber
|
||||||
|
controls
|
||||||
|
variant="borderless"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
disabled={disabled}
|
||||||
|
value={inputValue || undefined}
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
width: 140,
|
||||||
|
height: 32,
|
||||||
|
backgroundColor: 'var(--ant-color-bg-container)',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
fontWeight: inputValue ? 500 : 400,
|
||||||
|
...(styles?.input as React.CSSProperties)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({ id: 'common.option.other' })}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={commitInput}
|
||||||
|
onPressEnter={commitInput}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{showCustomInput && (
|
|
||||||
<div className={styles.inputWrapper}>
|
|
||||||
<div className={styles.line}></div>
|
|
||||||
<div
|
|
||||||
className={classNames(styles.inputContainer, {
|
|
||||||
[styles.hasValue]: !!inputValue
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
controls
|
|
||||||
variant="borderless"
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
step={step}
|
|
||||||
disabled={disabled}
|
|
||||||
value={inputValue || undefined}
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
width: 140,
|
|
||||||
backgroundColor: 'var(--ant-color-bg-container)',
|
|
||||||
...style
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
input: {
|
|
||||||
fontWeight: inputValue ? 500 : 400,
|
|
||||||
...(styles?.input as React.CSSProperties)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={intl.formatMessage({ id: 'common.option.other' })}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onBlur={commitInput}
|
|
||||||
onPressEnter={commitInput}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
border-radius: var(--ant-border-radius-lg);
|
border-radius: var(--ant-border-radius-lg);
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--ant-color-fill-quaternary);
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
.inputContainer {
|
.inputContainer {
|
||||||
&.hasValue {
|
&.hasValue {
|
||||||
background-color: var(--ant-color-bg-container);
|
background-color: var(--ant-color-bg-container);
|
||||||
@@ -28,6 +31,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border-left: 1px solid var(--ant-color-split);
|
border-left: 1px solid var(--ant-color-split);
|
||||||
|
margin-inline: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,14 +41,21 @@
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: var(--ant-color-text-tertiary);
|
color: var(--ant-color-text-tertiary);
|
||||||
white-space: nowrap;
|
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 {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 4px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
.numberItem {
|
.numberItem {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -53,7 +64,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 40px;
|
height: 32px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|||||||
@@ -370,9 +370,11 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<ColTitle>
|
{action !== PageAction.VIEW && (
|
||||||
{intl.formatMessage({ id: 'common.title.config' })}
|
<ColTitle>
|
||||||
</ColTitle>
|
{intl.formatMessage({ id: 'common.title.config' })}
|
||||||
|
</ColTitle>
|
||||||
|
)}
|
||||||
<GPUServiceInstanceForm
|
<GPUServiceInstanceForm
|
||||||
ref={form}
|
ref={form}
|
||||||
action={action}
|
action={action}
|
||||||
|
|||||||
@@ -66,16 +66,23 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const name = item.metadata?.name;
|
const name = item.metadata?.name;
|
||||||
const acceleratable = item.spec?.acceleratable;
|
const acceleratable = item.spec?.acceleratable;
|
||||||
const product = item.spec?.product;
|
|
||||||
const displayName = product || (!acceleratable ? 'CPU' : name);
|
|
||||||
const manufacturer = item.spec?.manufacturer?.toUpperCase();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>
|
<Title>
|
||||||
<Flex gap={8} align="center">
|
<Flex gap={8} align="center">
|
||||||
<AutoTooltip ghost minWidth={20} maxWidth={180}>
|
<AutoTooltip ghost minWidth={20} maxWidth={180}>
|
||||||
{item.spec ? displayName : '-'}
|
{item.spec ? renderName() : '-'}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
{acceleratable && manufacturer && (
|
{acceleratable && manufacturer && (
|
||||||
<span
|
<span
|
||||||
@@ -165,14 +172,18 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
|||||||
<span>{toDisplayUnit(item.status?.ram?.capacity) ?? '-'}</span>
|
<span>{toDisplayUnit(item.status?.ram?.capacity) ?? '-'}</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
</span>
|
</span>
|
||||||
<span className="dot"></span>
|
{acceleratable && (
|
||||||
<span className="meta-item">
|
<>
|
||||||
<IconFont type="icon-cpu" className="meta-icon" />
|
<span className="dot"></span>
|
||||||
<Flex align="center" gap={4}>
|
<span className="meta-item">
|
||||||
<span>vCPU</span>
|
<IconFont type="icon-cpu" className="meta-icon" />
|
||||||
<span>{item.status?.cpu?.capacity ?? '-'}</span>
|
<Flex align="center" gap={4}>
|
||||||
</Flex>
|
<span>vCPU</span>
|
||||||
</span>
|
<span>{item.status?.cpu?.capacity ?? '-'}</span>
|
||||||
|
</Flex>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Meta>
|
</Meta>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -160,11 +160,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ports = Form.useWatch(['spec', 'ports'], form) || [];
|
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
|
const selectedTypeName = Form.useWatch(['spec', 'type'], form) as
|
||||||
| string
|
| 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, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
submit: () => {
|
submit: () => {
|
||||||
form.submit();
|
form.submit();
|
||||||
@@ -262,10 +268,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
segmentOptions={segmentOptions}
|
segmentOptions={segmentOptions}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
setActiveKey={handleActiveChange}
|
setActiveKey={handleActiveChange}
|
||||||
segmentedTop={{
|
segmentedTop={segmentedTop}
|
||||||
top: 40,
|
|
||||||
offsetTop: 130
|
|
||||||
}}
|
|
||||||
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
|
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PageAction } from '@/config';
|
|||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import NumberSelection from '@/pages/_components/number-selection';
|
import NumberSelection from '@/pages/_components/number-selection';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Alert, Form } from 'antd';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import InstanceTypeItem from '../components/instance-type-item';
|
import InstanceTypeItem from '../components/instance-type-item';
|
||||||
@@ -118,17 +118,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
<InstanceTypePicker dataList={dataList} value={typeName} />
|
<InstanceTypePicker dataList={dataList} value={typeName} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
{selectedInstanceType?.spec?.acceleratable && (
|
{!selectedInstanceType?.spec?.acceleratable && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['spec', 'resources', 'accelerator']}
|
name={['spec', 'resources', 'accelerator']}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: intl.formatMessage({
|
|
||||||
id: 'gpuservice.instance.gpuCount.required'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
if (value > maxGpuCount) {
|
if (value > maxGpuCount) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
@@ -143,9 +138,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
if (value < 1) {
|
if (value < 1) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error(
|
new Error(
|
||||||
intl.formatMessage({
|
intl.formatMessage(
|
||||||
id: 'gpuservice.instance.gpuCount.min'
|
{
|
||||||
})
|
id: 'gpuservice.instance.gpuCount.min'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -160,6 +160,23 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
step={1}
|
step={1}
|
||||||
required
|
required
|
||||||
disabled={disabled || action === PageAction.EDIT}
|
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' })}
|
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { currentClusterAtom } from '@/atoms/gpuservice';
|
|||||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import {
|
import {
|
||||||
|
Input as CInput,
|
||||||
InputNumber as CInputNumber,
|
InputNumber as CInputNumber,
|
||||||
LabelInfo,
|
LabelInfo,
|
||||||
Select
|
Select,
|
||||||
|
useAppUtils
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Flex, Form, Radio } from 'antd';
|
import { Button, Flex, Form, Radio } from 'antd';
|
||||||
@@ -32,6 +34,7 @@ const StorageVolume = ({
|
|||||||
action: PageActionType;
|
action: PageActionType;
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { getRuleMessage } = useAppUtils();
|
||||||
const { fetchData: createStorage } = useCreateStorage();
|
const { fetchData: createStorage } = useCreateStorage();
|
||||||
const { detailData: storageData, fetchData: fetchStorage } =
|
const { detailData: storageData, fetchData: fetchStorage } =
|
||||||
useQueryStorage();
|
useQueryStorage();
|
||||||
@@ -84,7 +87,7 @@ const StorageVolume = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldBlock data-field="storage">
|
<FieldBlock>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@@ -146,6 +149,7 @@ const StorageVolume = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<div data-field="storage"></div>
|
||||||
|
|
||||||
{storageMode === StorageModeValueMap.Existing && (
|
{storageMode === StorageModeValueMap.Existing && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -199,6 +203,26 @@ const StorageVolume = ({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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
|
<StorageOverlay
|
||||||
open={overlayOpen}
|
open={overlayOpen}
|
||||||
|
|||||||
@@ -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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-field="template"></div>
|
<div data-field="template"></div>
|
||||||
@@ -174,20 +194,22 @@ const Basic: React.FC<BasicProps> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Flex gap={16}>
|
<Flex gap={12}>
|
||||||
<div style={{ flex: 1 }}>
|
{page === 'template' && (
|
||||||
<Form.Item<FormData> name={['spec', 'volumeMount']}>
|
<div style={{ flex: 1 }}>
|
||||||
<CInput.Input
|
<Form.Item<FormData> name={['spec', 'volumeMount']}>
|
||||||
label={intl.formatMessage({
|
<CInput.Input
|
||||||
id: 'gpuservice.template.mountPath'
|
label={intl.formatMessage({
|
||||||
})}
|
id: 'gpuservice.template.mountPath'
|
||||||
placeholder={intl.formatMessage({
|
})}
|
||||||
id: 'clusters.volume.mountPath.format'
|
placeholder={intl.formatMessage({
|
||||||
})}
|
id: 'clusters.volume.mountPath.format'
|
||||||
disabled={disabled}
|
})}
|
||||||
/>
|
disabled={disabled}
|
||||||
</Form.Item>
|
/>
|
||||||
</div>
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name={['spec', 'resources', 'localStorage']}
|
name={['spec', 'resources', 'localStorage']}
|
||||||
@@ -197,17 +219,14 @@ const Basic: React.FC<BasicProps> = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
label={renderMaxLabel(
|
label={renderStorageLabel()}
|
||||||
intl.formatMessage({ id: 'gpuservice.template.containerDisk' }),
|
|
||||||
onceMaxRequest?.localStorage
|
|
||||||
)}
|
|
||||||
max={onceMaxRequest?.localStorage ?? undefined}
|
max={onceMaxRequest?.localStorage ?? undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap={16}>
|
<Flex gap={12}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name={['spec', 'resources', 'cpu']}
|
name={['spec', 'resources', 'cpu']}
|
||||||
@@ -230,10 +249,7 @@ const Basic: React.FC<BasicProps> = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
label={renderMaxLabel(
|
label={renderMemoryLabel()}
|
||||||
intl.formatMessage({ id: 'gpuservice.template.memory' }),
|
|
||||||
onceMaxRequest?.memory
|
|
||||||
)}
|
|
||||||
max={onceMaxRequest?.memory ?? undefined}
|
max={onceMaxRequest?.memory ?? undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Form } from 'antd';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { FormData, PortItem as PortItemType } from '../config/types';
|
import { FormData, PortItem as PortItemType } from '../config/types';
|
||||||
|
import styles from '../styles/template.less';
|
||||||
|
|
||||||
type PortProtocol = PortItemType['protocol'];
|
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();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
|
<div className={styles.portItem}>
|
||||||
<div style={{ width: 120 }}>
|
<div className={styles.select}>
|
||||||
<SealSelect
|
<SealSelect
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={item.protocol}
|
value={item.protocol}
|
||||||
@@ -141,10 +147,9 @@ const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ item, inde
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
></SealSelect>
|
></SealSelect>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: 120 }}>
|
<div className={styles.port}>
|
||||||
<CInputNumber
|
<CInputNumber
|
||||||
min={PORT_MIN}
|
min={PORT_MIN}
|
||||||
max={PORT_MAX}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
precision={0}
|
precision={0}
|
||||||
value={item.port}
|
value={item.port}
|
||||||
@@ -166,7 +171,7 @@ const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ item, inde
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div className={styles.name}>
|
||||||
<PortNameInput
|
<PortNameInput
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={item.name}
|
value={item.name}
|
||||||
@@ -192,8 +197,6 @@ const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => {
|
|||||||
|
|
||||||
const updatePorts = (list: PortItemType[]) => {
|
const updatePorts = (list: PortItemType[]) => {
|
||||||
form.setFieldValue(['spec', 'ports'], list);
|
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 = () => {
|
const handleAdd = () => {
|
||||||
@@ -231,13 +234,19 @@ const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => {
|
|||||||
|
|
||||||
const items = value as PortItemType[];
|
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(
|
return Promise.reject(
|
||||||
new Error(
|
new Error(
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: 'common.validate.value' },
|
{ id: 'common.validate.group' },
|
||||||
{
|
{
|
||||||
name: intl.formatMessage({
|
group: intl.formatMessage({
|
||||||
id: 'gpuservice.template.ports'
|
id: 'gpuservice.template.ports'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -248,16 +257,6 @@ const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => {
|
|||||||
|
|
||||||
const namedItems = items.filter((item) => !!item.name);
|
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);
|
const names = namedItems.map((item) => item.name);
|
||||||
if (_.uniq(names).length !== names.length) {
|
if (_.uniq(names).length !== names.length) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user