fix: ux issues
This commit is contained in:
@@ -186,6 +186,7 @@ const baseRoutes = [
|
|||||||
name: 'gpuService',
|
name: 'gpuService',
|
||||||
path: '/gpu-service',
|
path: '/gpu-service',
|
||||||
key: 'gpuService',
|
key: 'gpuService',
|
||||||
|
access: 'canSeeAdmin',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/gpu-service',
|
path: '/gpu-service',
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export default {
|
|||||||
'gpuservice.storageType.kind': 'Type',
|
'gpuservice.storageType.kind': 'Type',
|
||||||
'gpuservice.storageType.mountOptions': 'Mount Options',
|
'gpuservice.storageType.mountOptions': 'Mount Options',
|
||||||
'gpuservice.storageType.nfs.server': 'NFS Server',
|
'gpuservice.storageType.nfs.server': 'NFS Server',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'Ensure the NFS server address is reachable from all Kubernetes clusters.',
|
||||||
'gpuservice.storageType.nfs.share': 'Share Path',
|
'gpuservice.storageType.nfs.share': 'Share Path',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'A directory based on the organization and storage names will be automatically created within this share path. If a subdirectory is specified, the generated directory will be created under that subdirectory.',
|
||||||
'gpuservice.storageType.nfs.subDirectory': 'Sub Directory',
|
'gpuservice.storageType.nfs.subDirectory': 'Sub Directory',
|
||||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
'If empty, a subdirectory named after the persistent volume will be created. If set, a directory with the persistent volume name will be created beneath this subdirectory.',
|
'If empty, a subdirectory named after the persistent volume will be created. If set, a directory with the persistent volume name will be created beneath this subdirectory.',
|
||||||
@@ -54,11 +58,17 @@ export default {
|
|||||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
'Inherit the file permissions from the NFS server.',
|
'Inherit the file permissions from the NFS server.',
|
||||||
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'Ensure the S3 endpoint is reachable from all Kubernetes clusters.',
|
||||||
'gpuservice.storageType.s3.endpoint.rule': 'Must start with http or https',
|
'gpuservice.storageType.s3.endpoint.rule': 'Must start with http or https',
|
||||||
'gpuservice.storageType.s3.region': 'Region',
|
'gpuservice.storageType.s3.region': 'Region',
|
||||||
'gpuservice.storageType.s3.bucket': 'Bucket',
|
'gpuservice.storageType.s3.bucket': 'Bucket',
|
||||||
'gpuservice.storageType.s3.bucket.tips':
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
'If empty, a new bucket named after the persistent volume will be created. If set, a subdirectory with the persistent volume name will be created inside this bucket.',
|
'If empty, a new bucket named after the persistent volume will be created. If set, a subdirectory with the persistent volume name will be created inside this bucket.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'A prefix based on the organization and storage names will be automatically created within this bucket.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'For example, if the organization is named <span class="desc-block">awesome-group</span> and the storage is named <span class="desc-block">storage-1</span>, the resulting prefix will be <span class="desc-block">awesome-group/storage-1</span>.',
|
||||||
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
'gpuservice.storageType.s3.insecure': 'Skip TLS/SSL certificate verification',
|
'gpuservice.storageType.s3.insecure': 'Skip TLS/SSL certificate verification',
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ export default {
|
|||||||
'gpuservice.storageType.kind': '種別',
|
'gpuservice.storageType.kind': '種別',
|
||||||
'gpuservice.storageType.mountOptions': 'マウントオプション',
|
'gpuservice.storageType.mountOptions': 'マウントオプション',
|
||||||
'gpuservice.storageType.nfs.server': 'NFS サーバー',
|
'gpuservice.storageType.nfs.server': 'NFS サーバー',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'すべての Kubernetes クラスターから NFS サーバーアドレスにアクセスできることを確認してください。',
|
||||||
'gpuservice.storageType.nfs.share': '共有パス',
|
'gpuservice.storageType.nfs.share': '共有パス',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'この共有パス配下に、組織名とストレージ名に基づくディレクトリが自動的に作成されます。サブディレクトリが指定されている場合、生成されたディレクトリはそのサブディレクトリ配下に作成されます。',
|
||||||
'gpuservice.storageType.nfs.subDirectory': 'サブディレクトリ',
|
'gpuservice.storageType.nfs.subDirectory': 'サブディレクトリ',
|
||||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
'空の場合、永続ボリューム名のサブディレクトリが作成されます。設定されている場合、このサブディレクトリ配下に永続ボリューム名のディレクトリが作成されます。',
|
'空の場合、永続ボリューム名のサブディレクトリが作成されます。設定されている場合、このサブディレクトリ配下に永続ボリューム名のディレクトリが作成されます。',
|
||||||
@@ -52,12 +56,18 @@ export default {
|
|||||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
'NFS サーバー上のファイル権限を継承します。',
|
'NFS サーバー上のファイル権限を継承します。',
|
||||||
'gpuservice.storageType.s3.endpoint': 'エンドポイント',
|
'gpuservice.storageType.s3.endpoint': 'エンドポイント',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'すべての Kubernetes クラスターから S3 エンドポイントにアクセスできることを確認してください。',
|
||||||
'gpuservice.storageType.s3.endpoint.rule':
|
'gpuservice.storageType.s3.endpoint.rule':
|
||||||
'http または https で始まる必要があります',
|
'http または https で始まる必要があります',
|
||||||
'gpuservice.storageType.s3.region': 'リージョン',
|
'gpuservice.storageType.s3.region': 'リージョン',
|
||||||
'gpuservice.storageType.s3.bucket': 'バケット',
|
'gpuservice.storageType.s3.bucket': 'バケット',
|
||||||
'gpuservice.storageType.s3.bucket.tips':
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
'空の場合、永続ボリューム名で新しいバケットが作成されます。設定されている場合、このバケット配下に永続ボリューム名のサブディレクトリが作成されます。',
|
'空の場合、永続ボリューム名で新しいバケットが作成されます。設定されている場合、このバケット配下に永続ボリューム名のサブディレクトリが作成されます。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'このバケット内に、組織名とストレージ名に基づくプレフィックスディレクトリが自動的に作成されます。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'例えば、組織名が <span class="desc-block">awesome-group</span>、ストレージ名が <span class="desc-block">storage-1</span> の場合、生成されるプレフィックスは <span class="desc-block">awesome-group/storage-1</span> になります。',
|
||||||
'gpuservice.storageType.s3.accessKey': 'アクセスキー',
|
'gpuservice.storageType.s3.accessKey': 'アクセスキー',
|
||||||
'gpuservice.storageType.s3.secretKey': 'シークレットキー',
|
'gpuservice.storageType.s3.secretKey': 'シークレットキー',
|
||||||
'gpuservice.storageType.s3.insecure': 'TLS/SSL 証明書の検証をスキップ',
|
'gpuservice.storageType.s3.insecure': 'TLS/SSL 証明書の検証をスキップ',
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ export default {
|
|||||||
'gpuservice.storageType.kind': 'Тип',
|
'gpuservice.storageType.kind': 'Тип',
|
||||||
'gpuservice.storageType.mountOptions': 'Параметры монтирования',
|
'gpuservice.storageType.mountOptions': 'Параметры монтирования',
|
||||||
'gpuservice.storageType.nfs.server': 'Сервер NFS',
|
'gpuservice.storageType.nfs.server': 'Сервер NFS',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'Убедитесь, что адрес NFS-сервера доступен из всех кластеров Kubernetes.',
|
||||||
'gpuservice.storageType.nfs.share': 'Путь общего ресурса',
|
'gpuservice.storageType.nfs.share': 'Путь общего ресурса',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'В этом общем пути будет автоматически создан каталог на основе названия организации и названия хранилища. Если указан подкаталог, итоговый каталог будет создан внутри него.',
|
||||||
'gpuservice.storageType.nfs.subDirectory': 'Подкаталог',
|
'gpuservice.storageType.nfs.subDirectory': 'Подкаталог',
|
||||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
'Если поле пустое, будет создан подкаталог с именем постоянного тома. Если задано, под этим подкаталогом будет создан каталог с именем постоянного тома.',
|
'Если поле пустое, будет создан подкаталог с именем постоянного тома. Если задано, под этим подкаталогом будет создан каталог с именем постоянного тома.',
|
||||||
@@ -55,15 +59,22 @@ export default {
|
|||||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
'Наследует права файлов с NFS-сервера.',
|
'Наследует права файлов с NFS-сервера.',
|
||||||
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'Убедитесь, что S3 endpoint доступен из всех кластеров Kubernetes.',
|
||||||
'gpuservice.storageType.s3.endpoint.rule':
|
'gpuservice.storageType.s3.endpoint.rule':
|
||||||
'Должен начинаться с http или https',
|
'Должен начинаться с http или https',
|
||||||
'gpuservice.storageType.s3.region': 'Регион',
|
'gpuservice.storageType.s3.region': 'Регион',
|
||||||
'gpuservice.storageType.s3.bucket': 'Бакет',
|
'gpuservice.storageType.s3.bucket': 'Бакет',
|
||||||
'gpuservice.storageType.s3.bucket.tips':
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
'Если поле пустое, будет создан новый бакет с именем постоянного тома. Если задано, в этом бакете будет создан подкаталог с именем постоянного тома.',
|
'Если поле пустое, будет создан новый бакет с именем постоянного тома. Если задано, в этом бакете будет создан подкаталог с именем постоянного тома.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'В этом бакете будет автоматически создан префикс на основе названия организации и названия хранилища.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'Например, если организация называется <span class="desc-block">awesome-group</span>, а хранилище — <span class="desc-block">storage-1</span>, итоговый префикс будет: <span class="desc-block">awesome-group/storage-1</span>.',
|
||||||
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
'gpuservice.storageType.s3.insecure': 'Пропустить проверку сертификата TLS/SSL',
|
'gpuservice.storageType.s3.insecure':
|
||||||
|
'Пропустить проверку сертификата TLS/SSL',
|
||||||
'gpuservice.storageType.s3.insecure.tips':
|
'gpuservice.storageType.s3.insecure.tips':
|
||||||
'Если включено, сертификат сервера S3 не проверяется. Подходит для внутреннего тестирования или самоподписанных сертификатов; в производственной среде включайте с осторожностью.',
|
'Если включено, сертификат сервера S3 не проверяется. Подходит для внутреннего тестирования или самоподписанных сертификатов; в производственной среде включайте с осторожностью.',
|
||||||
'gpuservice.publicKey': 'Открытый ключ SSH',
|
'gpuservice.publicKey': 'Открытый ключ SSH',
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export default {
|
|||||||
'gpuservice.storageType.kind': 'Tür',
|
'gpuservice.storageType.kind': 'Tür',
|
||||||
'gpuservice.storageType.mountOptions': 'Bağlama Seçenekleri',
|
'gpuservice.storageType.mountOptions': 'Bağlama Seçenekleri',
|
||||||
'gpuservice.storageType.nfs.server': 'NFS Sunucusu',
|
'gpuservice.storageType.nfs.server': 'NFS Sunucusu',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'NFS sunucu adresinin tüm Kubernetes kümelerinden erişilebilir olduğundan emin olun.',
|
||||||
'gpuservice.storageType.nfs.share': 'Paylaşım Yolu',
|
'gpuservice.storageType.nfs.share': 'Paylaşım Yolu',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'Bu paylaşım yolu altında organizasyon ve depolama adlarına dayalı bir dizin otomatik olarak oluşturulur. Bir alt dizin belirtilmişse, oluşturulan dizin o alt dizin altında yer alır.',
|
||||||
'gpuservice.storageType.nfs.subDirectory': 'Alt Dizin',
|
'gpuservice.storageType.nfs.subDirectory': 'Alt Dizin',
|
||||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
'Boş bırakılırsa kalıcı hacim adıyla bir alt dizin oluşturulur. Doldurulursa bu alt dizinin altında kalıcı hacim adıyla bir dizin oluşturulur.',
|
'Boş bırakılırsa kalıcı hacim adıyla bir alt dizin oluşturulur. Doldurulursa bu alt dizinin altında kalıcı hacim adıyla bir dizin oluşturulur.',
|
||||||
@@ -54,12 +58,17 @@ export default {
|
|||||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
'NFS sunucusundaki dosya izinleri devralınır.',
|
'NFS sunucusundaki dosya izinleri devralınır.',
|
||||||
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
'gpuservice.storageType.s3.endpoint.rule':
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
'http veya https ile başlamalıdır',
|
'S3 endpoint adresinin tüm Kubernetes kümelerinden erişilebilir olduğundan emin olun.',
|
||||||
|
'gpuservice.storageType.s3.endpoint.rule': 'http veya https ile başlamalıdır',
|
||||||
'gpuservice.storageType.s3.region': 'Bölge',
|
'gpuservice.storageType.s3.region': 'Bölge',
|
||||||
'gpuservice.storageType.s3.bucket': 'Kova',
|
'gpuservice.storageType.s3.bucket': 'Kova',
|
||||||
'gpuservice.storageType.s3.bucket.tips':
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
'Boş bırakılırsa kalıcı hacim adıyla yeni bir kova oluşturulur. Doldurulursa bu kova içinde kalıcı hacim adıyla bir alt dizin oluşturulur.',
|
'Boş bırakılırsa kalıcı hacim adıyla yeni bir kova oluşturulur. Doldurulursa bu kova içinde kalıcı hacim adıyla bir alt dizin oluşturulur.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'Bu kova içinde organizasyon ve depolama adlarına dayalı bir önek dizini otomatik olarak oluşturulur.',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'Örneğin, organizasyon adı <span class="desc-block">awesome-group</span> ve depolama adı <span class="desc-block">storage-1</span> ise, oluşacak önek: <span class="desc-block">awesome-group/storage-1</span>.',
|
||||||
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
'gpuservice.storageType.s3.insecure': 'TLS/SSL sertifika doğrulamasını atla',
|
'gpuservice.storageType.s3.insecure': 'TLS/SSL sertifika doğrulamasını atla',
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ export default {
|
|||||||
'gpuservice.storageType.kind': '类型',
|
'gpuservice.storageType.kind': '类型',
|
||||||
'gpuservice.storageType.mountOptions': '挂载参数',
|
'gpuservice.storageType.mountOptions': '挂载参数',
|
||||||
'gpuservice.storageType.nfs.server': 'NFS 服务器',
|
'gpuservice.storageType.nfs.server': 'NFS 服务器',
|
||||||
|
'gpuservice.storageType.nfs.server.tips':
|
||||||
|
'确保所有 Kubernetes 集群都能访问该 NFS 服务地址。',
|
||||||
'gpuservice.storageType.nfs.share': '共享路径',
|
'gpuservice.storageType.nfs.share': '共享路径',
|
||||||
|
'gpuservice.storageType.nfs.share.tips':
|
||||||
|
'系统会在该共享路径下自动创建一个基于组织名称和存储名称的目录。如果指定了子目录,则生成的目录会创建在该子目录下。',
|
||||||
'gpuservice.storageType.nfs.subDirectory': '子目录',
|
'gpuservice.storageType.nfs.subDirectory': '子目录',
|
||||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||||
'如果为空,会以持久卷的卷名创建一个子目录;如果不为空,会在同名子目录下,以持久卷的卷名创建一个孙目录。',
|
'如果为空,会以持久卷的卷名创建一个子目录;如果不为空,会在同名子目录下,以持久卷的卷名创建一个孙目录。',
|
||||||
@@ -49,11 +53,17 @@ export default {
|
|||||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||||
'沿用 NFS 服务器文件的权限。',
|
'沿用 NFS 服务器文件的权限。',
|
||||||
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||||
|
'gpuservice.storageType.s3.endpoint.tips':
|
||||||
|
'确保所有 Kubernetes 集群都能访问该 S3 Endpoint。',
|
||||||
'gpuservice.storageType.s3.endpoint.rule': '必须以 http 或 https 开头',
|
'gpuservice.storageType.s3.endpoint.rule': '必须以 http 或 https 开头',
|
||||||
'gpuservice.storageType.s3.region': '区域',
|
'gpuservice.storageType.s3.region': '区域',
|
||||||
'gpuservice.storageType.s3.bucket': '存储桶',
|
'gpuservice.storageType.s3.bucket': '存储桶',
|
||||||
'gpuservice.storageType.s3.bucket.tips':
|
'gpuservice.storageType.s3.bucket.tips':
|
||||||
'如果为空,会以持久卷的卷名创建一个新桶;如果不为空,会在同名桶下,以持久卷的卷名创建一个子目录。',
|
'如果为空,会以持久卷的卷名创建一个新桶;如果不为空,会在同名桶下,以持久卷的卷名创建一个子目录。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips1':
|
||||||
|
'系统会在该 Bucket 中自动创建一个基于组织名称和存储名称的前缀目录。',
|
||||||
|
'gpuservice.storageType.s3.bucket.tips2':
|
||||||
|
'例如,若组织名称为 <span class="desc-block">awesome-group</span>,存储名称为 <span class="desc-block">storage-1</span>,则生成的前缀为:<span class="desc-block">awesome-group/storage-1</span>。',
|
||||||
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
'gpuservice.storageType.s3.accessKey': 'Access Key',
|
||||||
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
'gpuservice.storageType.s3.secretKey': 'Secret Key',
|
||||||
'gpuservice.storageType.s3.insecure': '跳过 TLS/SSL 证书验证',
|
'gpuservice.storageType.s3.insecure': '跳过 TLS/SSL 证书验证',
|
||||||
|
|||||||
@@ -31,13 +31,6 @@ type AddModalProps = {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type InstanceTypeSelection = {
|
|
||||||
instanceType?: string;
|
|
||||||
manufacturer?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMPTY_INSTANCE_TYPE_SELECTION: InstanceTypeSelection = {};
|
|
||||||
|
|
||||||
const matchKeyword = (fields: Array<unknown>, keyword: string) => {
|
const matchKeyword = (fields: Array<unknown>, keyword: string) => {
|
||||||
const trimmed = keyword.trim().toLowerCase();
|
const trimmed = keyword.trim().toLowerCase();
|
||||||
if (!trimmed) return true;
|
if (!trimmed) return true;
|
||||||
@@ -85,11 +78,17 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = useRef<any>(null);
|
const form = useRef<any>(null);
|
||||||
const sessionRef = useRef(0);
|
const sessionRef = useRef(0);
|
||||||
const [instanceTypeSelection, setInstanceTypeSelection] =
|
const [instanceTypeSelection, setInstanceTypeSelection] = useState<{
|
||||||
useState<InstanceTypeSelection>(EMPTY_INSTANCE_TYPE_SELECTION);
|
instanceType?: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
}>({
|
||||||
|
instanceType: undefined,
|
||||||
|
manufacturer: undefined
|
||||||
|
});
|
||||||
const [templateId, setTemplateId] = useState<number | undefined>();
|
const [templateId, setTemplateId] = useState<number | undefined>();
|
||||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
detailData,
|
detailData,
|
||||||
@@ -116,6 +115,20 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveInstanceDataInDescription = (instanceType: InstanceTypeItem) => {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
name: instanceType.name,
|
||||||
|
spec: {
|
||||||
|
...instanceType.spec
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// apply the selection of instance type and template
|
||||||
const applySelection = (
|
const applySelection = (
|
||||||
instanceType: InstanceTypeItem,
|
instanceType: InstanceTypeItem,
|
||||||
template: TemplateItem | undefined
|
template: TemplateItem | undefined
|
||||||
@@ -126,37 +139,29 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
instanceType: instanceType.name,
|
instanceType: instanceType.name,
|
||||||
manufacturer
|
manufacturer
|
||||||
});
|
});
|
||||||
|
|
||||||
setTemplateId(template?.id);
|
setTemplateId(template?.id);
|
||||||
|
|
||||||
if (template) {
|
if (template) {
|
||||||
const currentSpec = form.current?.getFieldsValue()?.spec || {};
|
const formValues = form.current?.getFieldsValue();
|
||||||
form.current?.setFieldsValue({
|
form.current?.setFieldsValue({
|
||||||
manufacturer: template.manufacturer,
|
description: saveInstanceDataInDescription(instanceType),
|
||||||
description: JSON.stringify(
|
|
||||||
{
|
|
||||||
name: instanceType.name,
|
|
||||||
spec: {
|
|
||||||
...instanceType.spec
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
),
|
|
||||||
spec: {
|
spec: {
|
||||||
...currentSpec,
|
...formValues?.spec,
|
||||||
...template.spec,
|
...template.spec,
|
||||||
resources: {
|
sshPublicKeys: formValues?.spec?.sshPublicKeys,
|
||||||
...(currentSpec.resources || {}),
|
volume: {
|
||||||
...(template.spec?.resources || {})
|
...formValues?.spec?.volume
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.current?.setFieldsValue({
|
form.current?.setFieldsValue({
|
||||||
manufacturer: undefined,
|
description: saveInstanceDataInDescription(instanceType)
|
||||||
description: JSON.stringify(instanceType.spec, null, 2)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update form
|
||||||
form.current?.applyInstanceType?.(instanceType);
|
form.current?.applyInstanceType?.(instanceType);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,6 +180,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// initial
|
||||||
const applyAutoSelection = (
|
const applyAutoSelection = (
|
||||||
instanceTypes: InstanceTypeItem[],
|
instanceTypes: InstanceTypeItem[],
|
||||||
templates: TemplateItem[]
|
templates: TemplateItem[]
|
||||||
@@ -208,7 +214,10 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
sessionRef.current += 1;
|
sessionRef.current += 1;
|
||||||
setInstanceTypeSelection(EMPTY_INSTANCE_TYPE_SELECTION);
|
setInstanceTypeSelection({
|
||||||
|
instanceType: undefined,
|
||||||
|
manufacturer: undefined
|
||||||
|
});
|
||||||
setTemplateId(undefined);
|
setTemplateId(undefined);
|
||||||
setInstanceKeyword('');
|
setInstanceKeyword('');
|
||||||
setTemplateKeyword('');
|
setTemplateKeyword('');
|
||||||
@@ -240,6 +249,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchKeyword(
|
return matchKeyword(
|
||||||
[item.name, item.spec?.image, item.spec?.volumeMount],
|
[item.name, item.spec?.image, item.spec?.volumeMount],
|
||||||
templateKeyword
|
templateKeyword
|
||||||
@@ -256,9 +266,14 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = async (values: FormData) => {
|
const onFinish = async (values: FormData) => {
|
||||||
onOk({
|
setLoading(true);
|
||||||
...values
|
try {
|
||||||
});
|
await onOk({
|
||||||
|
...values
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
|
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
|
||||||
@@ -271,10 +286,19 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
|
|
||||||
const handleTemplateChange = (id: number, item: TemplateItem) => {
|
const handleTemplateChange = (id: number, item: TemplateItem) => {
|
||||||
setTemplateId(id);
|
setTemplateId(id);
|
||||||
|
const formValues = form.current?.getFieldsValue();
|
||||||
form.current?.setFieldsValue({
|
form.current?.setFieldsValue({
|
||||||
spec: {
|
spec: {
|
||||||
...form.current?.getFieldsValue()?.spec,
|
...formValues?.spec,
|
||||||
...item.spec
|
...item.spec,
|
||||||
|
sshPublicKeys: formValues?.spec?.sshPublicKeys,
|
||||||
|
resources: {
|
||||||
|
...item?.spec?.resources,
|
||||||
|
accelerator: formValues?.spec?.resources?.accelerator
|
||||||
|
},
|
||||||
|
volume: {
|
||||||
|
...formValues?.spec?.volume
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -400,6 +424,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
onOk={handleSubmit}
|
onOk={handleSubmit}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
showOkBtn={!readonly}
|
showOkBtn={!readonly}
|
||||||
|
loading={loading}
|
||||||
style={{
|
style={{
|
||||||
padding: '16px 24px 8px',
|
padding: '16px 24px 8px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
|||||||
loading
|
loading
|
||||||
}) => {
|
}) => {
|
||||||
const handleSelect = (item: InstanceTypeItemModel) => {
|
const handleSelect = (item: InstanceTypeItemModel) => {
|
||||||
if (!isAvailable(item)) return;
|
if (!isAvailable(item) || value === item.name) return;
|
||||||
onChange?.(item);
|
onChange?.(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type ViewEventsModalProps = {
|
|||||||
name: string;
|
name: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
clusterID?: number;
|
clusterID?: number;
|
||||||
|
hasPersistentVolume?: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,7 +32,8 @@ const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
|
|||||||
|
|
||||||
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { open, onCancel, name, namespace, clusterID } = props || {};
|
const { open, onCancel, name, namespace, clusterID, hasPersistentVolume } =
|
||||||
|
props || {};
|
||||||
const [activeKey, setActiveKey] = useState('instance');
|
const [activeKey, setActiveKey] = useState('instance');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -54,7 +56,9 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
|||||||
const refreshAll = () => {
|
const refreshAll = () => {
|
||||||
if (!name || !namespace || !clusterID) return;
|
if (!name || !namespace || !clusterID) return;
|
||||||
fetchInstanceEvents({ name, namespace, clusterID });
|
fetchInstanceEvents({ name, namespace, clusterID });
|
||||||
fetchVolumeEvents({ name, namespace, clusterID });
|
if (hasPersistentVolume) {
|
||||||
|
fetchVolumeEvents({ name, namespace, clusterID });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -184,16 +188,20 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
|||||||
}),
|
}),
|
||||||
children: renderTable(instanceEvents, instanceLoading)
|
children: renderTable(instanceEvents, instanceLoading)
|
||||||
},
|
},
|
||||||
{
|
...(hasPersistentVolume
|
||||||
key: 'volume',
|
? [
|
||||||
label: intl.formatMessage({
|
{
|
||||||
id: 'gpuservice.instance.event.tab.volume'
|
key: 'volume',
|
||||||
}),
|
label: intl.formatMessage({
|
||||||
children: renderTable(volumeEvents, volumeLoading)
|
id: 'gpuservice.instance.event.tab.volume'
|
||||||
}
|
}),
|
||||||
|
children: renderTable(volumeEvents, volumeLoading)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
];
|
];
|
||||||
|
|
||||||
const isLoading = instanceLoading || volumeLoading;
|
const isLoading = instanceLoading || (hasPersistentVolume && volumeLoading);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollerModal
|
<ScrollerModal
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { useIntl, useNavigate } from '@umijs/max';
|
import { useIntl, useNavigate } from '@umijs/max';
|
||||||
import { Button, Empty, Form } from 'antd';
|
import { Button, Empty, Form } from 'antd';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -229,6 +230,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
instanceType.status?.acceleratorTiers,
|
instanceType.status?.acceleratorTiers,
|
||||||
count
|
count
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedInstanceType(instanceType);
|
setSelectedInstanceType(instanceType);
|
||||||
setOnceMaxRequest({
|
setOnceMaxRequest({
|
||||||
cpu: parseQuantityToNumber(candidate?.cpu?.onceMaxRequest),
|
cpu: parseQuantityToNumber(candidate?.cpu?.onceMaxRequest),
|
||||||
@@ -237,11 +239,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
clusterId: candidate?.cluster ? Number(candidate.cluster) : null,
|
clusterId: candidate?.cluster ? _.toNumber(candidate.cluster) : null,
|
||||||
spec: {
|
spec: {
|
||||||
type: candidate?.name || '',
|
type: candidate?.name || '',
|
||||||
...(options.writeAccelerator
|
...(options.writeAccelerator
|
||||||
? { resources: { accelerator: count } }
|
? { resources: { accelerator: _.toString(count) } }
|
||||||
: {})
|
: {})
|
||||||
} as any
|
} as any
|
||||||
});
|
});
|
||||||
@@ -278,91 +280,23 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
currentData &&
|
action === PageAction.EDIT ||
|
||||||
(action === PageAction.EDIT ||
|
action === PageAction.VIEW ||
|
||||||
action === PageAction.VIEW ||
|
realAction === PageAction.CREATE
|
||||||
realAction === PageAction.CREATE)
|
|
||||||
) {
|
) {
|
||||||
// The server returns `accelerator` as a string; coerce to a number
|
|
||||||
// so NumberSelection's strict equality picks up the active item.
|
|
||||||
const persistedAccelerator = currentData.spec?.resources?.accelerator;
|
|
||||||
const acceleratorAsNumber =
|
|
||||||
persistedAccelerator != null && persistedAccelerator !== ''
|
|
||||||
? Number(persistedAccelerator)
|
|
||||||
: undefined;
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: currentData.name,
|
...currentData,
|
||||||
displayName: currentData.displayName,
|
enable_ssh: !!currentData?.spec?.sshPublicKeys?.length
|
||||||
description: currentData.description,
|
|
||||||
clusterId: currentData.clusterId,
|
|
||||||
spec: {
|
|
||||||
...currentData.spec,
|
|
||||||
imagePullPolicy:
|
|
||||||
currentData.spec?.imagePullPolicy || DefaultImagePullPolicy,
|
|
||||||
resources: {
|
|
||||||
...currentData.spec?.resources,
|
|
||||||
accelerator: acceleratorAsNumber
|
|
||||||
},
|
|
||||||
sshPublicKeys:
|
|
||||||
currentData.spec?.sshPublicKeys?.map((k) => k.name) ?? []
|
|
||||||
} as any,
|
|
||||||
enable_ssh: !!currentData.spec?.sshPublicKeys?.length
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const candidateName = currentData.spec?.type;
|
|
||||||
const clusterId = currentData.clusterId;
|
|
||||||
const aggregate = instanceTypeList.find((item) =>
|
|
||||||
(item.status?.acceleratorTiers ?? []).some((tier) =>
|
|
||||||
(tier.candidates ?? []).some(
|
|
||||||
(c) => c.name === candidateName && Number(c.cluster) === clusterId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (aggregate) {
|
|
||||||
const count =
|
|
||||||
Number(currentData.spec?.resources?.accelerator) ||
|
|
||||||
(aggregate.spec?.acceleratable ? 1 : 0);
|
|
||||||
// No accelerator rewrite — the form already has the persisted value.
|
|
||||||
resolveAndApply(aggregate, count);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}, [action, currentData, form, open, realAction, instanceTypeList]);
|
}, [action, currentData, form, open, realAction, instanceTypeList]);
|
||||||
|
|
||||||
const handleFinish = async (values: InstanceFormValues) => {
|
const handleFinish = async (values: InstanceFormValues) => {
|
||||||
const selectedKeys = (values.spec as any)?.sshPublicKeys as
|
|
||||||
| string[]
|
|
||||||
| undefined;
|
|
||||||
const volume = values.spec?.volume ?? {};
|
|
||||||
const normalizedVolume = volume.persistentTemplate
|
|
||||||
? {
|
|
||||||
persistentTemplate: {
|
|
||||||
...volume.persistentTemplate,
|
|
||||||
name: volume.persistentTemplate.name || values.name || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: volume.persistent
|
|
||||||
? { persistent: volume.persistent }
|
|
||||||
: volume.ephemeral
|
|
||||||
? { ephemeral: volume.ephemeral }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
// `spec.type` and `clusterId` are kept in sync with the resolved
|
|
||||||
// candidate via `resolveAndApply`, so submission is a straight pass
|
|
||||||
// -through. The API expects `accelerator` as a string per the schema.
|
|
||||||
const acceleratorValue = values.spec?.resources?.accelerator;
|
|
||||||
const normalizedResources = {
|
|
||||||
...values.spec?.resources,
|
|
||||||
accelerator:
|
|
||||||
acceleratorValue != null && acceleratorValue !== ''
|
|
||||||
? String(acceleratorValue)
|
|
||||||
: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const submittedPorts = [...(values.spec?.ports ?? [])];
|
const submittedPorts = [...(values.spec?.ports ?? [])];
|
||||||
const submittedHasSSHPort = submittedPorts.some(
|
const submittedHasSSHPort = submittedPorts.some(
|
||||||
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
||||||
);
|
);
|
||||||
|
|
||||||
if (values.enable_ssh && !submittedHasSSHPort) {
|
if (values.enable_ssh && !submittedHasSSHPort) {
|
||||||
submittedPorts.push({
|
submittedPorts.push({
|
||||||
protocol: 'TCP',
|
protocol: 'TCP',
|
||||||
@@ -370,17 +304,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
name: 'SSH'
|
name: 'SSH'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
console.log('submit values', values, submittedPorts);
|
||||||
await onFinish({
|
await onFinish({
|
||||||
...values,
|
..._.omit(values, ['enable_ssh']),
|
||||||
spec: {
|
spec: {
|
||||||
...values.spec,
|
...values.spec,
|
||||||
ports: submittedPorts,
|
ports: submittedPorts
|
||||||
resources: normalizedResources,
|
|
||||||
volume: normalizedVolume,
|
|
||||||
sshPublicKeys: values.enable_ssh
|
|
||||||
? (selectedKeys ?? []).map((name) => ({ name }))
|
|
||||||
: []
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -451,7 +380,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
},
|
},
|
||||||
volume: {
|
volume: {
|
||||||
ephemeral: {
|
ephemeral: {
|
||||||
capacity: '20Gi'
|
capacity: '50Gi'
|
||||||
},
|
},
|
||||||
persistent: {
|
persistent: {
|
||||||
name: ''
|
name: ''
|
||||||
@@ -462,8 +391,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Basic action={formAction} disabled={disabled} />
|
<Basic action={formAction} disabled={disabled} />
|
||||||
{/* Hidden form field: the candidate.cluster resolved from the
|
|
||||||
selected aggregate + accelerator count. */}
|
|
||||||
<Form.Item name="clusterId" hidden>
|
<Form.Item name="clusterId" hidden>
|
||||||
<CInput.Input />
|
<CInput.Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -537,6 +464,16 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
style={{
|
style={{
|
||||||
marginBottom: 12
|
marginBottom: 12
|
||||||
}}
|
}}
|
||||||
|
normalize={(value) =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value?.map((item) => ({ name: item }))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
getValueProps={(value) => ({
|
||||||
|
value: Array.isArray(value)
|
||||||
|
? value.map((item) => item?.name ?? item)
|
||||||
|
: []
|
||||||
|
})}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -79,13 +79,6 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
return selectedInstanceType?.spec?.acceleratable;
|
return selectedInstanceType?.spec?.acceleratable;
|
||||||
}, [selectedInstanceType, action]);
|
}, [selectedInstanceType, action]);
|
||||||
|
|
||||||
const selectedInstanceData = useMemo(() => {
|
|
||||||
if (action === PageAction.EDIT) {
|
|
||||||
return JSON.parse(currentData?.description || '{}') || {};
|
|
||||||
}
|
|
||||||
return selectedInstanceType;
|
|
||||||
}, [selectedInstanceType, action, currentData]);
|
|
||||||
|
|
||||||
const renderInstanceType = () => {
|
const renderInstanceType = () => {
|
||||||
const description = JSON.parse(currentData?.description || '{}').spec || {};
|
const description = JSON.parse(currentData?.description || '{}').spec || {};
|
||||||
return (
|
return (
|
||||||
@@ -130,6 +123,10 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name={['spec', 'resources', 'accelerator']}
|
name={['spec', 'resources', 'accelerator']}
|
||||||
hidden={action === PageAction.EDIT}
|
hidden={action === PageAction.EDIT}
|
||||||
|
normalize={(value) => (value ? _.toString(value) : undefined)}
|
||||||
|
getValueProps={(value) => ({
|
||||||
|
value: value ? _.toNumber(value) : undefined
|
||||||
|
})}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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 { FormContext } from '@/pages/gpu-service/storage/config/form-context';
|
||||||
|
import useQueryStorageClass from '@/pages/gpu-service/storage/services/use-query-storage-class';
|
||||||
import { ModalFooter } from '@gpustack/core-ui';
|
import { ModalFooter } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } 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,8 +20,17 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
onSubmit
|
onSubmit
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { storageClassList, fetchData: fetchStorageClass } =
|
||||||
|
useQueryStorageClass();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const formRef = useRef<any>(null);
|
const formRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchStorageClass({ page: -1 });
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
formRef.current?.submit();
|
formRef.current?.submit();
|
||||||
};
|
};
|
||||||
@@ -30,7 +41,12 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values: StorageFormData) => {
|
const handleFinish = async (values: StorageFormData) => {
|
||||||
await onSubmit(values);
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await onSubmit(values);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOverlayContainer = useCallback(() => {
|
const getOverlayContainer = useCallback(() => {
|
||||||
@@ -52,6 +68,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
<ModalFooter
|
<ModalFooter
|
||||||
onOk={handleSubmit}
|
onOk={handleSubmit}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
|
loading={loading}
|
||||||
style={{
|
style={{
|
||||||
padding: '16px 24px 24px',
|
padding: '16px 24px 24px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -60,12 +77,14 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GPUServiceStorageForm
|
<FormContext.Provider value={{ storageClassList }}>
|
||||||
ref={formRef}
|
<GPUServiceStorageForm
|
||||||
action={PageAction.CREATE}
|
ref={formRef}
|
||||||
open={open}
|
action={PageAction.CREATE}
|
||||||
onFinish={handleFinish}
|
open={open}
|
||||||
/>
|
onFinish={handleFinish}
|
||||||
|
/>
|
||||||
|
</FormContext.Provider>
|
||||||
</FormOverlayView>
|
</FormOverlayView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,12 +66,18 @@ const StorageVolume = ({
|
|||||||
|
|
||||||
const applyMode = (mode: string) => {
|
const applyMode = (mode: string) => {
|
||||||
if (mode === StorageModeValueMap.Temporary) {
|
if (mode === StorageModeValueMap.Temporary) {
|
||||||
form.setFieldValue(['spec', 'volume'], {
|
form.setFieldValue(
|
||||||
ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` }
|
['spec', 'volume', 'ephemeral', 'capacity'],
|
||||||
});
|
form.getFieldValue(['spec', 'volume', 'ephemeral', 'capacity']) ||
|
||||||
|
DEFAULT_TEMP_CAPACITY_GB
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } });
|
form.setFieldValue(
|
||||||
|
['spec', 'volume', 'persistent', 'name'],
|
||||||
|
form.getFieldValue(['spec', 'volume', 'persistent', 'name']) ||
|
||||||
|
(storageOptions[0]?.value as string)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModeChange = (mode: string) => {
|
const handleModeChange = (mode: string) => {
|
||||||
@@ -82,10 +88,8 @@ const StorageVolume = ({
|
|||||||
const handleCreateStorage = async (values: StorageFormData) => {
|
const handleCreateStorage = async (values: StorageFormData) => {
|
||||||
try {
|
try {
|
||||||
await createStorage({ data: values });
|
await createStorage({ data: values });
|
||||||
await fetchStorage({ page: 1, perPage: 100 });
|
await fetchStorage({ page: -1 });
|
||||||
form.setFieldValue(['spec', 'volume'], {
|
form.setFieldValue(['spec', 'volume', 'persistent', 'name'], values.name);
|
||||||
persistent: { name: values.name }
|
|
||||||
});
|
|
||||||
setOverlayOpen(false);
|
setOverlayOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore
|
// ignore
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
|||||||
dataList = []
|
dataList = []
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleSelect = (item: TemplateItem) => {
|
||||||
|
if (value === item.id) return;
|
||||||
|
onChange?.(item.id, item);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TemplateGrid>
|
<TemplateGrid>
|
||||||
{dataList.map((item: TemplateItem) => (
|
{dataList.map((item: TemplateItem) => (
|
||||||
@@ -63,7 +69,7 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
|||||||
hoverable
|
hoverable
|
||||||
height={102}
|
height={102}
|
||||||
active={value === item.id}
|
active={value === item.id}
|
||||||
onClick={() => onChange?.(item.id, item)}
|
onClick={() => handleSelect(item)}
|
||||||
>
|
>
|
||||||
<TemplateContent>
|
<TemplateContent>
|
||||||
<div className="name">
|
<div className="name">
|
||||||
|
|||||||
@@ -9,19 +9,23 @@ const useViewEvents = () => {
|
|||||||
name: string;
|
name: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
clusterID?: number;
|
clusterID?: number;
|
||||||
|
hasPersistentVolume: boolean;
|
||||||
}>({
|
}>({
|
||||||
open: false,
|
open: false,
|
||||||
name: '',
|
name: '',
|
||||||
namespace: '',
|
namespace: '',
|
||||||
clusterID: undefined
|
clusterID: undefined,
|
||||||
|
hasPersistentVolume: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const openModal = (row?: ListItem) => {
|
const openModal = (row?: ListItem) => {
|
||||||
|
const volume = row?.spec?.volume;
|
||||||
setOpenModalStatus({
|
setOpenModalStatus({
|
||||||
open: true,
|
open: true,
|
||||||
name: row?.name || '',
|
name: row?.name || '',
|
||||||
namespace: row?.status?.namespace || '',
|
namespace: row?.status?.namespace || '',
|
||||||
clusterID: row?.clusterId ?? undefined
|
clusterID: row?.clusterId ?? undefined,
|
||||||
|
hasPersistentVolume: !!volume?.persistent?.name
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,7 +34,8 @@ const useViewEvents = () => {
|
|||||||
open: false,
|
open: false,
|
||||||
name: '',
|
name: '',
|
||||||
namespace: '',
|
namespace: '',
|
||||||
clusterID: undefined
|
clusterID: undefined,
|
||||||
|
hasPersistentVolume: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const GPUService: React.FC = () => {
|
|||||||
if (openInstanceModalStatus.realAction === PageAction.CREATE) {
|
if (openInstanceModalStatus.realAction === PageAction.CREATE) {
|
||||||
await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id);
|
await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id);
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 300);
|
||||||
});
|
});
|
||||||
await createInstance({ data });
|
await createInstance({ data });
|
||||||
} else if (openInstanceModalStatus.action === PageAction.EDIT) {
|
} else if (openInstanceModalStatus.action === PageAction.EDIT) {
|
||||||
@@ -233,6 +233,7 @@ const GPUService: React.FC = () => {
|
|||||||
name={openViewEventsModalStatus.name}
|
name={openViewEventsModalStatus.name}
|
||||||
namespace={openViewEventsModalStatus.namespace}
|
namespace={openViewEventsModalStatus.namespace}
|
||||||
clusterID={openViewEventsModalStatus.clusterID}
|
clusterID={openViewEventsModalStatus.clusterID}
|
||||||
|
hasPersistentVolume={openViewEventsModalStatus.hasPersistentVolume}
|
||||||
onCancel={closeViewEventsModal}
|
onCancel={closeViewEventsModal}
|
||||||
/>
|
/>
|
||||||
<DeleteModal ref={modalRef} />
|
<DeleteModal ref={modalRef} />
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { PageAction, validateLabelNameRegxFor63 } from '@/config';
|
|||||||
import {
|
import {
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
Select as SealSelect,
|
Select as SealSelect,
|
||||||
Textarea,
|
|
||||||
useAppUtils
|
useAppUtils
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
@@ -43,13 +42,6 @@ const Basic = ({ action }: { action: string }) => {
|
|||||||
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name="description">
|
|
||||||
<Textarea
|
|
||||||
scaleSize={true}
|
|
||||||
trim={false}
|
|
||||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name="type"
|
name="type"
|
||||||
rules={[
|
rules={[
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const NFSForm = ({ action }: { action: string }) => {
|
|||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
required
|
required
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: 'gpuservice.storageType.nfs.server.tips'
|
||||||
|
})}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.nfs.server'
|
id: 'gpuservice.storageType.nfs.server'
|
||||||
})}
|
})}
|
||||||
@@ -40,14 +43,14 @@ const NFSForm = ({ action }: { action: string }) => {
|
|||||||
>
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
required
|
required
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: 'gpuservice.storageType.nfs.share.tips'
|
||||||
|
})}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.storageType.nfs.share' })}
|
label={intl.formatMessage({ id: 'gpuservice.storageType.nfs.share' })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name={['spec', 'nfs', 'subDirectory']}>
|
<Form.Item<FormData> name={['spec', 'nfs', 'subDirectory']}>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
description={intl.formatMessage({
|
|
||||||
id: 'gpuservice.storageType.nfs.subDirectory.tips'
|
|
||||||
})}
|
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.nfs.subDirectory'
|
id: 'gpuservice.storageType.nfs.subDirectory'
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -12,8 +12,23 @@ import { FormData } from '../config/types';
|
|||||||
const S3Form = ({ action }: { action: string }) => {
|
const S3Form = ({ action }: { action: string }) => {
|
||||||
console.log('action', action);
|
console.log('action', action);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const form = Form.useFormInstance<FormData>();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
|
|
||||||
|
const handleEndpointBlur = (e: any) => {
|
||||||
|
const value: string = e.target.value;
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
// Auto-fill region based on endpoint if it's an AWS S3 endpoint
|
||||||
|
const awsMatch = value.match(
|
||||||
|
/^https?:\/\/s3[.-]([a-z0-9-]+)\.amazonaws\.com/
|
||||||
|
);
|
||||||
|
if (awsMatch) {
|
||||||
|
const region = awsMatch[1];
|
||||||
|
form.setFieldValue(['spec', 's3', 'region'], region);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
@@ -39,7 +54,11 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.s3.endpoint'
|
id: 'gpuservice.storageType.s3.endpoint'
|
||||||
})}
|
})}
|
||||||
placeholder="http | https://..."
|
description={intl.formatMessage({
|
||||||
|
id: 'gpuservice.storageType.s3.endpoint.tips'
|
||||||
|
})}
|
||||||
|
onBlur={handleEndpointBlur}
|
||||||
|
placeholder="https://s3.<region>.amazonaws.com"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Flex gap={16}>
|
<Flex gap={16}>
|
||||||
@@ -53,11 +72,36 @@ const S3Form = ({ action }: { action: string }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Form.Item<FormData> name={['spec', 's3', 'bucket']}>
|
<Form.Item<FormData>
|
||||||
|
name={['spec', 's3', 'bucket']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: getRuleMessage(
|
||||||
|
'input',
|
||||||
|
'gpuservice.storageType.s3.bucket'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
description={intl.formatMessage({
|
required
|
||||||
id: 'gpuservice.storageType.s3.bucket.tips'
|
description={
|
||||||
})}
|
<Flex orientation="vertical" gap={4} align="start">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'gpuservice.storageType.s3.bucket.tips1'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: intl.formatMessage({
|
||||||
|
id: 'gpuservice.storageType.s3.bucket.tips2'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.storageType.s3.bucket'
|
id: 'gpuservice.storageType.s3.bucket'
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -66,18 +66,18 @@ const useStorageTypeColumns = ({
|
|||||||
sorter: false,
|
sorter: false,
|
||||||
render: (_text, record) => getKindLabel(record)
|
render: (_text, record) => getKindLabel(record)
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: intl.formatMessage({ id: 'common.table.description' }),
|
// title: intl.formatMessage({ id: 'common.table.description' }),
|
||||||
dataIndex: 'description',
|
// dataIndex: 'description',
|
||||||
key: 'description',
|
// key: 'description',
|
||||||
sorter: false,
|
// sorter: false,
|
||||||
ellipsis: { showTitle: false },
|
// ellipsis: { showTitle: false },
|
||||||
render: (text: string) => (
|
// render: (text: string) => (
|
||||||
<AutoTooltip ghost minWidth={20}>
|
// <AutoTooltip ghost minWidth={20}>
|
||||||
{text || '-'}
|
// {text || '-'}
|
||||||
</AutoTooltip>
|
// </AutoTooltip>
|
||||||
)
|
// )
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { PageActionType } from '@/config/types';
|
|||||||
import { ModalFooter } from '@gpustack/core-ui';
|
import { ModalFooter } from '@gpustack/core-ui';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import FormDrawer from '../../../_components/form-drawer';
|
import FormDrawer from '../../../_components/form-drawer';
|
||||||
|
import { FormContext } from '../config/form-context';
|
||||||
import { FormData, ListItem } from '../config/types';
|
import { FormData, ListItem } from '../config/types';
|
||||||
import GPUServiceStorageForm from '../forms';
|
import GPUServiceStorageForm from '../forms';
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ type AddModalProps = {
|
|||||||
onOk: (values: FormData) => void;
|
onOk: (values: FormData) => void;
|
||||||
data?: ListItem | null;
|
data?: ListItem | null;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
storageClassList: Global.BaseOption<string>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddModal: React.FC<AddModalProps> = ({
|
const AddModal: React.FC<AddModalProps> = ({
|
||||||
@@ -20,7 +22,8 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
open,
|
open,
|
||||||
onOk,
|
onOk,
|
||||||
data,
|
data,
|
||||||
onCancel
|
onCancel,
|
||||||
|
storageClassList
|
||||||
}) => {
|
}) => {
|
||||||
const form = useRef<any>(null);
|
const form = useRef<any>(null);
|
||||||
|
|
||||||
@@ -58,13 +61,15 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GPUServiceStorageForm
|
<FormContext.Provider value={{ storageClassList }}>
|
||||||
ref={form}
|
<GPUServiceStorageForm
|
||||||
action={action}
|
ref={form}
|
||||||
currentData={data}
|
action={action}
|
||||||
onFinish={onFinish}
|
currentData={data}
|
||||||
open={open}
|
onFinish={onFinish}
|
||||||
/>
|
open={open}
|
||||||
|
/>
|
||||||
|
</FormContext.Provider>
|
||||||
</FormDrawer>
|
</FormDrawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export interface FormContextValue {
|
||||||
|
storageClassList: Global.BaseOption<string>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormContext = createContext<FormContextValue>({
|
||||||
|
storageClassList: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FormContext;
|
||||||
@@ -3,26 +3,19 @@ import {
|
|||||||
Input as CInput,
|
Input as CInput,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select as SealSelect,
|
Select as SealSelect,
|
||||||
Textarea,
|
|
||||||
useAppUtils
|
useAppUtils
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Flex, Form } from 'antd';
|
import { Flex, Form } from 'antd';
|
||||||
import { useEffect } from 'react';
|
import { useContext } from 'react';
|
||||||
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
|
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
|
||||||
|
import { FormContext } from '../config/form-context';
|
||||||
import { FormData } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
import useQueryStorageClass from '../services/use-query-storage-class';
|
|
||||||
|
|
||||||
const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
const Basic = ({ action, open }: { action: string; open: boolean }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const { storageClassList, fetchData, loading } = useQueryStorageClass();
|
const { storageClassList } = useContext(FormContext);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
fetchData({ page: 1, perPage: 100 });
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -52,13 +45,6 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
|||||||
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData> name="description">
|
|
||||||
<Textarea
|
|
||||||
trim={false}
|
|
||||||
scaleSize={true}
|
|
||||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Flex gap={16}>
|
<Flex gap={16}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
@@ -74,7 +60,6 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
|||||||
disabled={action === PageAction.EDIT}
|
disabled={action === PageAction.EDIT}
|
||||||
label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
|
label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
|
||||||
required
|
required
|
||||||
loading={loading}
|
|
||||||
options={storageClassList}
|
options={storageClassList}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
|||||||
preserve={false}
|
preserve={false}
|
||||||
initialValues={{}}
|
initialValues={{}}
|
||||||
>
|
>
|
||||||
<Basic open={open} action={action} />
|
<Basic action={action} open={open} />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
|
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
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';
|
||||||
import { rowActionList, status, StoragePhaseLabelMap } from '../config';
|
import { rowActionList } from '../config';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
handleSelect: (val: string, record: ListItem) => void;
|
handleSelect: (val: string, record: ListItem) => void;
|
||||||
|
storageClassList: Global.BaseOption<string>[];
|
||||||
sortOrder: string[];
|
sortOrder: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStorageColumns = ({
|
const useStorageColumns = ({
|
||||||
handleSelect,
|
handleSelect,
|
||||||
|
storageClassList,
|
||||||
sortOrder
|
sortOrder
|
||||||
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -37,7 +39,14 @@ const useStorageColumns = ({
|
|||||||
dataIndex: ['spec', 'type'],
|
dataIndex: ['spec', 'type'],
|
||||||
key: 'type',
|
key: 'type',
|
||||||
sorter: false,
|
sorter: false,
|
||||||
render: (value: string) => value || '-'
|
render: (value: string) => {
|
||||||
|
return (
|
||||||
|
<AutoTooltip ghost>
|
||||||
|
{storageClassList.find((item) => item.value === value)?.label ||
|
||||||
|
'-'}
|
||||||
|
</AutoTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
|
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
|
||||||
@@ -46,23 +55,23 @@ const useStorageColumns = ({
|
|||||||
sorter: false,
|
sorter: false,
|
||||||
render: (value: string) => (value ? value.replace(/Gi$/, 'GB') : '-')
|
render: (value: string) => (value ? value.replace(/Gi$/, 'GB') : '-')
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
// title: intl.formatMessage({ id: 'common.table.status' }),
|
||||||
dataIndex: ['status', 'phase'],
|
// dataIndex: ['status', 'phase'],
|
||||||
key: 'status',
|
// key: 'status',
|
||||||
sorter: false,
|
// sorter: false,
|
||||||
render: (value: string) =>
|
// render: (value: string) =>
|
||||||
value ? (
|
// value ? (
|
||||||
<StatusTag
|
// <StatusTag
|
||||||
statusValue={{
|
// statusValue={{
|
||||||
status: status[value],
|
// status: status[value],
|
||||||
text: StoragePhaseLabelMap[value] || value
|
// text: StoragePhaseLabelMap[value] || value
|
||||||
}}
|
// }}
|
||||||
></StatusTag>
|
// ></StatusTag>
|
||||||
) : (
|
// ) : (
|
||||||
'-'
|
// '-'
|
||||||
)
|
// )
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
@@ -89,7 +98,7 @@ const useStorageColumns = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [handleSelect, sortOrder, intl]);
|
}, [handleSelect, sortOrder, storageClassList, intl]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useStorageColumns;
|
export default useStorageColumns;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useIntl } from '@umijs/max';
|
|||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { ConfigProvider, message, Table } from 'antd';
|
import { ConfigProvider, message, Table } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import PageBox from '../../_components/page-box';
|
import PageBox from '../../_components/page-box';
|
||||||
import {
|
import {
|
||||||
deleteGPUServiceStorage,
|
deleteGPUServiceStorage,
|
||||||
@@ -17,6 +18,7 @@ import { FormData, ListItem } from './config/types';
|
|||||||
import useCreateStorageModal from './hooks/use-create-storage-modal';
|
import useCreateStorageModal from './hooks/use-create-storage-modal';
|
||||||
import useStorageColumns from './hooks/use-storage-columns';
|
import useStorageColumns from './hooks/use-storage-columns';
|
||||||
import useCreateStorage from './services/use-create-storage';
|
import useCreateStorage from './services/use-create-storage';
|
||||||
|
import useQueryStorageClass from './services/use-query-storage-class';
|
||||||
import useUpdateStorage from './services/use-update-storage';
|
import useUpdateStorage from './services/use-update-storage';
|
||||||
|
|
||||||
const GPUServiceStorage: React.FC = () => {
|
const GPUServiceStorage: React.FC = () => {
|
||||||
@@ -40,16 +42,20 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
fetchAPI: queryGPUServiceStorage,
|
fetchAPI: queryGPUServiceStorage,
|
||||||
deleteAPI: deleteGPUServiceStorage,
|
deleteAPI: deleteGPUServiceStorage,
|
||||||
watch: false,
|
watch: false,
|
||||||
polling: true,
|
|
||||||
API: GPU_SERVICE_STORAGE_API,
|
API: GPU_SERVICE_STORAGE_API,
|
||||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
|
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
|
||||||
});
|
});
|
||||||
|
const { storageClassList, fetchData: fetchStorageClass } =
|
||||||
|
useQueryStorageClass();
|
||||||
const { fetchData: createStorage } = useCreateStorage();
|
const { fetchData: createStorage } = useCreateStorage();
|
||||||
const { fetchData: updateStorage } = useUpdateStorage();
|
const { fetchData: updateStorage } = useUpdateStorage();
|
||||||
const { openStorageModalStatus, openStorageModal, closeStorageModal } =
|
const { openStorageModalStatus, openStorageModal, closeStorageModal } =
|
||||||
useCreateStorageModal();
|
useCreateStorageModal();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStorageClass({ page: -1 });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAddStorage = () => {
|
const handleAddStorage = () => {
|
||||||
openStorageModal(
|
openStorageModal(
|
||||||
PageAction.CREATE,
|
PageAction.CREATE,
|
||||||
@@ -123,6 +129,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
|
|
||||||
const columns = useStorageColumns({
|
const columns = useStorageColumns({
|
||||||
handleSelect,
|
handleSelect,
|
||||||
|
storageClassList,
|
||||||
sortOrder
|
sortOrder
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,6 +177,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
action={openStorageModalStatus.action}
|
action={openStorageModalStatus.action}
|
||||||
title={openStorageModalStatus.title}
|
title={openStorageModalStatus.title}
|
||||||
data={openStorageModalStatus.currentData}
|
data={openStorageModalStatus.currentData}
|
||||||
|
storageClassList={storageClassList}
|
||||||
onCancel={closeStorageModal}
|
onCancel={closeStorageModal}
|
||||||
onOk={handleModalOk}
|
onOk={handleModalOk}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ const protocolOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const wellKnownTcpPortNames: Record<number, string> = {
|
const wellKnownTcpPortNames: Record<number, string> = {
|
||||||
22: 'ssh',
|
22: 'SSH',
|
||||||
80: 'http',
|
80: 'WEB',
|
||||||
443: 'https'
|
443: 'HTTPS'
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAutoFilledName = (
|
const getAutoFilledName = (
|
||||||
|
|||||||
Reference in New Issue
Block a user