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.template.card.ports': 'Ports',
'gpuservice.publicKey.label': 'SSH Public Key', 'gpuservice.publicKey.label': 'SSH Public Key',
'gpuservice.publicKey.placeholder': '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': 'noresult.gpuservice.template.subTitle':
'Once you create an instance template, it will appear here.', 'Once you create an instance template, it will appear here.',
'noresult.gpuservice.template.nofound': '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.template.card.ports': 'ポート',
'gpuservice.publicKey.label': 'SSH 公開鍵', 'gpuservice.publicKey.label': 'SSH 公開鍵',
'gpuservice.publicKey.placeholder': '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.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': '一致するストレージが見つかりません。'
}; };
+42 -1
View File
@@ -31,5 +31,46 @@ export default {
'gpuservice.template.card.ports': 'Порты', 'gpuservice.template.card.ports': 'Порты',
'gpuservice.publicKey.label': 'Открытый ключ SSH', 'gpuservice.publicKey.label': 'Открытый ключ SSH',
'gpuservice.publicKey.placeholder': '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.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': 'Подходящие хранилища не найдены.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== 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.template.card.ports': 'Bağlantı Noktaları',
'gpuservice.publicKey.label': 'SSH Açık Anahtarı', 'gpuservice.publicKey.label': 'SSH Açık Anahtarı',
'gpuservice.publicKey.placeholder': '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.title': 'Örnek Şablonu Yok',
'noresult.gpuservice.template.subTitle': 'noresult.gpuservice.template.subTitle':
'Bir örnek şablonu oluşturduğunuzda burada görünecek.', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
+39 -1
View File
@@ -29,5 +29,43 @@ export default {
'gpuservice.template.card.ports': '端口', 'gpuservice.template.card.ports': '端口',
'gpuservice.publicKey.label': 'SSH 公钥', 'gpuservice.publicKey.label': 'SSH 公钥',
'gpuservice.publicKey.placeholder': '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.instances.nofound': '未找到匹配的实例',
'noresult.gpuservice.template.title': '暂无实例模板', 'noresult.gpuservice.template.title': '暂无实例模板',
'noresult.gpuservice.template.subTitle': '创建一个实例模板后会显示在这里', '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 Separator from '@/pages/llmodels/components/separator';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui'; import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Empty, Input, Typography } from 'antd'; import { Empty, Input, Typography } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -58,8 +59,10 @@ const AddModal: React.FC<AddModalProps> = ({
data, data,
onCancel onCancel
}) => { }) => {
const intl = useIntl();
const form = useRef<any>(null); const form = useRef<any>(null);
const [selectedInstanceType, setSelectedInstanceType] = useState<string>(); const [selectedInstanceType, setSelectedInstanceType] = useState<string>();
const [selectedManufacturer, setSelectedManufacturer] = useState<string>();
const [templateId, setTemplateId] = useState<number>(); const [templateId, setTemplateId] = useState<number>();
const [instanceKeyword, setInstanceKeyword] = useState(''); const [instanceKeyword, setInstanceKeyword] = useState('');
const [templateKeyword, setTemplateKeyword] = useState(''); const [templateKeyword, setTemplateKeyword] = useState('');
@@ -95,17 +98,26 @@ const AddModal: React.FC<AddModalProps> = ({
}); });
}, [instanceKeyword, instanceTypeList]); }, [instanceKeyword, instanceTypeList]);
const manufacturerMatchedTemplates = useMemo(() => {
if (!selectedManufacturer) {
return templateList;
}
return templateList.filter(
(item) => item.manufacturer === selectedManufacturer
);
}, [selectedManufacturer, templateList]);
const filteredTemplates = useMemo(() => { const filteredTemplates = useMemo(() => {
const keyword = templateKeyword.trim().toLowerCase(); const keyword = templateKeyword.trim().toLowerCase();
if (!keyword) { if (!keyword) {
return templateList; return manufacturerMatchedTemplates;
} }
return templateList.filter((item) => return manufacturerMatchedTemplates.filter((item) =>
[item.name, item.spec?.image, item.spec?.volumeMount].some((text) => [item.name, item.spec?.image, item.spec?.volumeMount].some((text) =>
(text || '').toLowerCase().includes(keyword) (text || '').toLowerCase().includes(keyword)
) )
); );
}, [templateKeyword, templateList]); }, [templateKeyword, manufacturerMatchedTemplates]);
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); form.current?.submit();
@@ -124,13 +136,47 @@ const AddModal: React.FC<AddModalProps> = ({
const handleInstanceTypeChange = (item: InstanceTypeItem) => { const handleInstanceTypeChange = (item: InstanceTypeItem) => {
const name = item.metadata?.name; const name = item.metadata?.name;
const manufacturer = item.spec?.manufacturer;
setSelectedInstanceType(name); setSelectedInstanceType(name);
form.current?.setFieldsValue({ setSelectedManufacturer(manufacturer);
spec: {
type: name, const currentSpec = form.current?.getFieldsValue()?.spec || {};
resources: { accelerator: '1' } 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) => { const handleTemplateChange = (id: number, item: TemplateItem) => {
@@ -176,7 +222,9 @@ const AddModal: React.FC<AddModalProps> = ({
<Input <Input
allowClear allowClear
prefix={<SearchOutlined className="text-tertiary" />} prefix={<SearchOutlined className="text-tertiary" />}
placeholder="搜索名称、显存、内存或 vCPU" placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.type.placeholder'
})}
value={instanceKeyword} value={instanceKeyword}
onChange={(e) => setInstanceKeyword(e.target.value)} onChange={(e) => setInstanceKeyword(e.target.value)}
/> />
@@ -208,7 +256,9 @@ const AddModal: React.FC<AddModalProps> = ({
<Input <Input
allowClear allowClear
prefix={<SearchOutlined className="text-tertiary" />} prefix={<SearchOutlined className="text-tertiary" />}
placeholder="搜索模板名称、镜像或挂载路径" placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.template.placeholder'
})}
value={templateKeyword} value={templateKeyword}
onChange={(e) => setTemplateKeyword(e.target.value)} onChange={(e) => setTemplateKeyword(e.target.value)}
/> />
@@ -1,4 +1,5 @@
import { AutoTooltip, StatusTag, TemplateCard } from '@gpustack/core-ui'; import { AutoTooltip, StatusTag, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Tag } from 'antd'; import { Flex, Tag } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
@@ -55,6 +56,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
onChange, onChange,
dataList = [] dataList = []
}) => { }) => {
const intl = useIntl();
const handleSelect = (item: InstanceTypeItem) => { const handleSelect = (item: InstanceTypeItem) => {
if (!isAvailable(item)) return; if (!isAvailable(item)) return;
onChange?.(item); onChange?.(item);
@@ -104,7 +106,8 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
}} }}
> >
<span> <span>
{item.status?.accelerator?.remaining ?? '-'} {intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
{item.status?.accelerator?.remaining ?? '-'}
</span> </span>
</Tag> </Tag>
)} )}
@@ -114,12 +117,16 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
<span style={{ display: 'flex', height: 15 }}> <span style={{ display: 'flex', height: 15 }}>
{item.spec?.acceleratable && ( {item.spec?.acceleratable && (
<span className="meta-row"> <span className="meta-row">
{item.spec?.memory ?? '-'} {intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
{item.spec?.memory ?? '-'}
</span> </span>
)} )}
</span> </span>
<span className="meta-row gap-16"> <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>vCPU {item.status?.cpu?.capacity ?? '-'}</span>
</span> </span>
</TypeMeta> </TypeMeta>
@@ -46,13 +46,15 @@ export const status: Record<string, StatusType> = {
export const rowActionList = [ export const rowActionList = [
{ {
label: '编辑', label: 'common.button.edit',
key: 'edit', key: 'edit',
locale: true,
icon: icons.EditOutlined icon: icons.EditOutlined
}, },
{ {
label: '删除', label: 'common.button.delete',
key: 'delete', key: 'delete',
locale: true,
icon: icons.DeleteOutlined, icon: icons.DeleteOutlined,
props: { props: {
danger: true danger: true
+72 -80
View File
@@ -1,31 +1,5 @@
export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never'; 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 // instance form data
export interface FormData { export interface FormData {
metadata: { metadata: {
@@ -38,63 +12,74 @@ export interface FormData {
imagePullPolicy: ImagePullPolicy; imagePullPolicy: ImagePullPolicy;
displayName: string; displayName: string;
command: string[]; command: string[];
ports: InstancePort[]; ports: {
env: InstanceEnvVar[]; port: number;
protocol?: string;
}[];
env: {
name: string;
value?: string;
}[];
volumeMount: string; volumeMount: string;
resources: InstanceResources; resources: {
cpu?: string;
ram?: string;
localStorage?: string;
accelerator?: string;
};
description: string; description: string;
volume: InstanceVolume; volume: {
ephemeral?: {
capacity?: string;
};
persistent?: {
name: string;
};
};
sshPublicKey: { sshPublicKey: {
name: string; name: string;
}; };
}; };
} }
export interface ManagedField { export type InstanceServicePortProtocol = 'TCP' | 'UDP';
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}
export interface Metadata { export interface InstanceStatus {
name: string; hostIPs?: {
namespace?: string; ip: string;
uid: string; }[];
resourceVersion: string; phase?: string;
creationTimestamp: string; phaseMessage?: string;
annotations?: Record<string, string>; podIPs?: {
managedFields?: ManagedField[]; ip: string;
}[];
ports?: {
port: number;
nodePort?: number;
protocol?: InstanceServicePortProtocol;
}[];
} }
// instance list item // instance list item
export interface ListItem extends Omit<FormData, 'metadata'> { export interface ListItem extends Omit<FormData, 'metadata'> {
id: number; id: number;
status?: string; status?: InstanceStatus;
metadata: Metadata; metadata: {
} name: string;
namespace?: string;
export interface InstanceItem { uid: string;
id: number; resourceVersion: string;
name: string; creationTimestamp: string;
vram: number; // GiB annotations?: Record<string, string>;
ram: number; // GiB managedFields?: {
vCPU: number; // cores manager: string;
gpu_count: number; operation: string;
status: string; // available, unavailable apiVersion: string;
} time: string;
fieldsType: string;
export interface InstanceTypeSpec { fieldsV1: Record<string, any>;
acceleratable: boolean; }[];
computeCapability?: string; };
family?: string;
group: string;
manufacturer?: string;
memory?: string;
product?: string;
sliced?: number;
} }
type Quality = `${number}Ki` | `${number}Gi`; type Quality = `${number}Ki` | `${number}Gi`;
@@ -105,15 +90,6 @@ export interface InstanceTypeResource {
remaining?: Quality; remaining?: Quality;
} }
export interface InstanceTypeStatus {
accelerator: InstanceTypeResource;
cpu: InstanceTypeResource;
localStorage: InstanceTypeResource;
phase: string;
phaseMessage?: string;
ram: InstanceTypeResource;
}
export interface InstanceTypeItem { export interface InstanceTypeItem {
apiVersion: string; apiVersion: string;
kind: string; kind: string;
@@ -122,6 +98,22 @@ export interface InstanceTypeItem {
namespace: string; namespace: string;
}; };
id: number; id: number;
spec: InstanceTypeSpec; spec: {
status: InstanceTypeStatus; 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 { Input as CInput } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
const Basic = () => { const Basic = () => {
const intl = useIntl();
return ( return (
<> <>
<Form.Item<FormData> <Form.Item<FormData>
@@ -11,14 +13,22 @@ const Basic = () => {
rules={[ rules={[
{ {
required: true, 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>
<Form.Item<FormData> name={['spec', 'description']}> <Form.Item<FormData> name={['spec', 'description']}>
<CInput.TextArea label="描述" scaleSize /> <CInput.TextArea
label={intl.formatMessage({ id: 'common.table.description' })}
scaleSize
/>
</Form.Item> </Form.Item>
</> </>
); );
+24 -12
View File
@@ -9,6 +9,7 @@ import {
useScrollActiveChange, useScrollActiveChange,
useWrapperContext useWrapperContext
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { import {
forwardRef, forwardRef,
@@ -77,6 +78,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
namespace = 'default', namespace = 'default',
onFinish onFinish
} = props; } = props;
const intl = useIntl();
const [form] = Form.useForm<InstanceFormValues>(); const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null); const scrollTabsRef = useRef<any>(null);
const { detailData, fetchData } = useGetSshkey(); const { detailData, fetchData } = useGetSshkey();
@@ -98,9 +100,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchData({}).then((res) => { fetchData({});
form?.setFieldValue(['spec', 'sshPublicKey', 'name'], res.name);
});
} }
}, [open]); }, [open]);
@@ -108,30 +108,36 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
() => [ () => [
{ {
value: TABKeysMap.BASIC, value: TABKeysMap.BASIC,
label: '基础信息', label: intl.formatMessage({
id: 'gpuservice.instance.section.basic'
}),
icon: <IconFont type="icon-basic" />, icon: <IconFont type="icon-basic" />,
field: 'name' field: 'name'
}, },
{ {
value: TABKeysMap.INSTANCE_TYPE, value: TABKeysMap.INSTANCE_TYPE,
label: '实例类型', label: intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
icon: <IconFont type="icon-gpu1" />, icon: <IconFont type="icon-gpu1" />,
field: 'instanceType' field: 'instanceType'
}, },
{ {
value: TABKeysMap.TEMPLATE, value: TABKeysMap.TEMPLATE,
label: '实例模板', label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
icon: <IconFont type="icon-model" />, icon: <IconFont type="icon-model" />,
field: 'template' field: 'template'
}, },
{ {
value: TABKeysMap.STORAGE, value: TABKeysMap.STORAGE,
label: '存储卷', label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
icon: <IconFont type="icon-storage-outlined" />, icon: <IconFont type="icon-storage-outlined" />,
field: 'storage' field: 'storage'
} }
], ],
[] [intl]
); );
const ports = (Form.useWatch(['spec', 'ports'], form) || const ports = (Form.useWatch(['spec', 'ports'], form) ||
@@ -211,7 +217,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const handleFinish = async (values: InstanceFormValues) => { const handleFinish = async (values: InstanceFormValues) => {
const payload = buildPayload(values); const payload = buildPayload(values);
await onFinish(payload); await onFinish(values);
}; };
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -275,19 +281,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
items={[ items={[
{ {
key: TABKeysMap.INSTANCE_TYPE, key: TABKeysMap.INSTANCE_TYPE,
label: '实例类型', label: intl.formatMessage({
id: 'gpuservice.instance.section.type'
}),
forceRender: true, forceRender: true,
children: <InstanceTypeFormItem /> children: <InstanceTypeFormItem />
}, },
{ {
key: TABKeysMap.TEMPLATE, key: TABKeysMap.TEMPLATE,
label: '实例模板', label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
forceRender: true, forceRender: true,
children: <TemplateBasicForm page="instance" /> children: <TemplateBasicForm page="instance" />
}, },
{ {
key: TABKeysMap.STORAGE, key: TABKeysMap.STORAGE,
label: '存储卷', label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
forceRender: true, forceRender: true,
children: <StorageVolume /> children: <StorageVolume />
} }
@@ -1,4 +1,5 @@
import { AutoTooltip, InputNumber as CInputNumber } from '@gpustack/core-ui'; import { AutoTooltip, InputNumber as CInputNumber } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form, Tag } from 'antd'; import { Flex, Form, Tag } from 'antd';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -47,6 +48,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
value, value,
dataList dataList
}) => { }) => {
const intl = useIntl();
const selected = useMemo(() => { const selected = useMemo(() => {
return dataList.find((item) => (item.metadata?.name || '') === value); return dataList.find((item) => (item.metadata?.name || '') === value);
}, [value, dataList]); }, [value, dataList]);
@@ -67,17 +69,24 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
color: 'var(--ant-color-text-tertiary)' color: 'var(--ant-color-text-tertiary)'
}} }}
> >
{selected?.status?.accelerator?.remaining ?? '-'} {intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
{selected?.status?.accelerator?.remaining ?? '-'}
</Tag> </Tag>
)} )}
</Flex> </Flex>
</SummaryTitle> </SummaryTitle>
<SummaryMeta> <SummaryMeta>
{selected?.spec?.acceleratable && ( {selected?.spec?.acceleratable && (
<span> {selected?.spec?.memory ?? '-'}</span> <span>
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
{selected?.spec?.memory ?? '-'}
</span>
)} )}
<Flex gap={16}> <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> <span>vCPU {selected?.status?.cpu?.capacity ?? '-'}</span>
</Flex> </Flex>
</SummaryMeta> </SummaryMeta>
@@ -88,6 +97,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
}; };
const InstanceTypeFormItem = () => { const InstanceTypeFormItem = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined; const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
@@ -132,14 +142,16 @@ const InstanceTypeFormItem = () => {
}, [form, typeName, maxGpuCount, dataList]); }, [form, typeName, maxGpuCount, dataList]);
return ( return (
<> <div data-field="instanceType">
<FieldBlock data-field="instanceType"> <FieldBlock>
<Form.Item <Form.Item
name={['spec', 'type']} name={['spec', 'type']}
rules={[ rules={[
{ {
required: true, required: true,
message: '请选择实例类型' message: intl.formatMessage({
id: 'gpuservice.instance.type.required'
})
} }
]} ]}
> >
@@ -152,13 +164,20 @@ const InstanceTypeFormItem = () => {
rules={[ rules={[
{ {
required: true, required: true,
message: '请输入 GPU 数量' message: intl.formatMessage({
id: 'gpuservice.instance.gpuCount.required'
})
}, },
{ {
validator: (_, value) => { validator: (_, value) => {
if (value > maxGpuCount) { if (value > maxGpuCount) {
return Promise.reject( return Promise.reject(
new Error(`当前实例类型最多支持 ${maxGpuCount} 个 GPU`) new Error(
intl.formatMessage(
{ id: 'gpuservice.instance.gpuCount.max' },
{ count: maxGpuCount }
)
)
); );
} }
return Promise.resolve(); return Promise.resolve();
@@ -170,12 +189,12 @@ const InstanceTypeFormItem = () => {
min={1} min={1}
max={maxGpuCount} max={maxGpuCount}
precision={0} precision={0}
label="GPU 数量" label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
required required
/> />
</Form.Item> </Form.Item>
)} )}
</> </div>
); );
}; };
@@ -1,6 +1,7 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view'; import FormOverlayView from '@/pages/_components/form-overlay-view';
import { ModalFooter } from '@gpustack/core-ui'; import { ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { FormData as StorageFormData } from '../../storage/config/types'; import { FormData as StorageFormData } from '../../storage/config/types';
import GPUServiceStorageForm from '../../storage/forms'; import GPUServiceStorageForm from '../../storage/forms';
@@ -18,6 +19,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
onCancel, onCancel,
onSubmit onSubmit
}) => { }) => {
const intl = useIntl();
const formRef = useRef<any>(null); const formRef = useRef<any>(null);
const handleSubmit = () => { const handleSubmit = () => {
@@ -43,7 +45,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
return ( return (
<FormOverlayView <FormOverlayView
title="添加存储" title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
open={open} open={open}
width={600} width={600}
onCancel={handleCancel} onCancel={handleCancel}
@@ -1,5 +1,6 @@
import { useModel } from '@@/plugin-model'; import { useModel } from '@@/plugin-model';
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui'; import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Flex, Form, Radio } from 'antd'; import { Button, Flex, Form, Radio } from 'antd';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -17,6 +18,7 @@ const FieldBlock = styled.div`
const DEFAULT_TEMP_CAPACITY_GB = 50; const DEFAULT_TEMP_CAPACITY_GB = 50;
const StorageVolume = () => { const StorageVolume = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState'); const { initialState } = useModel('@@initialState');
const { fetchData: createStorage } = useCreateStorage(); const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } = const { detailData: storageData, fetchData: fetchStorage } =
@@ -92,8 +94,16 @@ const StorageVolume = () => {
value={storageMode} value={storageMode}
onChange={(e) => handleModeChange(e.target.value)} onChange={(e) => handleModeChange(e.target.value)}
options={[ 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 <Button
@@ -102,7 +112,7 @@ const StorageVolume = () => {
style={{ marginBottom: 6 }} style={{ marginBottom: 6 }}
onClick={() => setOverlayOpen(true)} onClick={() => setOverlayOpen(true)}
> >
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button> </Button>
</Flex> </Flex>
@@ -112,11 +122,19 @@ const StorageVolume = () => {
rules={[ rules={[
{ {
required: true, 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> </Form.Item>
)} )}
@@ -132,11 +150,20 @@ const StorageVolume = () => {
rules={[ rules={[
{ {
required: true, 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> </Form.Item>
)} )}
@@ -1,4 +1,5 @@
import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui'; import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import styled from 'styled-components'; import styled from 'styled-components';
import { ListItem as TemplateItem } from '../../templates/config/types'; import { ListItem as TemplateItem } from '../../templates/config/types';
@@ -51,6 +52,7 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
onChange, onChange,
dataList = [] dataList = []
}) => { }) => {
const intl = useIntl();
return ( return (
<TemplateGrid> <TemplateGrid>
{dataList.map((item: TemplateItem) => ( {dataList.map((item: TemplateItem) => (
@@ -71,7 +73,11 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
</div> </div>
<div className="info"> <div className="info">
<span> <span>
<IconFont className="icon" type="icon-model" /> : <IconFont className="icon" type="icon-model" />{' '}
{intl.formatMessage({
id: 'gpuservice.instance.template.image'
})}
:
</span> </span>
<AutoTooltip ghost minWidth={20}> <AutoTooltip ghost minWidth={20}>
<span className="value">{item.spec?.image || '-'}</span> <span className="value">{item.spec?.image || '-'}</span>
@@ -79,7 +85,11 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
</div> </div>
<div className="info"> <div className="info">
<span> <span>
<IconFont className="icon" type="icon-storage-outlined" /> : <IconFont className="icon" type="icon-storage-outlined" />{' '}
{intl.formatMessage({
id: 'gpuservice.instance.template.mount'
})}
:
</span> </span>
<span className="value">{item.spec?.volumeMount || '-'}</span> <span className="value">{item.spec?.volumeMount || '-'}</span>
</div> </div>
@@ -1,5 +1,6 @@
import { tableSorter } from '@/config/settings'; import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui'; import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table'; import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -15,10 +16,11 @@ const useInstancesColumns = ({
handleSelect, handleSelect,
sortOrder sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => { }: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => { return useMemo(() => {
return [ return [
{ {
title: '名称', title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'], dataIndex: ['metadata', 'name'],
key: 'name', key: 'name',
sorter: tableSorter(1), sorter: tableSorter(1),
@@ -32,7 +34,7 @@ const useInstancesColumns = ({
) )
}, },
{ {
title: '状态', title: intl.formatMessage({ id: 'common.table.status' }),
dataIndex: ['status', 'phase'], dataIndex: ['status', 'phase'],
key: 'status', key: 'status',
sorter: tableSorter(2), sorter: tableSorter(2),
@@ -48,7 +50,7 @@ const useInstancesColumns = ({
} }
}, },
{ {
title: '镜像', title: intl.formatMessage({ id: 'gpuservice.template.image' }),
dataIndex: ['spec', 'image'], dataIndex: ['spec', 'image'],
key: 'image', key: 'image',
sorter: tableSorter(3), sorter: tableSorter(3),
@@ -60,7 +62,7 @@ const useInstancesColumns = ({
) )
}, },
{ {
title: '创建时间', title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'], dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp', key: 'creationTimestamp',
sorter: tableSorter(5), sorter: tableSorter(5),
@@ -74,7 +76,7 @@ const useInstancesColumns = ({
) )
}, },
{ {
title: '操作', title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation', key: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
render: (_text, record) => ( render: (_text, record) => (
@@ -85,7 +87,7 @@ const useInstancesColumns = ({
) )
} }
]; ];
}, [handleSelect, sortOrder]); }, [handleSelect, sortOrder, intl]);
}; };
export default useInstancesColumns; 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'; import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => { const GPUService: React.FC = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState'); const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default'; const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom); const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
@@ -97,7 +98,7 @@ const GPUService: React.FC = () => {
deleteAPI: deleteInstance, deleteAPI: deleteInstance,
watch: true, watch: true,
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }), API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
contentForDelete: 'GPU 实例' contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
}); });
const { fetchData: createInstance } = useCreateInstance(); const { fetchData: createInstance } = useCreateInstance();
@@ -107,7 +108,6 @@ const GPUService: React.FC = () => {
cancelRequest: cancelClusterRequest, cancelRequest: cancelClusterRequest,
clusterList clusterList
} = useQueryClusterList(); } = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo( const k8sClusterList = useMemo(
() => () =>
@@ -155,7 +155,7 @@ const GPUService: React.FC = () => {
const handleAddInstance = () => { const handleAddInstance = () => {
setOpenAddModalStatus({ setOpenAddModalStatus({
action: PageAction.CREATE, action: PageAction.CREATE,
title: '添加 GPU 实例', title: intl.formatMessage({ id: 'gpuservice.instance.add' }),
open: true, open: true,
currentData: null currentData: null
}); });
@@ -164,7 +164,7 @@ const GPUService: React.FC = () => {
const handleEditInstance = (row: ListItem) => { const handleEditInstance = (row: ListItem) => {
setOpenAddModalStatus({ setOpenAddModalStatus({
action: PageAction.EDIT, action: PageAction.EDIT,
title: '编辑 GPU 实例', title: intl.formatMessage({ id: 'gpuservice.instance.edit' }),
open: true, open: true,
currentData: row currentData: row
}); });
@@ -192,9 +192,9 @@ const GPUService: React.FC = () => {
fetchData(); fetchData();
closeModal(); closeModal();
message.success('操作成功'); message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) { } catch (error) {
message.error('操作失败'); message.error(intl.formatMessage({ id: 'common.message.fail' }));
} }
}; };
@@ -224,13 +224,19 @@ const GPUService: React.FC = () => {
loading={dataSource.loading} loading={dataSource.loading}
loadend={dataSource.loadend} loadend={dataSource.loadend}
dataSource={dataSource.dataList} dataSource={dataSource.dataList}
image={<IconFont type="icon-instances-outlined" />} image={<IconFont type="icon-cloud-outlined" />}
filters={_.omit(queryParams, ['sort_by'])} filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的 GPU 实例" noFoundText={intl.formatMessage({
title="暂无 GPU 实例" id: 'noresult.gpuservice.instance.nofound'
subTitle="创建一个 GPU 实例后会显示在这里" })}
title={intl.formatMessage({
id: 'noresult.gpuservice.instance.title'
})}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.instance.subTitle'
})}
onClick={handleAddInstance} onClick={handleAddInstance}
buttonText="立即添加" buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/> />
); );
}; };
@@ -267,8 +273,10 @@ const GPUService: React.FC = () => {
showSelect={false} showSelect={false}
selectOptions={k8sClusterList} selectOptions={k8sClusterList}
select={{ showSearch: true }} select={{ showSearch: true }}
selectHolder="按集群过滤" selectHolder={intl.formatMessage({
buttonText="添加 GPU 实例" id: 'gpuservice.instance.filter.cluster'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })}
handleSearch={handleSearch} handleSearch={handleSearch}
handleSelectChange={handleClusterChange} handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
+13 -8
View File
@@ -7,23 +7,26 @@ export const StorageTypeValueMap = {
}; };
export const StorageTypeLabelMap: Record<string, string> = { export const StorageTypeLabelMap: Record<string, string> = {
[StorageTypeValueMap.Local]: '本地存储', [StorageTypeValueMap.Local]: 'gpuservice.storage.type.local',
[StorageTypeValueMap.Shared]: '共享存储', [StorageTypeValueMap.Shared]: 'gpuservice.storage.type.shared',
[StorageTypeValueMap.Object]: '对象存储' [StorageTypeValueMap.Object]: 'gpuservice.storage.type.object'
}; };
export const StorageTypeOptions = [ export const StorageTypeOptions = [
{ {
label: StorageTypeLabelMap[StorageTypeValueMap.Local], label: StorageTypeLabelMap[StorageTypeValueMap.Local],
value: StorageTypeValueMap.Local value: StorageTypeValueMap.Local,
locale: true
}, },
{ {
label: StorageTypeLabelMap[StorageTypeValueMap.Shared], label: StorageTypeLabelMap[StorageTypeValueMap.Shared],
value: StorageTypeValueMap.Shared value: StorageTypeValueMap.Shared,
locale: true
}, },
{ {
label: StorageTypeLabelMap[StorageTypeValueMap.Object], label: StorageTypeLabelMap[StorageTypeValueMap.Object],
value: StorageTypeValueMap.Object value: StorageTypeValueMap.Object,
locale: true
} }
]; ];
@@ -76,13 +79,15 @@ export const ReClaimPolicyOptions = [
export const rowActionList = [ export const rowActionList = [
{ {
label: '编辑', label: 'common.button.edit',
key: 'edit', key: 'edit',
locale: true,
icon: icons.EditOutlined icon: icons.EditOutlined
}, },
{ {
label: '删除', label: 'common.button.delete',
key: 'delete', key: 'delete',
locale: true,
icon: icons.DeleteOutlined, icon: icons.DeleteOutlined,
props: { props: {
danger: true danger: true
+23 -6
View File
@@ -4,12 +4,14 @@ import {
Select as SealSelect, Select as SealSelect,
useAppUtils useAppUtils
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form } from 'antd'; import { Flex, Form } from 'antd';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import useQueryStorageClass from '../services/use-query-storage-class'; import useQueryStorageClass from '../services/use-query-storage-class';
const Basic = ({ open }: { open: boolean }) => { const Basic = ({ open }: { open: boolean }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const { storageClassList, fetchData, loading } = useQueryStorageClass(); const { storageClassList, fetchData, loading } = useQueryStorageClass();
@@ -26,11 +28,17 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[ rules={[
{ {
required: true, 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> </Form.Item>
<Flex gap={16}> <Flex gap={16}>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
@@ -39,12 +47,15 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[ rules={[
{ {
required: true, required: true,
message: getRuleMessage('select', '存储类型') message: getRuleMessage(
'select',
intl.formatMessage({ id: 'gpuservice.storage.type' })
)
} }
]} ]}
> >
<SealSelect <SealSelect
label="存储类型" label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
required required
loading={loading} loading={loading}
options={storageClassList} options={storageClassList}
@@ -61,11 +72,17 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[ rules={[
{ {
required: true, 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> </Form.Item>
</div> </div>
</Flex> </Flex>
@@ -1,5 +1,6 @@
import { tableSorter } from '@/config/settings'; import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui'; import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table'; import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -15,10 +16,11 @@ const useStorageColumns = ({
handleSelect, handleSelect,
sortOrder sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => { }: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => { return useMemo(() => {
return [ return [
{ {
title: '名称', title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'], dataIndex: ['metadata', 'name'],
key: 'name', key: 'name',
sorter: tableSorter(1), sorter: tableSorter(1),
@@ -32,28 +34,31 @@ const useStorageColumns = ({
) )
}, },
{ {
title: '类型', title: intl.formatMessage({ id: 'common.table.type' }),
dataIndex: ['spec', 'type'], dataIndex: ['spec', 'type'],
key: 'type', key: 'type',
sorter: tableSorter(2), 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'], dataIndex: ['spec', 'capacity'],
key: 'capacity', key: 'capacity',
sorter: tableSorter(3), sorter: tableSorter(3),
render: (value: string) => value ?? '-' render: (value: string) => value ?? '-'
}, },
{ {
title: '访问模式', title: intl.formatMessage({ id: 'gpuservice.storage.accessMode' }),
dataIndex: ['spec', 'accessMode'], dataIndex: ['spec', 'accessMode'],
key: 'accessMode', key: 'accessMode',
sorter: tableSorter(4), sorter: tableSorter(4),
render: (value: string) => value || '-' render: (value: string) => value || '-'
}, },
{ {
title: '创建时间', title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'], dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp', key: 'creationTimestamp',
sorter: tableSorter(5), sorter: tableSorter(5),
@@ -67,7 +72,7 @@ const useStorageColumns = ({
) )
}, },
{ {
title: '操作', title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation', key: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
render: (_text, record) => ( render: (_text, record) => (
@@ -78,7 +83,7 @@ const useStorageColumns = ({
) )
} }
]; ];
}, [handleSelect, sortOrder]); }, [handleSelect, sortOrder, intl]);
}; };
export default useStorageColumns; 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'; import useUpdateStorage from './services/use-update-storage';
const GPUServiceStorage: React.FC = () => { const GPUServiceStorage: React.FC = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState'); const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default'; const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom); const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
@@ -98,7 +99,7 @@ const GPUServiceStorage: React.FC = () => {
deleteAPI: deleteStorage, deleteAPI: deleteStorage,
watch: false, watch: false,
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }), API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
contentForDelete: '存储' contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
}); });
const { fetchData: createStorage } = useCreateStorage(); const { fetchData: createStorage } = useCreateStorage();
@@ -108,7 +109,6 @@ const GPUServiceStorage: React.FC = () => {
cancelRequest: cancelClusterRequest, cancelRequest: cancelClusterRequest,
clusterList clusterList
} = useQueryClusterList(); } = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo( const k8sClusterList = useMemo(
() => () =>
@@ -156,7 +156,7 @@ const GPUServiceStorage: React.FC = () => {
const handleAddStorage = () => { const handleAddStorage = () => {
setOpenAddModalStatus({ setOpenAddModalStatus({
action: PageAction.CREATE, action: PageAction.CREATE,
title: '添加存储', title: intl.formatMessage({ id: 'gpuservice.storage.add' }),
open: true, open: true,
currentData: null currentData: null
}); });
@@ -165,7 +165,7 @@ const GPUServiceStorage: React.FC = () => {
const handleEditStorage = (row: ListItem) => { const handleEditStorage = (row: ListItem) => {
setOpenAddModalStatus({ setOpenAddModalStatus({
action: PageAction.EDIT, action: PageAction.EDIT,
title: '编辑存储', title: intl.formatMessage({ id: 'gpuservice.storage.edit' }),
open: true, open: true,
currentData: row currentData: row
}); });
@@ -193,9 +193,9 @@ const GPUServiceStorage: React.FC = () => {
fetchData(); fetchData();
closeModal(); closeModal();
message.success('操作成功'); message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) { } catch (error) {
message.error('操作失败'); message.error(intl.formatMessage({ id: 'common.message.fail' }));
} }
}; };
@@ -227,11 +227,15 @@ const GPUServiceStorage: React.FC = () => {
dataSource={dataSource.dataList} dataSource={dataSource.dataList}
image={<IconFont type="icon-storage-outlined" />} image={<IconFont type="icon-storage-outlined" />}
filters={_.omit(queryParams, ['sort_by'])} filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的存储" noFoundText={intl.formatMessage({
title="暂无存储" id: 'noresult.gpuservice.storage.nofound'
subTitle="创建一个存储后会显示在这里" })}
title={intl.formatMessage({ id: 'noresult.gpuservice.storage.title' })}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.storage.subTitle'
})}
onClick={handleAddStorage} onClick={handleAddStorage}
buttonText="立即添加" buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/> />
); );
}; };
@@ -269,8 +273,10 @@ const GPUServiceStorage: React.FC = () => {
showSelect={false} showSelect={false}
selectOptions={k8sClusterList} selectOptions={k8sClusterList}
select={{ showSearch: true }} select={{ showSearch: true }}
selectHolder="按集群过滤" selectHolder={intl.formatMessage({
buttonText="添加存储" id: 'gpuservice.storage.filter.cluster'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.storage.add' })}
handleSearch={handleSearch} handleSearch={handleSearch}
handleSelectChange={handleClusterChange} handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
@@ -34,6 +34,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
return ( return (
<> <>
<div data-field="template"></div>
{page === 'template' && ( {page === 'template' && (
<> <>
<Form.Item<FormData> <Form.Item<FormData>