fix: ux issues
This commit is contained in:
@@ -186,6 +186,7 @@ const baseRoutes = [
|
||||
name: 'gpuService',
|
||||
path: '/gpu-service',
|
||||
key: 'gpuService',
|
||||
access: 'canSeeAdmin',
|
||||
routes: [
|
||||
{
|
||||
path: '/gpu-service',
|
||||
|
||||
@@ -46,7 +46,11 @@ export default {
|
||||
'gpuservice.storageType.kind': 'Type',
|
||||
'gpuservice.storageType.mountOptions': 'Mount Options',
|
||||
'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.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.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.',
|
||||
@@ -54,11 +58,17 @@ export default {
|
||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||
'Inherit the file permissions from the NFS server.',
|
||||
'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.region': 'Region',
|
||||
'gpuservice.storageType.s3.bucket': 'Bucket',
|
||||
'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.',
|
||||
'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.secretKey': 'Secret Key',
|
||||
'gpuservice.storageType.s3.insecure': 'Skip TLS/SSL certificate verification',
|
||||
|
||||
@@ -44,7 +44,11 @@ export default {
|
||||
'gpuservice.storageType.kind': '種別',
|
||||
'gpuservice.storageType.mountOptions': 'マウントオプション',
|
||||
'gpuservice.storageType.nfs.server': 'NFS サーバー',
|
||||
'gpuservice.storageType.nfs.server.tips':
|
||||
'すべての Kubernetes クラスターから NFS サーバーアドレスにアクセスできることを確認してください。',
|
||||
'gpuservice.storageType.nfs.share': '共有パス',
|
||||
'gpuservice.storageType.nfs.share.tips':
|
||||
'この共有パス配下に、組織名とストレージ名に基づくディレクトリが自動的に作成されます。サブディレクトリが指定されている場合、生成されたディレクトリはそのサブディレクトリ配下に作成されます。',
|
||||
'gpuservice.storageType.nfs.subDirectory': 'サブディレクトリ',
|
||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||
'空の場合、永続ボリューム名のサブディレクトリが作成されます。設定されている場合、このサブディレクトリ配下に永続ボリューム名のディレクトリが作成されます。',
|
||||
@@ -52,12 +56,18 @@ export default {
|
||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||
'NFS サーバー上のファイル権限を継承します。',
|
||||
'gpuservice.storageType.s3.endpoint': 'エンドポイント',
|
||||
'gpuservice.storageType.s3.endpoint.tips':
|
||||
'すべての Kubernetes クラスターから S3 エンドポイントにアクセスできることを確認してください。',
|
||||
'gpuservice.storageType.s3.endpoint.rule':
|
||||
'http または https で始まる必要があります',
|
||||
'gpuservice.storageType.s3.region': 'リージョン',
|
||||
'gpuservice.storageType.s3.bucket': 'バケット',
|
||||
'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.secretKey': 'シークレットキー',
|
||||
'gpuservice.storageType.s3.insecure': 'TLS/SSL 証明書の検証をスキップ',
|
||||
|
||||
@@ -47,7 +47,11 @@ export default {
|
||||
'gpuservice.storageType.kind': 'Тип',
|
||||
'gpuservice.storageType.mountOptions': 'Параметры монтирования',
|
||||
'gpuservice.storageType.nfs.server': 'Сервер NFS',
|
||||
'gpuservice.storageType.nfs.server.tips':
|
||||
'Убедитесь, что адрес NFS-сервера доступен из всех кластеров Kubernetes.',
|
||||
'gpuservice.storageType.nfs.share': 'Путь общего ресурса',
|
||||
'gpuservice.storageType.nfs.share.tips':
|
||||
'В этом общем пути будет автоматически создан каталог на основе названия организации и названия хранилища. Если указан подкаталог, итоговый каталог будет создан внутри него.',
|
||||
'gpuservice.storageType.nfs.subDirectory': 'Подкаталог',
|
||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||
'Если поле пустое, будет создан подкаталог с именем постоянного тома. Если задано, под этим подкаталогом будет создан каталог с именем постоянного тома.',
|
||||
@@ -55,15 +59,22 @@ export default {
|
||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||
'Наследует права файлов с NFS-сервера.',
|
||||
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||
'gpuservice.storageType.s3.endpoint.tips':
|
||||
'Убедитесь, что S3 endpoint доступен из всех кластеров Kubernetes.',
|
||||
'gpuservice.storageType.s3.endpoint.rule':
|
||||
'Должен начинаться с http или https',
|
||||
'gpuservice.storageType.s3.region': 'Регион',
|
||||
'gpuservice.storageType.s3.bucket': 'Бакет',
|
||||
'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.secretKey': 'Secret Key',
|
||||
'gpuservice.storageType.s3.insecure': 'Пропустить проверку сертификата TLS/SSL',
|
||||
'gpuservice.storageType.s3.insecure':
|
||||
'Пропустить проверку сертификата TLS/SSL',
|
||||
'gpuservice.storageType.s3.insecure.tips':
|
||||
'Если включено, сертификат сервера S3 не проверяется. Подходит для внутреннего тестирования или самоподписанных сертификатов; в производственной среде включайте с осторожностью.',
|
||||
'gpuservice.publicKey': 'Открытый ключ SSH',
|
||||
|
||||
@@ -46,7 +46,11 @@ export default {
|
||||
'gpuservice.storageType.kind': 'Tür',
|
||||
'gpuservice.storageType.mountOptions': 'Bağlama Seçenekleri',
|
||||
'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.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.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.',
|
||||
@@ -54,12 +58,17 @@ export default {
|
||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||
'NFS sunucusundaki dosya izinleri devralınır.',
|
||||
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||
'gpuservice.storageType.s3.endpoint.rule':
|
||||
'http veya https ile başlamalıdır',
|
||||
'gpuservice.storageType.s3.endpoint.tips':
|
||||
'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.bucket': 'Kova',
|
||||
'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.',
|
||||
'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.secretKey': 'Secret Key',
|
||||
'gpuservice.storageType.s3.insecure': 'TLS/SSL sertifika doğrulamasını atla',
|
||||
|
||||
@@ -41,7 +41,11 @@ export default {
|
||||
'gpuservice.storageType.kind': '类型',
|
||||
'gpuservice.storageType.mountOptions': '挂载参数',
|
||||
'gpuservice.storageType.nfs.server': 'NFS 服务器',
|
||||
'gpuservice.storageType.nfs.server.tips':
|
||||
'确保所有 Kubernetes 集群都能访问该 NFS 服务地址。',
|
||||
'gpuservice.storageType.nfs.share': '共享路径',
|
||||
'gpuservice.storageType.nfs.share.tips':
|
||||
'系统会在该共享路径下自动创建一个基于组织名称和存储名称的目录。如果指定了子目录,则生成的目录会创建在该子目录下。',
|
||||
'gpuservice.storageType.nfs.subDirectory': '子目录',
|
||||
'gpuservice.storageType.nfs.subDirectory.tips':
|
||||
'如果为空,会以持久卷的卷名创建一个子目录;如果不为空,会在同名子目录下,以持久卷的卷名创建一个孙目录。',
|
||||
@@ -49,11 +53,17 @@ export default {
|
||||
'gpuservice.storageType.nfs.mountPermissions.tips':
|
||||
'沿用 NFS 服务器文件的权限。',
|
||||
'gpuservice.storageType.s3.endpoint': 'Endpoint',
|
||||
'gpuservice.storageType.s3.endpoint.tips':
|
||||
'确保所有 Kubernetes 集群都能访问该 S3 Endpoint。',
|
||||
'gpuservice.storageType.s3.endpoint.rule': '必须以 http 或 https 开头',
|
||||
'gpuservice.storageType.s3.region': '区域',
|
||||
'gpuservice.storageType.s3.bucket': '存储桶',
|
||||
'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.secretKey': 'Secret Key',
|
||||
'gpuservice.storageType.s3.insecure': '跳过 TLS/SSL 证书验证',
|
||||
|
||||
@@ -31,13 +31,6 @@ type AddModalProps = {
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
type InstanceTypeSelection = {
|
||||
instanceType?: string;
|
||||
manufacturer?: string;
|
||||
};
|
||||
|
||||
const EMPTY_INSTANCE_TYPE_SELECTION: InstanceTypeSelection = {};
|
||||
|
||||
const matchKeyword = (fields: Array<unknown>, keyword: string) => {
|
||||
const trimmed = keyword.trim().toLowerCase();
|
||||
if (!trimmed) return true;
|
||||
@@ -85,11 +78,17 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
const intl = useIntl();
|
||||
const form = useRef<any>(null);
|
||||
const sessionRef = useRef(0);
|
||||
const [instanceTypeSelection, setInstanceTypeSelection] =
|
||||
useState<InstanceTypeSelection>(EMPTY_INSTANCE_TYPE_SELECTION);
|
||||
const [instanceTypeSelection, setInstanceTypeSelection] = useState<{
|
||||
instanceType?: string;
|
||||
manufacturer?: string;
|
||||
}>({
|
||||
instanceType: undefined,
|
||||
manufacturer: undefined
|
||||
});
|
||||
const [templateId, setTemplateId] = useState<number | undefined>();
|
||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
detailData,
|
||||
@@ -116,6 +115,20 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
: 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 = (
|
||||
instanceType: InstanceTypeItem,
|
||||
template: TemplateItem | undefined
|
||||
@@ -126,37 +139,29 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
instanceType: instanceType.name,
|
||||
manufacturer
|
||||
});
|
||||
|
||||
setTemplateId(template?.id);
|
||||
|
||||
if (template) {
|
||||
const currentSpec = form.current?.getFieldsValue()?.spec || {};
|
||||
const formValues = form.current?.getFieldsValue();
|
||||
form.current?.setFieldsValue({
|
||||
manufacturer: template.manufacturer,
|
||||
description: JSON.stringify(
|
||||
{
|
||||
name: instanceType.name,
|
||||
spec: {
|
||||
...instanceType.spec
|
||||
}
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
description: saveInstanceDataInDescription(instanceType),
|
||||
spec: {
|
||||
...currentSpec,
|
||||
...formValues?.spec,
|
||||
...template.spec,
|
||||
resources: {
|
||||
...(currentSpec.resources || {}),
|
||||
...(template.spec?.resources || {})
|
||||
sshPublicKeys: formValues?.spec?.sshPublicKeys,
|
||||
volume: {
|
||||
...formValues?.spec?.volume
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
form.current?.setFieldsValue({
|
||||
manufacturer: undefined,
|
||||
description: JSON.stringify(instanceType.spec, null, 2)
|
||||
description: saveInstanceDataInDescription(instanceType)
|
||||
});
|
||||
}
|
||||
|
||||
// update form
|
||||
form.current?.applyInstanceType?.(instanceType);
|
||||
};
|
||||
|
||||
@@ -175,6 +180,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// initial
|
||||
const applyAutoSelection = (
|
||||
instanceTypes: InstanceTypeItem[],
|
||||
templates: TemplateItem[]
|
||||
@@ -208,7 +214,10 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
sessionRef.current += 1;
|
||||
setInstanceTypeSelection(EMPTY_INSTANCE_TYPE_SELECTION);
|
||||
setInstanceTypeSelection({
|
||||
instanceType: undefined,
|
||||
manufacturer: undefined
|
||||
});
|
||||
setTemplateId(undefined);
|
||||
setInstanceKeyword('');
|
||||
setTemplateKeyword('');
|
||||
@@ -240,6 +249,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return matchKeyword(
|
||||
[item.name, item.spec?.image, item.spec?.volumeMount],
|
||||
templateKeyword
|
||||
@@ -256,9 +266,14 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
};
|
||||
|
||||
const onFinish = async (values: FormData) => {
|
||||
onOk({
|
||||
...values
|
||||
});
|
||||
setLoading(true);
|
||||
try {
|
||||
await onOk({
|
||||
...values
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
|
||||
@@ -271,10 +286,19 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
|
||||
const handleTemplateChange = (id: number, item: TemplateItem) => {
|
||||
setTemplateId(id);
|
||||
const formValues = form.current?.getFieldsValue();
|
||||
form.current?.setFieldsValue({
|
||||
spec: {
|
||||
...form.current?.getFieldsValue()?.spec,
|
||||
...item.spec
|
||||
...formValues?.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}
|
||||
onCancel={handleCancel}
|
||||
showOkBtn={!readonly}
|
||||
loading={loading}
|
||||
style={{
|
||||
padding: '16px 24px 8px',
|
||||
display: 'flex',
|
||||
|
||||
@@ -34,7 +34,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||
loading
|
||||
}) => {
|
||||
const handleSelect = (item: InstanceTypeItemModel) => {
|
||||
if (!isAvailable(item)) return;
|
||||
if (!isAvailable(item) || value === item.name) return;
|
||||
onChange?.(item);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type ViewEventsModalProps = {
|
||||
name: string;
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
hasPersistentVolume?: boolean;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
@@ -31,7 +32,8 @@ const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
|
||||
|
||||
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { open, onCancel, name, namespace, clusterID } = props || {};
|
||||
const { open, onCancel, name, namespace, clusterID, hasPersistentVolume } =
|
||||
props || {};
|
||||
const [activeKey, setActiveKey] = useState('instance');
|
||||
|
||||
const {
|
||||
@@ -54,7 +56,9 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
const refreshAll = () => {
|
||||
if (!name || !namespace || !clusterID) return;
|
||||
fetchInstanceEvents({ name, namespace, clusterID });
|
||||
fetchVolumeEvents({ name, namespace, clusterID });
|
||||
if (hasPersistentVolume) {
|
||||
fetchVolumeEvents({ name, namespace, clusterID });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -184,16 +188,20 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
}),
|
||||
children: renderTable(instanceEvents, instanceLoading)
|
||||
},
|
||||
{
|
||||
key: 'volume',
|
||||
label: intl.formatMessage({
|
||||
id: 'gpuservice.instance.event.tab.volume'
|
||||
}),
|
||||
children: renderTable(volumeEvents, volumeLoading)
|
||||
}
|
||||
...(hasPersistentVolume
|
||||
? [
|
||||
{
|
||||
key: 'volume',
|
||||
label: intl.formatMessage({
|
||||
id: 'gpuservice.instance.event.tab.volume'
|
||||
}),
|
||||
children: renderTable(volumeEvents, volumeLoading)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
|
||||
const isLoading = instanceLoading || volumeLoading;
|
||||
const isLoading = instanceLoading || (hasPersistentVolume && volumeLoading);
|
||||
|
||||
return (
|
||||
<ScrollerModal
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
import { Button, Empty, Form } from 'antd';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
@@ -229,6 +230,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
instanceType.status?.acceleratorTiers,
|
||||
count
|
||||
);
|
||||
|
||||
setSelectedInstanceType(instanceType);
|
||||
setOnceMaxRequest({
|
||||
cpu: parseQuantityToNumber(candidate?.cpu?.onceMaxRequest),
|
||||
@@ -237,11 +239,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
});
|
||||
|
||||
form.setFieldsValue({
|
||||
clusterId: candidate?.cluster ? Number(candidate.cluster) : null,
|
||||
clusterId: candidate?.cluster ? _.toNumber(candidate.cluster) : null,
|
||||
spec: {
|
||||
type: candidate?.name || '',
|
||||
...(options.writeAccelerator
|
||||
? { resources: { accelerator: count } }
|
||||
? { resources: { accelerator: _.toString(count) } }
|
||||
: {})
|
||||
} as any
|
||||
});
|
||||
@@ -278,91 +280,23 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}
|
||||
|
||||
if (
|
||||
currentData &&
|
||||
(action === PageAction.EDIT ||
|
||||
action === PageAction.VIEW ||
|
||||
realAction === PageAction.CREATE)
|
||||
action === PageAction.EDIT ||
|
||||
action === PageAction.VIEW ||
|
||||
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({
|
||||
name: currentData.name,
|
||||
displayName: currentData.displayName,
|
||||
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
|
||||
...currentData,
|
||||
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]);
|
||||
|
||||
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 submittedHasSSHPort = submittedPorts.some(
|
||||
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
||||
);
|
||||
|
||||
if (values.enable_ssh && !submittedHasSSHPort) {
|
||||
submittedPorts.push({
|
||||
protocol: 'TCP',
|
||||
@@ -370,17 +304,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
name: 'SSH'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('submit values', values, submittedPorts);
|
||||
await onFinish({
|
||||
...values,
|
||||
..._.omit(values, ['enable_ssh']),
|
||||
spec: {
|
||||
...values.spec,
|
||||
ports: submittedPorts,
|
||||
resources: normalizedResources,
|
||||
volume: normalizedVolume,
|
||||
sshPublicKeys: values.enable_ssh
|
||||
? (selectedKeys ?? []).map((name) => ({ name }))
|
||||
: []
|
||||
ports: submittedPorts
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -451,7 +380,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
},
|
||||
volume: {
|
||||
ephemeral: {
|
||||
capacity: '20Gi'
|
||||
capacity: '50Gi'
|
||||
},
|
||||
persistent: {
|
||||
name: ''
|
||||
@@ -462,8 +391,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}}
|
||||
>
|
||||
<Basic action={formAction} disabled={disabled} />
|
||||
{/* Hidden form field: the candidate.cluster resolved from the
|
||||
selected aggregate + accelerator count. */}
|
||||
<Form.Item name="clusterId" hidden>
|
||||
<CInput.Input />
|
||||
</Form.Item>
|
||||
@@ -537,6 +464,16 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
style={{
|
||||
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={[
|
||||
{
|
||||
required: true,
|
||||
|
||||
@@ -79,13 +79,6 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
return selectedInstanceType?.spec?.acceleratable;
|
||||
}, [selectedInstanceType, action]);
|
||||
|
||||
const selectedInstanceData = useMemo(() => {
|
||||
if (action === PageAction.EDIT) {
|
||||
return JSON.parse(currentData?.description || '{}') || {};
|
||||
}
|
||||
return selectedInstanceType;
|
||||
}, [selectedInstanceType, action, currentData]);
|
||||
|
||||
const renderInstanceType = () => {
|
||||
const description = JSON.parse(currentData?.description || '{}').spec || {};
|
||||
return (
|
||||
@@ -130,6 +123,10 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'resources', 'accelerator']}
|
||||
hidden={action === PageAction.EDIT}
|
||||
normalize={(value) => (value ? _.toString(value) : undefined)}
|
||||
getValueProps={(value) => ({
|
||||
value: value ? _.toNumber(value) : undefined
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PageAction } from '@/config';
|
||||
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 { 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 GPUServiceStorageForm from '../../storage/forms';
|
||||
|
||||
@@ -18,8 +20,17 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
||||
onSubmit
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { storageClassList, fetchData: fetchStorageClass } =
|
||||
useQueryStorageClass();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const formRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchStorageClass({ page: -1 });
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
formRef.current?.submit();
|
||||
};
|
||||
@@ -30,7 +41,12 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
||||
};
|
||||
|
||||
const handleFinish = async (values: StorageFormData) => {
|
||||
await onSubmit(values);
|
||||
setLoading(true);
|
||||
try {
|
||||
await onSubmit(values);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getOverlayContainer = useCallback(() => {
|
||||
@@ -52,6 +68,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
||||
<ModalFooter
|
||||
onOk={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
loading={loading}
|
||||
style={{
|
||||
padding: '16px 24px 24px',
|
||||
display: 'flex',
|
||||
@@ -60,12 +77,14 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GPUServiceStorageForm
|
||||
ref={formRef}
|
||||
action={PageAction.CREATE}
|
||||
open={open}
|
||||
onFinish={handleFinish}
|
||||
/>
|
||||
<FormContext.Provider value={{ storageClassList }}>
|
||||
<GPUServiceStorageForm
|
||||
ref={formRef}
|
||||
action={PageAction.CREATE}
|
||||
open={open}
|
||||
onFinish={handleFinish}
|
||||
/>
|
||||
</FormContext.Provider>
|
||||
</FormOverlayView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,12 +66,18 @@ const StorageVolume = ({
|
||||
|
||||
const applyMode = (mode: string) => {
|
||||
if (mode === StorageModeValueMap.Temporary) {
|
||||
form.setFieldValue(['spec', 'volume'], {
|
||||
ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` }
|
||||
});
|
||||
form.setFieldValue(
|
||||
['spec', 'volume', 'ephemeral', 'capacity'],
|
||||
form.getFieldValue(['spec', 'volume', 'ephemeral', 'capacity']) ||
|
||||
DEFAULT_TEMP_CAPACITY_GB
|
||||
);
|
||||
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) => {
|
||||
@@ -82,10 +88,8 @@ const StorageVolume = ({
|
||||
const handleCreateStorage = async (values: StorageFormData) => {
|
||||
try {
|
||||
await createStorage({ data: values });
|
||||
await fetchStorage({ page: 1, perPage: 100 });
|
||||
form.setFieldValue(['spec', 'volume'], {
|
||||
persistent: { name: values.name }
|
||||
});
|
||||
await fetchStorage({ page: -1 });
|
||||
form.setFieldValue(['spec', 'volume', 'persistent', 'name'], values.name);
|
||||
setOverlayOpen(false);
|
||||
} catch (error) {
|
||||
// ignore
|
||||
|
||||
@@ -53,6 +53,12 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
||||
dataList = []
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSelect = (item: TemplateItem) => {
|
||||
if (value === item.id) return;
|
||||
onChange?.(item.id, item);
|
||||
};
|
||||
|
||||
return (
|
||||
<TemplateGrid>
|
||||
{dataList.map((item: TemplateItem) => (
|
||||
@@ -63,7 +69,7 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
|
||||
hoverable
|
||||
height={102}
|
||||
active={value === item.id}
|
||||
onClick={() => onChange?.(item.id, item)}
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
<TemplateContent>
|
||||
<div className="name">
|
||||
|
||||
@@ -9,19 +9,23 @@ const useViewEvents = () => {
|
||||
name: string;
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
hasPersistentVolume: boolean;
|
||||
}>({
|
||||
open: false,
|
||||
name: '',
|
||||
namespace: '',
|
||||
clusterID: undefined
|
||||
clusterID: undefined,
|
||||
hasPersistentVolume: false
|
||||
});
|
||||
|
||||
const openModal = (row?: ListItem) => {
|
||||
const volume = row?.spec?.volume;
|
||||
setOpenModalStatus({
|
||||
open: true,
|
||||
name: row?.name || '',
|
||||
namespace: row?.status?.namespace || '',
|
||||
clusterID: row?.clusterId ?? undefined
|
||||
clusterID: row?.clusterId ?? undefined,
|
||||
hasPersistentVolume: !!volume?.persistent?.name
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,7 +34,8 @@ const useViewEvents = () => {
|
||||
open: false,
|
||||
name: '',
|
||||
namespace: '',
|
||||
clusterID: undefined
|
||||
clusterID: undefined,
|
||||
hasPersistentVolume: false
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ const GPUService: React.FC = () => {
|
||||
if (openInstanceModalStatus.realAction === PageAction.CREATE) {
|
||||
await deleteGPUServiceInstance(openInstanceModalStatus.currentData!.id);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
setTimeout(resolve, 300);
|
||||
});
|
||||
await createInstance({ data });
|
||||
} else if (openInstanceModalStatus.action === PageAction.EDIT) {
|
||||
@@ -233,6 +233,7 @@ const GPUService: React.FC = () => {
|
||||
name={openViewEventsModalStatus.name}
|
||||
namespace={openViewEventsModalStatus.namespace}
|
||||
clusterID={openViewEventsModalStatus.clusterID}
|
||||
hasPersistentVolume={openViewEventsModalStatus.hasPersistentVolume}
|
||||
onCancel={closeViewEventsModal}
|
||||
/>
|
||||
<DeleteModal ref={modalRef} />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { PageAction, validateLabelNameRegxFor63 } from '@/config';
|
||||
import {
|
||||
Input as CInput,
|
||||
Select as SealSelect,
|
||||
Textarea,
|
||||
useAppUtils
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
@@ -43,13 +42,6 @@ const Basic = ({ action }: { action: string }) => {
|
||||
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData> name="description">
|
||||
<Textarea
|
||||
scaleSize={true}
|
||||
trim={false}
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="type"
|
||||
rules={[
|
||||
|
||||
@@ -24,6 +24,9 @@ const NFSForm = ({ action }: { action: string }) => {
|
||||
>
|
||||
<CInput.Input
|
||||
required
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storageType.nfs.server.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.storageType.nfs.server'
|
||||
})}
|
||||
@@ -40,14 +43,14 @@ const NFSForm = ({ action }: { action: string }) => {
|
||||
>
|
||||
<CInput.Input
|
||||
required
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storageType.nfs.share.tips'
|
||||
})}
|
||||
label={intl.formatMessage({ id: 'gpuservice.storageType.nfs.share' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData> name={['spec', 'nfs', 'subDirectory']}>
|
||||
<CInput.Input
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storageType.nfs.subDirectory.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.storageType.nfs.subDirectory'
|
||||
})}
|
||||
|
||||
@@ -12,8 +12,23 @@ import { FormData } from '../config/types';
|
||||
const S3Form = ({ action }: { action: string }) => {
|
||||
console.log('action', action);
|
||||
const intl = useIntl();
|
||||
const form = Form.useFormInstance<FormData>();
|
||||
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 (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
@@ -39,7 +54,11 @@ const S3Form = ({ action }: { action: string }) => {
|
||||
label={intl.formatMessage({
|
||||
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>
|
||||
<Flex gap={16}>
|
||||
@@ -53,11 +72,36 @@ const S3Form = ({ action }: { action: string }) => {
|
||||
</Form.Item>
|
||||
</div>
|
||||
<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
|
||||
description={intl.formatMessage({
|
||||
id: 'gpuservice.storageType.s3.bucket.tips'
|
||||
})}
|
||||
required
|
||||
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({
|
||||
id: 'gpuservice.storageType.s3.bucket'
|
||||
})}
|
||||
|
||||
@@ -66,18 +66,18 @@ const useStorageTypeColumns = ({
|
||||
sorter: false,
|
||||
render: (_text, record) => getKindLabel(record)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.description' }),
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
sorter: false,
|
||||
ellipsis: { showTitle: false },
|
||||
render: (text: string) => (
|
||||
<AutoTooltip ghost minWidth={20}>
|
||||
{text || '-'}
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// title: intl.formatMessage({ id: 'common.table.description' }),
|
||||
// dataIndex: 'description',
|
||||
// key: 'description',
|
||||
// sorter: false,
|
||||
// ellipsis: { showTitle: false },
|
||||
// render: (text: string) => (
|
||||
// <AutoTooltip ghost minWidth={20}>
|
||||
// {text || '-'}
|
||||
// </AutoTooltip>
|
||||
// )
|
||||
// },
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: 'created_at',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PageActionType } from '@/config/types';
|
||||
import { ModalFooter } from '@gpustack/core-ui';
|
||||
import { useRef } from 'react';
|
||||
import FormDrawer from '../../../_components/form-drawer';
|
||||
import { FormContext } from '../config/form-context';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
import GPUServiceStorageForm from '../forms';
|
||||
|
||||
@@ -12,6 +13,7 @@ type AddModalProps = {
|
||||
onOk: (values: FormData) => void;
|
||||
data?: ListItem | null;
|
||||
onCancel: () => void;
|
||||
storageClassList: Global.BaseOption<string>[];
|
||||
};
|
||||
|
||||
const AddModal: React.FC<AddModalProps> = ({
|
||||
@@ -20,7 +22,8 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
open,
|
||||
onOk,
|
||||
data,
|
||||
onCancel
|
||||
onCancel,
|
||||
storageClassList
|
||||
}) => {
|
||||
const form = useRef<any>(null);
|
||||
|
||||
@@ -58,13 +61,15 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GPUServiceStorageForm
|
||||
ref={form}
|
||||
action={action}
|
||||
currentData={data}
|
||||
onFinish={onFinish}
|
||||
open={open}
|
||||
/>
|
||||
<FormContext.Provider value={{ storageClassList }}>
|
||||
<GPUServiceStorageForm
|
||||
ref={form}
|
||||
action={action}
|
||||
currentData={data}
|
||||
onFinish={onFinish}
|
||||
open={open}
|
||||
/>
|
||||
</FormContext.Provider>
|
||||
</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,
|
||||
InputNumber,
|
||||
Select as SealSelect,
|
||||
Textarea,
|
||||
useAppUtils
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex, Form } from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
|
||||
import { FormContext } from '../config/form-context';
|
||||
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 { getRuleMessage } = useAppUtils();
|
||||
const { storageClassList, fetchData, loading } = useQueryStorageClass();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchData({ page: 1, perPage: 100 });
|
||||
}
|
||||
}, [open]);
|
||||
const { storageClassList } = useContext(FormContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -52,13 +45,6 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
||||
label={intl.formatMessage({ id: 'common.table.displayName' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData> name="description">
|
||||
<Textarea
|
||||
trim={false}
|
||||
scaleSize={true}
|
||||
label={intl.formatMessage({ id: 'common.table.description' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Flex gap={16}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item<FormData>
|
||||
@@ -74,7 +60,6 @@ const Basic = ({ open, action }: { open: boolean; action: string }) => {
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
|
||||
required
|
||||
loading={loading}
|
||||
options={storageClassList}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -54,7 +54,7 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
||||
preserve={false}
|
||||
initialValues={{}}
|
||||
>
|
||||
<Basic open={open} action={action} />
|
||||
<Basic action={action} open={open} />
|
||||
</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 type { ColumnsType } from 'antd/lib/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemo } from 'react';
|
||||
import { rowActionList, status, StoragePhaseLabelMap } from '../config';
|
||||
import { rowActionList } from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
interface ColumnsHookProps {
|
||||
handleSelect: (val: string, record: ListItem) => void;
|
||||
storageClassList: Global.BaseOption<string>[];
|
||||
sortOrder: string[];
|
||||
}
|
||||
|
||||
const useStorageColumns = ({
|
||||
handleSelect,
|
||||
storageClassList,
|
||||
sortOrder
|
||||
}: ColumnsHookProps): ColumnsType<ListItem> => {
|
||||
const intl = useIntl();
|
||||
@@ -37,7 +39,14 @@ const useStorageColumns = ({
|
||||
dataIndex: ['spec', 'type'],
|
||||
key: 'type',
|
||||
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' }),
|
||||
@@ -46,23 +55,23 @@ const useStorageColumns = ({
|
||||
sorter: false,
|
||||
render: (value: string) => (value ? value.replace(/Gi$/, 'GB') : '-')
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.status' }),
|
||||
dataIndex: ['status', 'phase'],
|
||||
key: 'status',
|
||||
sorter: false,
|
||||
render: (value: string) =>
|
||||
value ? (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: status[value],
|
||||
text: StoragePhaseLabelMap[value] || value
|
||||
}}
|
||||
></StatusTag>
|
||||
) : (
|
||||
'-'
|
||||
)
|
||||
},
|
||||
// {
|
||||
// title: intl.formatMessage({ id: 'common.table.status' }),
|
||||
// dataIndex: ['status', 'phase'],
|
||||
// key: 'status',
|
||||
// sorter: false,
|
||||
// render: (value: string) =>
|
||||
// value ? (
|
||||
// <StatusTag
|
||||
// statusValue={{
|
||||
// status: status[value],
|
||||
// text: StoragePhaseLabelMap[value] || value
|
||||
// }}
|
||||
// ></StatusTag>
|
||||
// ) : (
|
||||
// '-'
|
||||
// )
|
||||
// },
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||
dataIndex: 'created_at',
|
||||
@@ -89,7 +98,7 @@ const useStorageColumns = ({
|
||||
)
|
||||
}
|
||||
];
|
||||
}, [handleSelect, sortOrder, intl]);
|
||||
}, [handleSelect, sortOrder, storageClassList, intl]);
|
||||
};
|
||||
|
||||
export default useStorageColumns;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, message, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
import PageBox from '../../_components/page-box';
|
||||
import {
|
||||
deleteGPUServiceStorage,
|
||||
@@ -17,6 +18,7 @@ import { FormData, ListItem } from './config/types';
|
||||
import useCreateStorageModal from './hooks/use-create-storage-modal';
|
||||
import useStorageColumns from './hooks/use-storage-columns';
|
||||
import useCreateStorage from './services/use-create-storage';
|
||||
import useQueryStorageClass from './services/use-query-storage-class';
|
||||
import useUpdateStorage from './services/use-update-storage';
|
||||
|
||||
const GPUServiceStorage: React.FC = () => {
|
||||
@@ -40,16 +42,20 @@ const GPUServiceStorage: React.FC = () => {
|
||||
fetchAPI: queryGPUServiceStorage,
|
||||
deleteAPI: deleteGPUServiceStorage,
|
||||
watch: false,
|
||||
polling: true,
|
||||
API: GPU_SERVICE_STORAGE_API,
|
||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
|
||||
});
|
||||
|
||||
const { storageClassList, fetchData: fetchStorageClass } =
|
||||
useQueryStorageClass();
|
||||
const { fetchData: createStorage } = useCreateStorage();
|
||||
const { fetchData: updateStorage } = useUpdateStorage();
|
||||
const { openStorageModalStatus, openStorageModal, closeStorageModal } =
|
||||
useCreateStorageModal();
|
||||
|
||||
useEffect(() => {
|
||||
fetchStorageClass({ page: -1 });
|
||||
}, []);
|
||||
|
||||
const handleAddStorage = () => {
|
||||
openStorageModal(
|
||||
PageAction.CREATE,
|
||||
@@ -123,6 +129,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
|
||||
const columns = useStorageColumns({
|
||||
handleSelect,
|
||||
storageClassList,
|
||||
sortOrder
|
||||
});
|
||||
|
||||
@@ -170,6 +177,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
action={openStorageModalStatus.action}
|
||||
title={openStorageModalStatus.title}
|
||||
data={openStorageModalStatus.currentData}
|
||||
storageClassList={storageClassList}
|
||||
onCancel={closeStorageModal}
|
||||
onOk={handleModalOk}
|
||||
/>
|
||||
|
||||
@@ -29,9 +29,9 @@ const protocolOptions = [
|
||||
];
|
||||
|
||||
const wellKnownTcpPortNames: Record<number, string> = {
|
||||
22: 'ssh',
|
||||
80: 'http',
|
||||
443: 'https'
|
||||
22: 'SSH',
|
||||
80: 'WEB',
|
||||
443: 'HTTPS'
|
||||
};
|
||||
|
||||
const getAutoFilledName = (
|
||||
|
||||
Reference in New Issue
Block a user