fix: display templates by manufacturer

This commit is contained in:
jialin
2026-05-11 19:49:21 +08:00
committed by jialin
parent 4128de61f9
commit 9e0dd5b003
27 changed files with 614 additions and 194 deletions
+43 -1
View File
@@ -31,5 +31,47 @@ export default {
'gpuservice.template.card.ports': 'Ports',
'gpuservice.publicKey.label': 'SSH Public Key',
'gpuservice.publicKey.placeholder':
'Begin with ssh-rsa or ssh-ed25519. One Public Key per line.\n\nView Public Key:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub'
'Begin with ssh-rsa or ssh-ed25519. One Public Key per line.\n\nView Public Key:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
'gpuservice.instance': 'GPU Instance',
'gpuservice.instance.add': 'Add GPU Instance',
'gpuservice.instance.edit': 'Edit GPU Instance',
'gpuservice.instance.filter.cluster': 'Filter by cluster',
'gpuservice.instance.name': 'Instance Name',
'gpuservice.instance.name.required': 'Please enter the instance name',
'gpuservice.instance.section.basic': 'Basic Info',
'gpuservice.instance.section.type': 'Instance Type',
'gpuservice.instance.section.template': 'Instance Template',
'gpuservice.instance.section.storage': 'Storage Volume',
'gpuservice.instance.type.required': 'Please select an instance type',
'gpuservice.instance.gpuCount': 'GPU Count',
'gpuservice.instance.gpuCount.required': 'Please enter the GPU count',
'gpuservice.instance.gpuCount.max':
'The current instance type supports at most {count} GPU(s)',
'gpuservice.instance.stock': 'Stock',
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'Memory',
'gpuservice.instance.search.type.placeholder':
'Search by name, VRAM, memory or vCPU',
'gpuservice.instance.search.template.placeholder':
'Search by template name, image or mount path',
'gpuservice.instance.template.image': 'Image',
'gpuservice.instance.template.mount': 'Mount',
'gpuservice.storage': 'Storage',
'gpuservice.storage.add': 'Add Storage',
'gpuservice.storage.edit': 'Edit Storage',
'gpuservice.storage.filter.cluster': 'Filter by cluster',
'gpuservice.storage.type': 'Storage Type',
'gpuservice.storage.type.local': 'Local Storage',
'gpuservice.storage.type.shared': 'Shared Storage',
'gpuservice.storage.type.object': 'Object Storage',
'gpuservice.storage.capacity': 'Capacity',
'gpuservice.storage.accessMode': 'Access Mode',
'gpuservice.storage.persistent': 'Persistent Storage',
'gpuservice.storage.temporary': 'Temporary Storage',
'gpuservice.storage.persistentVolume': 'Persistent Volume',
'gpuservice.storage.persistentVolume.required':
'Please select a persistent volume',
'gpuservice.storage.tempCapacity': 'Storage Capacity (GB)',
'gpuservice.storage.tempCapacity.required':
'Please enter the local temporary storage capacity'
};
+9 -1
View File
@@ -57,5 +57,13 @@ export default {
'noresult.gpuservice.template.subTitle':
'Once you create an instance template, it will appear here.',
'noresult.gpuservice.template.nofound':
'No matching instance templates found.'
'No matching instance templates found.',
'noresult.gpuservice.instance.title': 'No GPU Instances',
'noresult.gpuservice.instance.subTitle':
'Once you create a GPU instance, it will appear here.',
'noresult.gpuservice.instance.nofound': 'No matching GPU instances found.',
'noresult.gpuservice.storage.title': 'No Storage',
'noresult.gpuservice.storage.subTitle':
'Once you create a storage, it will appear here.',
'noresult.gpuservice.storage.nofound': 'No matching storage found.'
};
+43 -1
View File
@@ -29,5 +29,47 @@ export default {
'gpuservice.template.card.ports': 'ポート',
'gpuservice.publicKey.label': 'SSH 公開鍵',
'gpuservice.publicKey.placeholder':
'ssh-rsa または ssh-ed25519 で始まり、各公開鍵は1行ずつ記述します\n\n公開鍵を確認:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub'
'ssh-rsa または ssh-ed25519 で始まり、各公開鍵は1行ずつ記述します\n\n公開鍵を確認:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
'gpuservice.instance': 'GPU インスタンス',
'gpuservice.instance.add': 'GPU インスタンスを追加',
'gpuservice.instance.edit': 'GPU インスタンスを編集',
'gpuservice.instance.filter.cluster': 'クラスターでフィルター',
'gpuservice.instance.name': 'インスタンス名',
'gpuservice.instance.name.required': 'インスタンス名を入力してください',
'gpuservice.instance.section.basic': '基本情報',
'gpuservice.instance.section.type': 'インスタンスタイプ',
'gpuservice.instance.section.template': 'インスタンステンプレート',
'gpuservice.instance.section.storage': 'ストレージボリューム',
'gpuservice.instance.type.required': 'インスタンスタイプを選択してください',
'gpuservice.instance.gpuCount': 'GPU 数',
'gpuservice.instance.gpuCount.required': 'GPU 数を入力してください',
'gpuservice.instance.gpuCount.max':
'現在のインスタンスタイプは最大 {count} 個の GPU をサポートします',
'gpuservice.instance.stock': '在庫',
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'メモリ',
'gpuservice.instance.search.type.placeholder':
'名前、VRAM、メモリまたは vCPU で検索',
'gpuservice.instance.search.template.placeholder':
'テンプレート名、イメージまたはマウントパスで検索',
'gpuservice.instance.template.image': 'イメージ',
'gpuservice.instance.template.mount': 'マウント',
'gpuservice.storage': 'ストレージ',
'gpuservice.storage.add': 'ストレージを追加',
'gpuservice.storage.edit': 'ストレージを編集',
'gpuservice.storage.filter.cluster': 'クラスターでフィルター',
'gpuservice.storage.type': 'ストレージタイプ',
'gpuservice.storage.type.local': 'ローカルストレージ',
'gpuservice.storage.type.shared': '共有ストレージ',
'gpuservice.storage.type.object': 'オブジェクトストレージ',
'gpuservice.storage.capacity': '容量',
'gpuservice.storage.accessMode': 'アクセスモード',
'gpuservice.storage.persistent': '永続ストレージ',
'gpuservice.storage.temporary': '一時ストレージ',
'gpuservice.storage.persistentVolume': '永続ボリューム',
'gpuservice.storage.persistentVolume.required':
'永続ボリュームを選択してください',
'gpuservice.storage.tempCapacity': 'ストレージ容量 (GB)',
'gpuservice.storage.tempCapacity.required':
'ローカルの一時ストレージ容量を入力してください'
};
+10 -1
View File
@@ -57,5 +57,14 @@ export default {
'noresult.gpuservice.template.subTitle':
'インスタンステンプレートを作成すると、ここに表示されます。',
'noresult.gpuservice.template.nofound':
'一致するインスタンステンプレートが見つかりません。'
'一致するインスタンステンプレートが見つかりません。',
'noresult.gpuservice.instance.title': 'GPU インスタンスなし',
'noresult.gpuservice.instance.subTitle':
'GPU インスタンスを作成すると、ここに表示されます。',
'noresult.gpuservice.instance.nofound':
'一致する GPU インスタンスが見つかりません。',
'noresult.gpuservice.storage.title': 'ストレージなし',
'noresult.gpuservice.storage.subTitle':
'ストレージを作成すると、ここに表示されます。',
'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。'
};
+42 -1
View File
@@ -31,5 +31,46 @@ export default {
'gpuservice.template.card.ports': 'Порты',
'gpuservice.publicKey.label': 'Открытый ключ SSH',
'gpuservice.publicKey.placeholder':
'Начинается с ssh-rsa или ssh-ed25519, по одному открытому ключу на строку\n\nПросмотр открытого ключа:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub'
'Начинается с ssh-rsa или ssh-ed25519, по одному открытому ключу на строку\n\nПросмотр открытого ключа:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
'gpuservice.instance': 'Экземпляр GPU',
'gpuservice.instance.add': 'Добавить экземпляр GPU',
'gpuservice.instance.edit': 'Редактировать экземпляр GPU',
'gpuservice.instance.filter.cluster': 'Фильтр по кластеру',
'gpuservice.instance.name': 'Имя экземпляра',
'gpuservice.instance.name.required': 'Введите имя экземпляра',
'gpuservice.instance.section.basic': 'Основная информация',
'gpuservice.instance.section.type': 'Тип экземпляра',
'gpuservice.instance.section.template': 'Шаблон экземпляра',
'gpuservice.instance.section.storage': 'Том хранилища',
'gpuservice.instance.type.required': 'Выберите тип экземпляра',
'gpuservice.instance.gpuCount': 'Количество GPU',
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
'gpuservice.instance.gpuCount.max':
'Текущий тип экземпляра поддерживает максимум {count} GPU',
'gpuservice.instance.stock': 'Остаток',
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'Память',
'gpuservice.instance.search.type.placeholder':
'Поиск по имени, VRAM, памяти или vCPU',
'gpuservice.instance.search.template.placeholder':
'Поиск по имени шаблона, образу или пути монтирования',
'gpuservice.instance.template.image': 'Образ',
'gpuservice.instance.template.mount': 'Монтирование',
'gpuservice.storage': 'Хранилище',
'gpuservice.storage.add': 'Добавить хранилище',
'gpuservice.storage.edit': 'Редактировать хранилище',
'gpuservice.storage.filter.cluster': 'Фильтр по кластеру',
'gpuservice.storage.type': 'Тип хранилища',
'gpuservice.storage.type.local': 'Локальное хранилище',
'gpuservice.storage.type.shared': 'Общее хранилище',
'gpuservice.storage.type.object': 'Объектное хранилище',
'gpuservice.storage.capacity': 'Объём',
'gpuservice.storage.accessMode': 'Режим доступа',
'gpuservice.storage.persistent': 'Постоянное хранилище',
'gpuservice.storage.temporary': 'Временное хранилище',
'gpuservice.storage.persistentVolume': 'Постоянный том',
'gpuservice.storage.persistentVolume.required': 'Выберите постоянный том',
'gpuservice.storage.tempCapacity': 'Объём хранилища (ГБ)',
'gpuservice.storage.tempCapacity.required':
'Введите объём локального временного хранилища'
};
+10 -1
View File
@@ -58,7 +58,16 @@ export default {
'noresult.gpuservice.template.subTitle':
'После создания шаблона экземпляра он появится здесь.',
'noresult.gpuservice.template.nofound':
'Подходящие шаблоны экземпляров не найдены.'
'Подходящие шаблоны экземпляров не найдены.',
'noresult.gpuservice.instance.title': 'Нет экземпляров GPU',
'noresult.gpuservice.instance.subTitle':
'После создания экземпляра GPU он появится здесь.',
'noresult.gpuservice.instance.nofound':
'Подходящие экземпляры GPU не найдены.',
'noresult.gpuservice.storage.title': 'Нет хранилищ',
'noresult.gpuservice.storage.subTitle':
'После создания хранилища оно появится здесь.',
'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+43 -1
View File
@@ -30,5 +30,47 @@ export default {
'gpuservice.template.card.ports': 'Bağlantı Noktaları',
'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
'gpuservice.publicKey.placeholder':
'ssh-rsa veya ssh-ed25519 ile başlar, her açık anahtar ayrı bir satırda\n\nAçık anahtarı görüntüle:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub'
'ssh-rsa veya ssh-ed25519 ile başlar, her açık anahtar ayrı bir satırda\n\nAçık anahtarı görüntüle:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
'gpuservice.instance': 'GPU Örneği',
'gpuservice.instance.add': 'GPU Örneği Ekle',
'gpuservice.instance.edit': 'GPU Örneğini Düzenle',
'gpuservice.instance.filter.cluster': 'Kümeye göre filtrele',
'gpuservice.instance.name': 'Örnek Adı',
'gpuservice.instance.name.required': 'Lütfen örnek adını girin',
'gpuservice.instance.section.basic': 'Temel Bilgiler',
'gpuservice.instance.section.type': 'Örnek Türü',
'gpuservice.instance.section.template': 'Örnek Şablonu',
'gpuservice.instance.section.storage': 'Depolama Hacmi',
'gpuservice.instance.type.required': 'Lütfen bir örnek türü seçin',
'gpuservice.instance.gpuCount': 'GPU Sayısı',
'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin',
'gpuservice.instance.gpuCount.max':
'Mevcut örnek türü en fazla {count} GPU destekler',
'gpuservice.instance.stock': 'Stok',
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'Bellek',
'gpuservice.instance.search.type.placeholder':
"Ada, VRAM, belleğe veya vCPU'ya göre ara",
'gpuservice.instance.search.template.placeholder':
'Şablon adına, imaja veya bağlama yoluna göre ara',
'gpuservice.instance.template.image': 'İmaj',
'gpuservice.instance.template.mount': 'Bağlama',
'gpuservice.storage': 'Depolama',
'gpuservice.storage.add': 'Depolama Ekle',
'gpuservice.storage.edit': 'Depolamayı Düzenle',
'gpuservice.storage.filter.cluster': 'Kümeye göre filtrele',
'gpuservice.storage.type': 'Depolama Türü',
'gpuservice.storage.type.local': 'Yerel Depolama',
'gpuservice.storage.type.shared': 'Paylaşımlı Depolama',
'gpuservice.storage.type.object': 'Nesne Depolama',
'gpuservice.storage.capacity': 'Kapasite',
'gpuservice.storage.accessMode': 'Erişim Modu',
'gpuservice.storage.persistent': 'Kalıcı Depolama',
'gpuservice.storage.temporary': 'Geçici Depolama',
'gpuservice.storage.persistentVolume': 'Kalıcı Hacim',
'gpuservice.storage.persistentVolume.required':
'Lütfen bir kalıcı hacim seçin',
'gpuservice.storage.tempCapacity': 'Depolama Kapasitesi (GB)',
'gpuservice.storage.tempCapacity.required':
'Lütfen yerel geçici depolama kapasitesini girin'
};
+9 -1
View File
@@ -58,7 +58,15 @@ export default {
'noresult.gpuservice.template.title': 'Örnek Şablonu Yok',
'noresult.gpuservice.template.subTitle':
'Bir örnek şablonu oluşturduğunuzda burada görünecek.',
'noresult.gpuservice.template.nofound': 'Eşleşen örnek şablonu bulunamadı.'
'noresult.gpuservice.template.nofound': 'Eşleşen örnek şablonu bulunamadı.',
'noresult.gpuservice.instance.title': 'GPU Örneği Yok',
'noresult.gpuservice.instance.subTitle':
'Bir GPU örneği oluşturduğunuzda burada görünecek.',
'noresult.gpuservice.instance.nofound': 'Eşleşen GPU örneği bulunamadı.',
'noresult.gpuservice.storage.title': 'Depolama Yok',
'noresult.gpuservice.storage.subTitle':
'Bir depolama oluşturduğunuzda burada görünecek.',
'noresult.gpuservice.storage.nofound': 'Eşleşen depolama bulunamadı.'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+39 -1
View File
@@ -29,5 +29,43 @@ export default {
'gpuservice.template.card.ports': '端口',
'gpuservice.publicKey.label': 'SSH 公钥',
'gpuservice.publicKey.placeholder':
'以 ssh-rsa 或 ssh-ed25519 开头,每个 Public Key 单独一行\n\n查看 Public Key\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub'
'以 ssh-rsa 或 ssh-ed25519 开头,每个 Public Key 单独一行\n\n查看 Public Key\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub',
'gpuservice.instance': 'GPU 实例',
'gpuservice.instance.add': '添加 GPU 实例',
'gpuservice.instance.edit': '编辑 GPU 实例',
'gpuservice.instance.filter.cluster': '按集群过滤',
'gpuservice.instance.name': '实例名称',
'gpuservice.instance.name.required': '请输入实例名称',
'gpuservice.instance.section.basic': '基础信息',
'gpuservice.instance.section.type': '实例类型',
'gpuservice.instance.section.template': '实例模板',
'gpuservice.instance.section.storage': '存储卷',
'gpuservice.instance.type.required': '请选择实例类型',
'gpuservice.instance.gpuCount': 'GPU 数量',
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
'gpuservice.instance.gpuCount.max': '当前实例类型最多支持 {count} 个 GPU',
'gpuservice.instance.stock': '库存',
'gpuservice.instance.memory': '显存',
'gpuservice.instance.ram': '内存',
'gpuservice.instance.search.type.placeholder': '搜索名称、显存、内存或 vCPU',
'gpuservice.instance.search.template.placeholder':
'搜索模板名称、镜像或挂载路径',
'gpuservice.instance.template.image': '镜像',
'gpuservice.instance.template.mount': '挂载',
'gpuservice.storage': '存储',
'gpuservice.storage.add': '添加存储',
'gpuservice.storage.edit': '编辑存储',
'gpuservice.storage.filter.cluster': '按集群过滤',
'gpuservice.storage.type': '存储类型',
'gpuservice.storage.type.local': '本地存储',
'gpuservice.storage.type.shared': '共享存储',
'gpuservice.storage.type.object': '对象存储',
'gpuservice.storage.capacity': '容量',
'gpuservice.storage.accessMode': '访问模式',
'gpuservice.storage.persistent': '持久存储',
'gpuservice.storage.temporary': '临时存储',
'gpuservice.storage.persistentVolume': '持久卷',
'gpuservice.storage.persistentVolume.required': '请选择持久卷',
'gpuservice.storage.tempCapacity': '存储容量 (GB)',
'gpuservice.storage.tempCapacity.required': '请输入本地临时存储容量'
};
+7 -1
View File
@@ -53,5 +53,11 @@ export default {
'noresult.instances.nofound': '未找到匹配的实例',
'noresult.gpuservice.template.title': '暂无实例模板',
'noresult.gpuservice.template.subTitle': '创建一个实例模板后会显示在这里',
'noresult.gpuservice.template.nofound': '未找到匹配的实例模板'
'noresult.gpuservice.template.nofound': '未找到匹配的实例模板',
'noresult.gpuservice.instance.title': '暂无 GPU 实例',
'noresult.gpuservice.instance.subTitle': '创建一个 GPU 实例后会显示在这里',
'noresult.gpuservice.instance.nofound': '未找到匹配的 GPU 实例',
'noresult.gpuservice.storage.title': '暂无存储',
'noresult.gpuservice.storage.subTitle': '创建一个存储后会显示在这里',
'noresult.gpuservice.storage.nofound': '未找到匹配的存储'
};
@@ -2,6 +2,7 @@ import { PageActionType } from '@/config/types';
import Separator from '@/pages/llmodels/components/separator';
import { SearchOutlined } from '@ant-design/icons';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Empty, Input, Typography } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
@@ -58,8 +59,10 @@ const AddModal: React.FC<AddModalProps> = ({
data,
onCancel
}) => {
const intl = useIntl();
const form = useRef<any>(null);
const [selectedInstanceType, setSelectedInstanceType] = useState<string>();
const [selectedManufacturer, setSelectedManufacturer] = useState<string>();
const [templateId, setTemplateId] = useState<number>();
const [instanceKeyword, setInstanceKeyword] = useState('');
const [templateKeyword, setTemplateKeyword] = useState('');
@@ -95,17 +98,26 @@ const AddModal: React.FC<AddModalProps> = ({
});
}, [instanceKeyword, instanceTypeList]);
const manufacturerMatchedTemplates = useMemo(() => {
if (!selectedManufacturer) {
return templateList;
}
return templateList.filter(
(item) => item.manufacturer === selectedManufacturer
);
}, [selectedManufacturer, templateList]);
const filteredTemplates = useMemo(() => {
const keyword = templateKeyword.trim().toLowerCase();
if (!keyword) {
return templateList;
return manufacturerMatchedTemplates;
}
return templateList.filter((item) =>
return manufacturerMatchedTemplates.filter((item) =>
[item.name, item.spec?.image, item.spec?.volumeMount].some((text) =>
(text || '').toLowerCase().includes(keyword)
)
);
}, [templateKeyword, templateList]);
}, [templateKeyword, manufacturerMatchedTemplates]);
const handleSubmit = () => {
form.current?.submit();
@@ -124,13 +136,47 @@ const AddModal: React.FC<AddModalProps> = ({
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
const name = item.metadata?.name;
const manufacturer = item.spec?.manufacturer;
setSelectedInstanceType(name);
form.current?.setFieldsValue({
spec: {
type: name,
resources: { accelerator: '1' }
}
});
setSelectedManufacturer(manufacturer);
const currentSpec = form.current?.getFieldsValue()?.spec || {};
const updatedSpec = {
...currentSpec,
type: name,
resources: { ...(currentSpec.resources || {}), accelerator: '1' }
};
const currentTemplate = templateList.find((t) => t.id === templateId);
const isMatched =
!manufacturer ||
(!!currentTemplate && currentTemplate.manufacturer === manufacturer);
if (isMatched) {
form.current?.setFieldsValue({ spec: updatedSpec });
return;
}
const firstTemplate = templateList.find(
(t) => t.manufacturer === manufacturer
);
if (firstTemplate) {
setTemplateId(firstTemplate.id);
form.current?.setFieldsValue({
manufacturer: firstTemplate.manufacturer,
spec: {
...updatedSpec,
...firstTemplate.spec
}
});
} else {
setTemplateId(undefined);
form.current?.setFieldsValue({
manufacturer: undefined,
spec: updatedSpec
});
}
};
const handleTemplateChange = (id: number, item: TemplateItem) => {
@@ -176,7 +222,9 @@ const AddModal: React.FC<AddModalProps> = ({
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder="搜索名称、显存、内存或 vCPU"
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.type.placeholder'
})}
value={instanceKeyword}
onChange={(e) => setInstanceKeyword(e.target.value)}
/>
@@ -208,7 +256,9 @@ const AddModal: React.FC<AddModalProps> = ({
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder="搜索模板名称、镜像或挂载路径"
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.template.placeholder'
})}
value={templateKeyword}
onChange={(e) => setTemplateKeyword(e.target.value)}
/>
@@ -1,4 +1,5 @@
import { AutoTooltip, StatusTag, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Tag } from 'antd';
import styled from 'styled-components';
import {
@@ -55,6 +56,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
onChange,
dataList = []
}) => {
const intl = useIntl();
const handleSelect = (item: InstanceTypeItem) => {
if (!isAvailable(item)) return;
onChange?.(item);
@@ -104,7 +106,8 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
}}
>
<span>
{item.status?.accelerator?.remaining ?? '-'}
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
{item.status?.accelerator?.remaining ?? '-'}
</span>
</Tag>
)}
@@ -114,12 +117,16 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
<span style={{ display: 'flex', height: 15 }}>
{item.spec?.acceleratable && (
<span className="meta-row">
{item.spec?.memory ?? '-'}
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
{item.spec?.memory ?? '-'}
</span>
)}
</span>
<span className="meta-row gap-16">
<span> {item.status?.ram?.capacity ?? '-'}</span>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
{item.status?.ram?.capacity ?? '-'}
</span>
<span>vCPU {item.status?.cpu?.capacity ?? '-'}</span>
</span>
</TypeMeta>
@@ -46,13 +46,15 @@ export const status: Record<string, StatusType> = {
export const rowActionList = [
{
label: '编辑',
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: '删除',
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
+72 -80
View File
@@ -1,31 +1,5 @@
export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never';
export interface InstanceEnvVar {
name: string;
value?: string;
}
export interface InstancePort {
port: number;
protocol?: string;
}
export interface InstanceResources {
cpu?: string;
ram?: string;
localStorage?: string;
accelerator?: string;
}
export interface InstanceVolume {
ephemeral?: {
capacity?: string;
};
persistent?: {
name: string;
};
}
// instance form data
export interface FormData {
metadata: {
@@ -38,63 +12,74 @@ export interface FormData {
imagePullPolicy: ImagePullPolicy;
displayName: string;
command: string[];
ports: InstancePort[];
env: InstanceEnvVar[];
ports: {
port: number;
protocol?: string;
}[];
env: {
name: string;
value?: string;
}[];
volumeMount: string;
resources: InstanceResources;
resources: {
cpu?: string;
ram?: string;
localStorage?: string;
accelerator?: string;
};
description: string;
volume: InstanceVolume;
volume: {
ephemeral?: {
capacity?: string;
};
persistent?: {
name: string;
};
};
sshPublicKey: {
name: string;
};
};
}
export interface ManagedField {
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}
export type InstanceServicePortProtocol = 'TCP' | 'UDP';
export interface Metadata {
name: string;
namespace?: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
managedFields?: ManagedField[];
export interface InstanceStatus {
hostIPs?: {
ip: string;
}[];
phase?: string;
phaseMessage?: string;
podIPs?: {
ip: string;
}[];
ports?: {
port: number;
nodePort?: number;
protocol?: InstanceServicePortProtocol;
}[];
}
// instance list item
export interface ListItem extends Omit<FormData, 'metadata'> {
id: number;
status?: string;
metadata: Metadata;
}
export interface InstanceItem {
id: number;
name: string;
vram: number; // GiB
ram: number; // GiB
vCPU: number; // cores
gpu_count: number;
status: string; // available, unavailable
}
export interface InstanceTypeSpec {
acceleratable: boolean;
computeCapability?: string;
family?: string;
group: string;
manufacturer?: string;
memory?: string;
product?: string;
sliced?: number;
status?: InstanceStatus;
metadata: {
name: string;
namespace?: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
managedFields?: {
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}[];
};
}
type Quality = `${number}Ki` | `${number}Gi`;
@@ -105,15 +90,6 @@ export interface InstanceTypeResource {
remaining?: Quality;
}
export interface InstanceTypeStatus {
accelerator: InstanceTypeResource;
cpu: InstanceTypeResource;
localStorage: InstanceTypeResource;
phase: string;
phaseMessage?: string;
ram: InstanceTypeResource;
}
export interface InstanceTypeItem {
apiVersion: string;
kind: string;
@@ -122,6 +98,22 @@ export interface InstanceTypeItem {
namespace: string;
};
id: number;
spec: InstanceTypeSpec;
status: InstanceTypeStatus;
spec: {
acceleratable: boolean;
computeCapability?: string;
family?: string;
group: string;
manufacturer?: string;
memory?: string;
product?: string;
sliced?: number;
};
status: {
accelerator: InstanceTypeResource;
cpu: InstanceTypeResource;
localStorage: InstanceTypeResource;
phase: string;
phaseMessage?: string;
ram: InstanceTypeResource;
};
}
@@ -1,8 +1,10 @@
import { Input as CInput } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData } from '../config/types';
const Basic = () => {
const intl = useIntl();
return (
<>
<Form.Item<FormData>
@@ -11,14 +13,22 @@ const Basic = () => {
rules={[
{
required: true,
message: '请输入实例名称'
message: intl.formatMessage({
id: 'gpuservice.instance.name.required'
})
}
]}
>
<CInput.Input label="实例名称" required />
<CInput.Input
label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
required
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'description']}>
<CInput.TextArea label="描述" scaleSize />
<CInput.TextArea
label={intl.formatMessage({ id: 'common.table.description' })}
scaleSize
/>
</Form.Item>
</>
);
+24 -12
View File
@@ -9,6 +9,7 @@ import {
useScrollActiveChange,
useWrapperContext
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import {
forwardRef,
@@ -77,6 +78,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
namespace = 'default',
onFinish
} = props;
const intl = useIntl();
const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null);
const { detailData, fetchData } = useGetSshkey();
@@ -98,9 +100,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
useEffect(() => {
if (open) {
fetchData({}).then((res) => {
form?.setFieldValue(['spec', 'sshPublicKey', 'name'], res.name);
});
fetchData({});
}
}, [open]);
@@ -108,30 +108,36 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
() => [
{
value: TABKeysMap.BASIC,
label: '基础信息',
label: intl.formatMessage({
id: 'gpuservice.instance.section.basic'
}),
icon: <IconFont type="icon-basic" />,
field: 'name'
},
{
value: TABKeysMap.INSTANCE_TYPE,
label: '实例类型',
label: intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
icon: <IconFont type="icon-gpu1" />,
field: 'instanceType'
},
{
value: TABKeysMap.TEMPLATE,
label: '实例模板',
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
icon: <IconFont type="icon-model" />,
field: 'template'
},
{
value: TABKeysMap.STORAGE,
label: '存储卷',
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
icon: <IconFont type="icon-storage-outlined" />,
field: 'storage'
}
],
[]
[intl]
);
const ports = (Form.useWatch(['spec', 'ports'], form) ||
@@ -211,7 +217,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const handleFinish = async (values: InstanceFormValues) => {
const payload = buildPayload(values);
await onFinish(payload);
await onFinish(values);
};
useImperativeHandle(ref, () => ({
@@ -275,19 +281,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
items={[
{
key: TABKeysMap.INSTANCE_TYPE,
label: '实例类型',
label: intl.formatMessage({
id: 'gpuservice.instance.section.type'
}),
forceRender: true,
children: <InstanceTypeFormItem />
},
{
key: TABKeysMap.TEMPLATE,
label: '实例模板',
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
forceRender: true,
children: <TemplateBasicForm page="instance" />
},
{
key: TABKeysMap.STORAGE,
label: '存储卷',
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
forceRender: true,
children: <StorageVolume />
}
@@ -1,4 +1,5 @@
import { AutoTooltip, InputNumber as CInputNumber } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form, Tag } from 'antd';
import { useEffect, useMemo } from 'react';
import styled from 'styled-components';
@@ -47,6 +48,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
value,
dataList
}) => {
const intl = useIntl();
const selected = useMemo(() => {
return dataList.find((item) => (item.metadata?.name || '') === value);
}, [value, dataList]);
@@ -67,17 +69,24 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
color: 'var(--ant-color-text-tertiary)'
}}
>
{selected?.status?.accelerator?.remaining ?? '-'}
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
{selected?.status?.accelerator?.remaining ?? '-'}
</Tag>
)}
</Flex>
</SummaryTitle>
<SummaryMeta>
{selected?.spec?.acceleratable && (
<span> {selected?.spec?.memory ?? '-'}</span>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
{selected?.spec?.memory ?? '-'}
</span>
)}
<Flex gap={16}>
<span> {selected?.status?.ram?.capacity ?? '-'}</span>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
{selected?.status?.ram?.capacity ?? '-'}
</span>
<span>vCPU {selected?.status?.cpu?.capacity ?? '-'}</span>
</Flex>
</SummaryMeta>
@@ -88,6 +97,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
};
const InstanceTypeFormItem = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
@@ -132,14 +142,16 @@ const InstanceTypeFormItem = () => {
}, [form, typeName, maxGpuCount, dataList]);
return (
<>
<FieldBlock data-field="instanceType">
<div data-field="instanceType">
<FieldBlock>
<Form.Item
name={['spec', 'type']}
rules={[
{
required: true,
message: '请选择实例类型'
message: intl.formatMessage({
id: 'gpuservice.instance.type.required'
})
}
]}
>
@@ -152,13 +164,20 @@ const InstanceTypeFormItem = () => {
rules={[
{
required: true,
message: '请输入 GPU 数量'
message: intl.formatMessage({
id: 'gpuservice.instance.gpuCount.required'
})
},
{
validator: (_, value) => {
if (value > maxGpuCount) {
return Promise.reject(
new Error(`当前实例类型最多支持 ${maxGpuCount} 个 GPU`)
new Error(
intl.formatMessage(
{ id: 'gpuservice.instance.gpuCount.max' },
{ count: maxGpuCount }
)
)
);
}
return Promise.resolve();
@@ -170,12 +189,12 @@ const InstanceTypeFormItem = () => {
min={1}
max={maxGpuCount}
precision={0}
label="GPU 数量"
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
required
/>
</Form.Item>
)}
</>
</div>
);
};
@@ -1,6 +1,7 @@
import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view';
import { ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useCallback, useRef } from 'react';
import { FormData as StorageFormData } from '../../storage/config/types';
import GPUServiceStorageForm from '../../storage/forms';
@@ -18,6 +19,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
onCancel,
onSubmit
}) => {
const intl = useIntl();
const formRef = useRef<any>(null);
const handleSubmit = () => {
@@ -43,7 +45,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
return (
<FormOverlayView
title="添加存储"
title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
open={open}
width={600}
onCancel={handleCancel}
@@ -1,5 +1,6 @@
import { useModel } from '@@/plugin-model';
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Flex, Form, Radio } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
@@ -17,6 +18,7 @@ const FieldBlock = styled.div`
const DEFAULT_TEMP_CAPACITY_GB = 50;
const StorageVolume = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState');
const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } =
@@ -92,8 +94,16 @@ const StorageVolume = () => {
value={storageMode}
onChange={(e) => handleModeChange(e.target.value)}
options={[
{ label: '持久存储', value: StorageModeValueMap.Existing },
{ label: '临时存储', value: StorageModeValueMap.Temporary }
{
label: intl.formatMessage({
id: 'gpuservice.storage.persistent'
}),
value: StorageModeValueMap.Existing
},
{
label: intl.formatMessage({ id: 'gpuservice.storage.temporary' }),
value: StorageModeValueMap.Temporary
}
]}
/>
<Button
@@ -102,7 +112,7 @@ const StorageVolume = () => {
style={{ marginBottom: 6 }}
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
</Flex>
@@ -112,11 +122,19 @@ const StorageVolume = () => {
rules={[
{
required: true,
message: '请选择持久卷'
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
>
<Select label="持久卷" required options={storageOptions} />
<Select
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
required
options={storageOptions}
/>
</Form.Item>
)}
@@ -132,11 +150,20 @@ const StorageVolume = () => {
rules={[
{
required: true,
message: '请输入本地临时存储容量'
message: intl.formatMessage({
id: 'gpuservice.storage.tempCapacity.required'
})
}
]}
>
<CInputNumber min={1} precision={0} label="存储容量 (GB)" required />
<CInputNumber
min={1}
precision={0}
label={intl.formatMessage({
id: 'gpuservice.storage.tempCapacity'
})}
required
/>
</Form.Item>
)}
@@ -1,4 +1,5 @@
import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import styled from 'styled-components';
import { ListItem as TemplateItem } from '../../templates/config/types';
@@ -51,6 +52,7 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
onChange,
dataList = []
}) => {
const intl = useIntl();
return (
<TemplateGrid>
{dataList.map((item: TemplateItem) => (
@@ -71,7 +73,11 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
</div>
<div className="info">
<span>
<IconFont className="icon" type="icon-model" /> :
<IconFont className="icon" type="icon-model" />{' '}
{intl.formatMessage({
id: 'gpuservice.instance.template.image'
})}
:
</span>
<AutoTooltip ghost minWidth={20}>
<span className="value">{item.spec?.image || '-'}</span>
@@ -79,7 +85,11 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
</div>
<div className="info">
<span>
<IconFont className="icon" type="icon-storage-outlined" /> :
<IconFont className="icon" type="icon-storage-outlined" />{' '}
{intl.formatMessage({
id: 'gpuservice.instance.template.mount'
})}
:
</span>
<span className="value">{item.spec?.volumeMount || '-'}</span>
</div>
@@ -1,5 +1,6 @@
import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
@@ -15,10 +16,11 @@ const useInstancesColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: '名称',
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'],
key: 'name',
sorter: tableSorter(1),
@@ -32,7 +34,7 @@ const useInstancesColumns = ({
)
},
{
title: '状态',
title: intl.formatMessage({ id: 'common.table.status' }),
dataIndex: ['status', 'phase'],
key: 'status',
sorter: tableSorter(2),
@@ -48,7 +50,7 @@ const useInstancesColumns = ({
}
},
{
title: '镜像',
title: intl.formatMessage({ id: 'gpuservice.template.image' }),
dataIndex: ['spec', 'image'],
key: 'image',
sorter: tableSorter(3),
@@ -60,7 +62,7 @@ const useInstancesColumns = ({
)
},
{
title: '创建时间',
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp',
sorter: tableSorter(5),
@@ -74,7 +76,7 @@ const useInstancesColumns = ({
)
},
{
title: '操作',
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
@@ -85,7 +87,7 @@ const useInstancesColumns = ({
)
}
];
}, [handleSelect, sortOrder]);
}, [handleSelect, sortOrder, intl]);
};
export default useInstancesColumns;
+21 -13
View File
@@ -32,6 +32,7 @@ import useCreateInstance from './services/use-create-instance';
import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
@@ -97,7 +98,7 @@ const GPUService: React.FC = () => {
deleteAPI: deleteInstance,
watch: true,
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
contentForDelete: 'GPU 实例'
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
});
const { fetchData: createInstance } = useCreateInstance();
@@ -107,7 +108,6 @@ const GPUService: React.FC = () => {
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo(
() =>
@@ -155,7 +155,7 @@ const GPUService: React.FC = () => {
const handleAddInstance = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '添加 GPU 实例',
title: intl.formatMessage({ id: 'gpuservice.instance.add' }),
open: true,
currentData: null
});
@@ -164,7 +164,7 @@ const GPUService: React.FC = () => {
const handleEditInstance = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: '编辑 GPU 实例',
title: intl.formatMessage({ id: 'gpuservice.instance.edit' }),
open: true,
currentData: row
});
@@ -192,9 +192,9 @@ const GPUService: React.FC = () => {
fetchData();
closeModal();
message.success('操作成功');
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
message.error('操作失败');
message.error(intl.formatMessage({ id: 'common.message.fail' }));
}
};
@@ -224,13 +224,19 @@ const GPUService: React.FC = () => {
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-instances-outlined" />}
image={<IconFont type="icon-cloud-outlined" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的 GPU 实例"
title="暂无 GPU 实例"
subTitle="创建一个 GPU 实例后会显示在这里"
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.instance.nofound'
})}
title={intl.formatMessage({
id: 'noresult.gpuservice.instance.title'
})}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.instance.subTitle'
})}
onClick={handleAddInstance}
buttonText="立即添加"
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
};
@@ -267,8 +273,10 @@ const GPUService: React.FC = () => {
showSelect={false}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder="按集群过滤"
buttonText="添加 GPU 实例"
selectHolder={intl.formatMessage({
id: 'gpuservice.instance.filter.cluster'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })}
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch}
+13 -8
View File
@@ -7,23 +7,26 @@ export const StorageTypeValueMap = {
};
export const StorageTypeLabelMap: Record<string, string> = {
[StorageTypeValueMap.Local]: '本地存储',
[StorageTypeValueMap.Shared]: '共享存储',
[StorageTypeValueMap.Object]: '对象存储'
[StorageTypeValueMap.Local]: 'gpuservice.storage.type.local',
[StorageTypeValueMap.Shared]: 'gpuservice.storage.type.shared',
[StorageTypeValueMap.Object]: 'gpuservice.storage.type.object'
};
export const StorageTypeOptions = [
{
label: StorageTypeLabelMap[StorageTypeValueMap.Local],
value: StorageTypeValueMap.Local
value: StorageTypeValueMap.Local,
locale: true
},
{
label: StorageTypeLabelMap[StorageTypeValueMap.Shared],
value: StorageTypeValueMap.Shared
value: StorageTypeValueMap.Shared,
locale: true
},
{
label: StorageTypeLabelMap[StorageTypeValueMap.Object],
value: StorageTypeValueMap.Object
value: StorageTypeValueMap.Object,
locale: true
}
];
@@ -76,13 +79,15 @@ export const ReClaimPolicyOptions = [
export const rowActionList = [
{
label: '编辑',
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: '删除',
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
+23 -6
View File
@@ -4,12 +4,14 @@ import {
Select as SealSelect,
useAppUtils
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form } from 'antd';
import { useEffect } from 'react';
import { FormData } from '../config/types';
import useQueryStorageClass from '../services/use-query-storage-class';
const Basic = ({ open }: { open: boolean }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const { storageClassList, fetchData, loading } = useQueryStorageClass();
@@ -26,11 +28,17 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[
{
required: true,
message: getRuleMessage('input', '名称')
message: getRuleMessage(
'input',
intl.formatMessage({ id: 'common.table.name' })
)
}
]}
>
<CInput.Input label="名称" required />
<CInput.Input
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
<Flex gap={16}>
<div style={{ flex: 1 }}>
@@ -39,12 +47,15 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[
{
required: true,
message: getRuleMessage('select', '存储类型')
message: getRuleMessage(
'select',
intl.formatMessage({ id: 'gpuservice.storage.type' })
)
}
]}
>
<SealSelect
label="存储类型"
label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
required
loading={loading}
options={storageClassList}
@@ -61,11 +72,17 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[
{
required: true,
message: getRuleMessage('input', '容量')
message: getRuleMessage(
'input',
intl.formatMessage({ id: 'gpuservice.storage.capacity' })
)
}
]}
>
<InputNumber label="容量" required />
<InputNumber
label={intl.formatMessage({ id: 'gpuservice.storage.capacity' })}
required
/>
</Form.Item>
</div>
</Flex>
@@ -1,5 +1,6 @@
import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
@@ -15,10 +16,11 @@ const useStorageColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: '名称',
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'],
key: 'name',
sorter: tableSorter(1),
@@ -32,28 +34,31 @@ const useStorageColumns = ({
)
},
{
title: '类型',
title: intl.formatMessage({ id: 'common.table.type' }),
dataIndex: ['spec', 'type'],
key: 'type',
sorter: tableSorter(2),
render: (value: string) => StorageTypeLabelMap[value] || value || '-'
render: (value: string) =>
StorageTypeLabelMap[value]
? intl.formatMessage({ id: StorageTypeLabelMap[value] })
: value || '-'
},
{
title: '容量',
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
dataIndex: ['spec', 'capacity'],
key: 'capacity',
sorter: tableSorter(3),
render: (value: string) => value ?? '-'
},
{
title: '访问模式',
title: intl.formatMessage({ id: 'gpuservice.storage.accessMode' }),
dataIndex: ['spec', 'accessMode'],
key: 'accessMode',
sorter: tableSorter(4),
render: (value: string) => value || '-'
},
{
title: '创建时间',
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp',
sorter: tableSorter(5),
@@ -67,7 +72,7 @@ const useStorageColumns = ({
)
},
{
title: '操作',
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
@@ -78,7 +83,7 @@ const useStorageColumns = ({
)
}
];
}, [handleSelect, sortOrder]);
}, [handleSelect, sortOrder, intl]);
};
export default useStorageColumns;
+18 -12
View File
@@ -33,6 +33,7 @@ import useCreateStorage from './services/use-create-storage';
import useUpdateStorage from './services/use-update-storage';
const GPUServiceStorage: React.FC = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
@@ -98,7 +99,7 @@ const GPUServiceStorage: React.FC = () => {
deleteAPI: deleteStorage,
watch: false,
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
contentForDelete: '存储'
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
});
const { fetchData: createStorage } = useCreateStorage();
@@ -108,7 +109,6 @@ const GPUServiceStorage: React.FC = () => {
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo(
() =>
@@ -156,7 +156,7 @@ const GPUServiceStorage: React.FC = () => {
const handleAddStorage = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '添加存储',
title: intl.formatMessage({ id: 'gpuservice.storage.add' }),
open: true,
currentData: null
});
@@ -165,7 +165,7 @@ const GPUServiceStorage: React.FC = () => {
const handleEditStorage = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: '编辑存储',
title: intl.formatMessage({ id: 'gpuservice.storage.edit' }),
open: true,
currentData: row
});
@@ -193,9 +193,9 @@ const GPUServiceStorage: React.FC = () => {
fetchData();
closeModal();
message.success('操作成功');
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
message.error('操作失败');
message.error(intl.formatMessage({ id: 'common.message.fail' }));
}
};
@@ -227,11 +227,15 @@ const GPUServiceStorage: React.FC = () => {
dataSource={dataSource.dataList}
image={<IconFont type="icon-storage-outlined" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的存储"
title="暂无存储"
subTitle="创建一个存储后会显示在这里"
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.storage.nofound'
})}
title={intl.formatMessage({ id: 'noresult.gpuservice.storage.title' })}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.storage.subTitle'
})}
onClick={handleAddStorage}
buttonText="立即添加"
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
};
@@ -269,8 +273,10 @@ const GPUServiceStorage: React.FC = () => {
showSelect={false}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder="按集群过滤"
buttonText="添加存储"
selectHolder={intl.formatMessage({
id: 'gpuservice.storage.filter.cluster'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.storage.add' })}
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch}
@@ -34,6 +34,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
return (
<>
<div data-field="template"></div>
{page === 'template' && (
<>
<Form.Item<FormData>