fix: alignment api
This commit is contained in:
@@ -27,3 +27,14 @@ export const initialPasswordAtom = atomWithStorage<string>(
|
|||||||
'initialPassword',
|
'initialPassword',
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getCurrentOrganizationId = (): string => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('currentOrganizationId');
|
||||||
|
if (raw) {
|
||||||
|
const value = JSON.parse(raw);
|
||||||
|
if (value != null) return String(value);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return 'gpustack-organization-1';
|
||||||
|
};
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ export default (props: any) => {
|
|||||||
config={{
|
config={{
|
||||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||||
theme: userSettings.theme,
|
theme: userSettings.theme,
|
||||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_nujs54seb6.js',
|
iconUrl: '//at.alicdn.com/t/c/font_4613488_t4uxqkp138r.js',
|
||||||
isDarkTheme: userSettings.isDarkTheme,
|
isDarkTheme: userSettings.isDarkTheme,
|
||||||
defaultColorPrimary: COLOR_PRIMARY
|
defaultColorPrimary: COLOR_PRIMARY
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
'gpuservice.template.imagePullPolicy.never': 'Never',
|
'gpuservice.template.imagePullPolicy.never': 'Never',
|
||||||
'gpuservice.template.command': 'Container Startup Command',
|
'gpuservice.template.command': 'Container Startup Command',
|
||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'One argument per line, e.g.:\n/bin/bash\n-c\nyour command',
|
'Separate arguments with spaces; wrap arguments containing spaces in quotes, e.g.: /bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': 'Mount Path',
|
'gpuservice.template.mountPath': 'Mount Path',
|
||||||
'gpuservice.template.containerDisk': 'Container Disk (Gi)',
|
'gpuservice.template.containerDisk': 'Container Disk (Gi)',
|
||||||
'gpuservice.template.memory': 'Memory (Gi)',
|
'gpuservice.template.memory': 'Memory (Gi)',
|
||||||
@@ -48,14 +48,16 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max':
|
||||||
'The current instance type supports at most {count} GPU(s)',
|
'The current instance type supports at most {count} GPU(s)',
|
||||||
'gpuservice.instance.stock': 'Stock',
|
'gpuservice.instance.stock': 'Stock',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
'gpuservice.instance.ram': 'Memory',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.search.type.placeholder':
|
||||||
'Search by name, VRAM, memory or vCPU',
|
'Search by name, VRAM, memory or vCPU',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'Search by template name, image or mount path',
|
'Search by template name, image or mount path',
|
||||||
'gpuservice.instance.template.image': 'Image',
|
'gpuservice.instance.template.image': 'Image',
|
||||||
'gpuservice.instance.template.mount': 'Mount',
|
'gpuservice.instance.template.mount': 'Mount',
|
||||||
|
'gpuservice.instance.connect': 'Connect',
|
||||||
|
'gpuservice.instance.connect.copySshCommand': 'Copy SSH Command',
|
||||||
'gpuservice.storage': 'Storage',
|
'gpuservice.storage': 'Storage',
|
||||||
'gpuservice.storage.add': 'Add Storage',
|
'gpuservice.storage.add': 'Add Storage',
|
||||||
'gpuservice.storage.edit': 'Edit Storage',
|
'gpuservice.storage.edit': 'Edit Storage',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
'gpuservice.template.imagePullPolicy.never': '取得しない',
|
'gpuservice.template.imagePullPolicy.never': '取得しない',
|
||||||
'gpuservice.template.command': 'コンテナ起動コマンド',
|
'gpuservice.template.command': 'コンテナ起動コマンド',
|
||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'1行に1つの引数、例:\n/bin/bash\n-c\nyour command',
|
'引数はスペースで区切り、スペースを含む引数は引用符で囲んでください。例:/bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': 'マウントパス',
|
'gpuservice.template.mountPath': 'マウントパス',
|
||||||
'gpuservice.template.containerDisk': 'コンテナディスク (Gi)',
|
'gpuservice.template.containerDisk': 'コンテナディスク (Gi)',
|
||||||
'gpuservice.template.memory': 'メモリ (Gi)',
|
'gpuservice.template.memory': 'メモリ (Gi)',
|
||||||
@@ -46,14 +46,16 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max':
|
||||||
'現在のインスタンスタイプは最大 {count} 個の GPU をサポートします',
|
'現在のインスタンスタイプは最大 {count} 個の GPU をサポートします',
|
||||||
'gpuservice.instance.stock': '在庫',
|
'gpuservice.instance.stock': '在庫',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
'gpuservice.instance.ram': 'メモリ',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.search.type.placeholder':
|
||||||
'名前、VRAM、メモリまたは vCPU で検索',
|
'名前、VRAM、メモリまたは vCPU で検索',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'テンプレート名、イメージまたはマウントパスで検索',
|
'テンプレート名、イメージまたはマウントパスで検索',
|
||||||
'gpuservice.instance.template.image': 'イメージ',
|
'gpuservice.instance.template.image': 'イメージ',
|
||||||
'gpuservice.instance.template.mount': 'マウント',
|
'gpuservice.instance.template.mount': 'マウント',
|
||||||
|
'gpuservice.instance.connect': '接続',
|
||||||
|
'gpuservice.instance.connect.copySshCommand': 'SSH コマンドをコピー',
|
||||||
'gpuservice.storage': 'ストレージ',
|
'gpuservice.storage': 'ストレージ',
|
||||||
'gpuservice.storage.add': 'ストレージを追加',
|
'gpuservice.storage.add': 'ストレージを追加',
|
||||||
'gpuservice.storage.edit': 'ストレージを編集',
|
'gpuservice.storage.edit': 'ストレージを編集',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
'gpuservice.template.imagePullPolicy.never': 'Никогда не загружать',
|
'gpuservice.template.imagePullPolicy.never': 'Никогда не загружать',
|
||||||
'gpuservice.template.command': 'Команда запуска контейнера',
|
'gpuservice.template.command': 'Команда запуска контейнера',
|
||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'Один аргумент на строку, например:\n/bin/bash\n-c\nyour command',
|
'Разделяйте аргументы пробелами; аргументы с пробелами заключайте в кавычки, например: /bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': 'Путь монтирования',
|
'gpuservice.template.mountPath': 'Путь монтирования',
|
||||||
'gpuservice.template.containerDisk': 'Диск контейнера (Gi)',
|
'gpuservice.template.containerDisk': 'Диск контейнера (Gi)',
|
||||||
'gpuservice.template.memory': 'Память (Gi)',
|
'gpuservice.template.memory': 'Память (Gi)',
|
||||||
@@ -48,14 +48,16 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max':
|
||||||
'Текущий тип экземпляра поддерживает максимум {count} GPU',
|
'Текущий тип экземпляра поддерживает максимум {count} GPU',
|
||||||
'gpuservice.instance.stock': 'Остаток',
|
'gpuservice.instance.stock': 'Остаток',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'Память',
|
||||||
'gpuservice.instance.ram': 'Память',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.search.type.placeholder':
|
||||||
'Поиск по имени, VRAM, памяти или vCPU',
|
'Поиск по имени, VRAM, памяти или vCPU',
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'Поиск по имени шаблона, образу или пути монтирования',
|
'Поиск по имени шаблона, образу или пути монтирования',
|
||||||
'gpuservice.instance.template.image': 'Образ',
|
'gpuservice.instance.template.image': 'Образ',
|
||||||
'gpuservice.instance.template.mount': 'Монтирование',
|
'gpuservice.instance.template.mount': 'Монтирование',
|
||||||
|
'gpuservice.instance.connect': 'Подключение',
|
||||||
|
'gpuservice.instance.connect.copySshCommand': 'Скопировать команду SSH',
|
||||||
'gpuservice.storage': 'Хранилище',
|
'gpuservice.storage': 'Хранилище',
|
||||||
'gpuservice.storage.add': 'Добавить хранилище',
|
'gpuservice.storage.add': 'Добавить хранилище',
|
||||||
'gpuservice.storage.edit': 'Редактировать хранилище',
|
'gpuservice.storage.edit': 'Редактировать хранилище',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
'gpuservice.template.imagePullPolicy.never': 'Asla çekme',
|
'gpuservice.template.imagePullPolicy.never': 'Asla çekme',
|
||||||
'gpuservice.template.command': 'Konteyner Başlatma Komutu',
|
'gpuservice.template.command': 'Konteyner Başlatma Komutu',
|
||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'Her satırda bir argüman, örn.:\n/bin/bash\n-c\nyour command',
|
'Argümanları boşlukla ayırın; boşluk içeren argümanları tırnak içine alın, örn.: /bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
'gpuservice.template.mountPath': 'Bağlama Yolu',
|
||||||
'gpuservice.template.containerDisk': 'Konteyner Diski (Gi)',
|
'gpuservice.template.containerDisk': 'Konteyner Diski (Gi)',
|
||||||
'gpuservice.template.memory': 'Bellek (Gi)',
|
'gpuservice.template.memory': 'Bellek (Gi)',
|
||||||
@@ -47,14 +47,16 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.max':
|
'gpuservice.instance.gpuCount.max':
|
||||||
'Mevcut örnek türü en fazla {count} GPU destekler',
|
'Mevcut örnek türü en fazla {count} GPU destekler',
|
||||||
'gpuservice.instance.stock': 'Stok',
|
'gpuservice.instance.stock': 'Stok',
|
||||||
'gpuservice.instance.memory': 'VRAM',
|
'gpuservice.instance.memory': 'Bellek',
|
||||||
'gpuservice.instance.ram': 'Bellek',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.search.type.placeholder':
|
'gpuservice.instance.search.type.placeholder':
|
||||||
"Ada, VRAM, belleğe veya vCPU'ya göre ara",
|
"Ada, VRAM, belleğe veya vCPU'ya göre ara",
|
||||||
'gpuservice.instance.search.template.placeholder':
|
'gpuservice.instance.search.template.placeholder':
|
||||||
'Şablon adına, imaja veya bağlama yoluna göre ara',
|
'Şablon adına, imaja veya bağlama yoluna göre ara',
|
||||||
'gpuservice.instance.template.image': 'İmaj',
|
'gpuservice.instance.template.image': 'İmaj',
|
||||||
'gpuservice.instance.template.mount': 'Bağlama',
|
'gpuservice.instance.template.mount': 'Bağlama',
|
||||||
|
'gpuservice.instance.connect': 'Bağlan',
|
||||||
|
'gpuservice.instance.connect.copySshCommand': 'SSH Komutunu Kopyala',
|
||||||
'gpuservice.storage': 'Depolama',
|
'gpuservice.storage': 'Depolama',
|
||||||
'gpuservice.storage.add': 'Depolama Ekle',
|
'gpuservice.storage.add': 'Depolama Ekle',
|
||||||
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
'gpuservice.storage.edit': 'Depolamayı Düzenle',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
'gpuservice.template.imagePullPolicy.never': '从不拉取',
|
'gpuservice.template.imagePullPolicy.never': '从不拉取',
|
||||||
'gpuservice.template.command': '容器启动命令',
|
'gpuservice.template.command': '容器启动命令',
|
||||||
'gpuservice.template.command.placeholder':
|
'gpuservice.template.command.placeholder':
|
||||||
'每行一个参数,例如:\n/bin/bash\n-c\nyour command',
|
'使用空格分隔参数;含空格的参数请用引号包裹,例如:/bin/bash -c "echo hello world"',
|
||||||
'gpuservice.template.mountPath': '挂载路径',
|
'gpuservice.template.mountPath': '挂载路径',
|
||||||
'gpuservice.template.containerDisk': '容器磁盘(Gi)',
|
'gpuservice.template.containerDisk': '容器磁盘(Gi)',
|
||||||
'gpuservice.template.memory': '内存(Gi)',
|
'gpuservice.template.memory': '内存(Gi)',
|
||||||
@@ -52,6 +52,8 @@ export default {
|
|||||||
'搜索模板名称、镜像或挂载路径',
|
'搜索模板名称、镜像或挂载路径',
|
||||||
'gpuservice.instance.template.image': '镜像',
|
'gpuservice.instance.template.image': '镜像',
|
||||||
'gpuservice.instance.template.mount': '挂载',
|
'gpuservice.instance.template.mount': '挂载',
|
||||||
|
'gpuservice.instance.connect': '连接',
|
||||||
|
'gpuservice.instance.connect.copySshCommand': '复制 SSH 命令',
|
||||||
'gpuservice.storage': '存储',
|
'gpuservice.storage': '存储',
|
||||||
'gpuservice.storage.add': '添加存储',
|
'gpuservice.storage.add': '添加存储',
|
||||||
'gpuservice.storage.edit': '编辑存储',
|
'gpuservice.storage.edit': '编辑存储',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
import { omitPathParams } from '../../utils';
|
||||||
import { InstanceTypeItem, ListItem } from '../config/types';
|
import { InstanceTypeItem, ListItem } from '../config/types';
|
||||||
|
|
||||||
export const GPU_SERVICE_INSTANCES_API = (params: {
|
export const GPU_SERVICE_INSTANCES_API = (params: {
|
||||||
@@ -19,6 +20,9 @@ export async function queryGPUServiceInstances(
|
|||||||
},
|
},
|
||||||
options?: any
|
options?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<Global.K8sPageResponse<ListItem>>(
|
return request<Global.K8sPageResponse<ListItem>>(
|
||||||
GPU_SERVICE_INSTANCES_API({
|
GPU_SERVICE_INSTANCES_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -26,7 +30,7 @@ export async function queryGPUServiceInstances(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params: omitPathParams(params),
|
||||||
cancelToken: options?.token
|
cancelToken: options?.token
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -40,6 +44,9 @@ export async function createGPUServiceInstance(
|
|||||||
},
|
},
|
||||||
option?: any
|
option?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<ListItem>(
|
return request<ListItem>(
|
||||||
GPU_SERVICE_INSTANCES_API({
|
GPU_SERVICE_INSTANCES_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -62,6 +69,9 @@ export async function updateGPUServiceInstance(
|
|||||||
},
|
},
|
||||||
option?: any
|
option?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<ListItem>(
|
return request<ListItem>(
|
||||||
`${GPU_SERVICE_INSTANCES_API({
|
`${GPU_SERVICE_INSTANCES_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -83,6 +93,9 @@ export async function deleteGPUServiceInstance(
|
|||||||
},
|
},
|
||||||
option?: any
|
option?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request(
|
return request(
|
||||||
`${GPU_SERVICE_INSTANCES_API({
|
`${GPU_SERVICE_INSTANCES_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -101,11 +114,14 @@ export async function queryGPUServiceInstanceTypes(
|
|||||||
params: Global.K8sSearchParams & { clusterID?: number },
|
params: Global.K8sSearchParams & { clusterID?: number },
|
||||||
options?: any
|
options?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<Global.K8sPageResponse<InstanceTypeItem>>(
|
return request<Global.K8sPageResponse<InstanceTypeItem>>(
|
||||||
GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID }),
|
GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID }),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params: omitPathParams(params),
|
||||||
cancelToken: options?.token
|
cancelToken: options?.token
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -118,11 +134,14 @@ export async function queryGPUServiceInstanceTypeItems(
|
|||||||
},
|
},
|
||||||
options?: any
|
options?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<InstanceTypeItem>(
|
return request<InstanceTypeItem>(
|
||||||
`${GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID })}/${params.name}`,
|
`${GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID })}/${params.name}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params: omitPathParams(params),
|
||||||
cancelToken: options?.token
|
cancelToken: options?.token
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PageAction } from '@/config';
|
||||||
import { PageActionType } from '@/config/types';
|
import { PageActionType } from '@/config/types';
|
||||||
import Separator from '@/pages/llmodels/components/separator';
|
import Separator from '@/pages/llmodels/components/separator';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
@@ -61,20 +62,32 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = useRef<any>(null);
|
const form = useRef<any>(null);
|
||||||
|
const autoSelectedRef = useRef(false);
|
||||||
const [selectedInstanceType, setSelectedInstanceType] = useState<string>();
|
const [selectedInstanceType, setSelectedInstanceType] = useState<string>();
|
||||||
const [selectedManufacturer, setSelectedManufacturer] = useState<string>();
|
const [selectedManufacturer, setSelectedManufacturer] = useState<string>();
|
||||||
const [templateId, setTemplateId] = useState<number>();
|
const [templateId, setTemplateId] = useState<number>();
|
||||||
const [instanceKeyword, setInstanceKeyword] = useState('');
|
const [instanceKeyword, setInstanceKeyword] = useState('');
|
||||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||||
|
|
||||||
const { detailData, fetchData } = useQueryInstanceTypes();
|
const {
|
||||||
|
detailData,
|
||||||
|
loading: instanceTypesLoading,
|
||||||
|
fetchData
|
||||||
|
} = useQueryInstanceTypes();
|
||||||
const { detailData: templatesData, fetchData: fetchTemplates } =
|
const { detailData: templatesData, fetchData: fetchTemplates } =
|
||||||
useQueryTemplates();
|
useQueryTemplates();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
autoSelectedRef.current = false;
|
||||||
fetchData({});
|
fetchData({});
|
||||||
fetchTemplates({ page: 1, perPage: 100 });
|
fetchTemplates({ page: 1, perPage: 100 });
|
||||||
|
} else {
|
||||||
|
setSelectedInstanceType(undefined);
|
||||||
|
setSelectedManufacturer(undefined);
|
||||||
|
setTemplateId(undefined);
|
||||||
|
setInstanceKeyword('');
|
||||||
|
setTemplateKeyword('');
|
||||||
}
|
}
|
||||||
}, [open, fetchData, fetchTemplates]);
|
}, [open, fetchData, fetchTemplates]);
|
||||||
|
|
||||||
@@ -119,6 +132,59 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
);
|
);
|
||||||
}, [templateKeyword, manufacturerMatchedTemplates]);
|
}, [templateKeyword, manufacturerMatchedTemplates]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
if (action !== PageAction.CREATE) return;
|
||||||
|
if (autoSelectedRef.current) return;
|
||||||
|
if (!instanceTypeList.length) return;
|
||||||
|
if (!templatesData) return;
|
||||||
|
|
||||||
|
const firstInstanceType = instanceTypeList[0];
|
||||||
|
const manufacturer = firstInstanceType.spec?.manufacturer;
|
||||||
|
const name = firstInstanceType.metadata?.name;
|
||||||
|
const matchedTemplate = manufacturer
|
||||||
|
? templateList.find((t) => t.manufacturer === manufacturer)
|
||||||
|
: templateList[0];
|
||||||
|
|
||||||
|
autoSelectedRef.current = true;
|
||||||
|
setSelectedInstanceType(name);
|
||||||
|
setSelectedManufacturer(manufacturer);
|
||||||
|
if (matchedTemplate) {
|
||||||
|
setTemplateId(matchedTemplate.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyToForm = () => {
|
||||||
|
const currentSpec = form.current?.getFieldsValue()?.spec || {};
|
||||||
|
const updatedSpec = {
|
||||||
|
...currentSpec,
|
||||||
|
type: name,
|
||||||
|
resources: { ...(currentSpec.resources || {}), accelerator: '1' }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (matchedTemplate) {
|
||||||
|
form.current?.setFieldsValue({
|
||||||
|
manufacturer: matchedTemplate.manufacturer,
|
||||||
|
spec: {
|
||||||
|
...updatedSpec,
|
||||||
|
...matchedTemplate.spec,
|
||||||
|
resources: {
|
||||||
|
...(updatedSpec.resources || {}),
|
||||||
|
...(matchedTemplate.spec?.resources || {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.current?.setFieldsValue({ spec: updatedSpec });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (form.current) {
|
||||||
|
applyToForm();
|
||||||
|
} else {
|
||||||
|
queueMicrotask(applyToForm);
|
||||||
|
}
|
||||||
|
}, [open, action, instanceTypeList, templateList, templatesData]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
form.current?.submit();
|
form.current?.submit();
|
||||||
};
|
};
|
||||||
@@ -229,15 +295,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
onChange={(e) => setInstanceKeyword(e.target.value)}
|
onChange={(e) => setInstanceKeyword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{filteredInstanceTypes.length > 0 ? (
|
<InstanceTypeList
|
||||||
<InstanceTypeList
|
value={selectedInstanceType}
|
||||||
value={selectedInstanceType}
|
dataList={filteredInstanceTypes}
|
||||||
dataList={filteredInstanceTypes}
|
loading={instanceTypesLoading}
|
||||||
onChange={handleInstanceTypeChange}
|
onChange={handleInstanceTypeChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
)}
|
|
||||||
</PanelBody>
|
</PanelBody>
|
||||||
</ColumnWrapper>
|
</ColumnWrapper>
|
||||||
<Separator></Separator>
|
<Separator></Separator>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { AutoTooltip, IconFont, StatusTag, ThemeTag } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Flex } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
convertKiToGi,
|
||||||
|
InstanceTypePhaseLabelMap,
|
||||||
|
InstanceTypePhaseStatus
|
||||||
|
} from '../config';
|
||||||
|
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Meta = styled.div`
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--ant-color-text-quaternary);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ant-color-text-quaternary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface InstanceTypeItemProps {
|
||||||
|
item: InstanceTypeItemModel;
|
||||||
|
showStatus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||||
|
item,
|
||||||
|
showStatus = true
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const name = item.metadata?.name;
|
||||||
|
const acceleratable = item.spec?.acceleratable;
|
||||||
|
const product = item.spec?.product;
|
||||||
|
const displayName = product || (!acceleratable ? 'CPU' : name);
|
||||||
|
const manufacturer = item.spec?.manufacturer?.toUpperCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>
|
||||||
|
<Flex gap={8} align="center">
|
||||||
|
<AutoTooltip ghost minWidth={20} maxWidth={180}>
|
||||||
|
{displayName}
|
||||||
|
</AutoTooltip>
|
||||||
|
{acceleratable && manufacturer && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: 'var(--ant-color-text-tertiary)',
|
||||||
|
fontWeight: 400
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeTag color="purple">{manufacturer}</ThemeTag>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{showStatus && (
|
||||||
|
<Flex gap={8} align="center">
|
||||||
|
{item.status?.phase && (
|
||||||
|
<StatusTag
|
||||||
|
statusValue={{
|
||||||
|
status:
|
||||||
|
InstanceTypePhaseStatus[item.status.phase] ?? 'inactive',
|
||||||
|
text:
|
||||||
|
InstanceTypePhaseLabelMap[item.status.phase] ??
|
||||||
|
item.status.phase,
|
||||||
|
message: ''
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Title>
|
||||||
|
<Meta>
|
||||||
|
<span style={{ display: 'flex', height: 15 }}>
|
||||||
|
{acceleratable && (
|
||||||
|
<span className="meta-row">
|
||||||
|
<span className="meta-item">
|
||||||
|
<IconFont type="icon-gpu1" className="meta-icon" />
|
||||||
|
<Flex align="center" gap={4}>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}
|
||||||
|
</span>
|
||||||
|
<span>{convertKiToGi(item.spec?.memory) ?? '-'}</span>
|
||||||
|
</Flex>
|
||||||
|
</span>
|
||||||
|
<span className="dot"></span>
|
||||||
|
<span className="meta-item">
|
||||||
|
<IconFont type="icon-cube" className="meta-icon" />
|
||||||
|
<Flex align="center" gap={4}>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}
|
||||||
|
</span>
|
||||||
|
<span>{item.status?.accelerator?.remaining ?? '-'}</span>
|
||||||
|
</Flex>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="meta-row">
|
||||||
|
<span className="meta-item">
|
||||||
|
<IconFont type="icon-ram-02" className="meta-icon" />
|
||||||
|
<Flex align="center" gap={4}>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}
|
||||||
|
</span>
|
||||||
|
<span>{item.status?.ram?.capacity ?? '-'}</span>
|
||||||
|
</Flex>
|
||||||
|
</span>
|
||||||
|
<span className="dot"></span>
|
||||||
|
<span className="meta-item">
|
||||||
|
<IconFont type="icon-cpu" className="meta-icon" />
|
||||||
|
<Flex align="center" gap={4}>
|
||||||
|
<span>vCPU</span>
|
||||||
|
<span>{item.status?.cpu?.capacity ?? '-'}</span>
|
||||||
|
</Flex>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Meta>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InstanceTypeItem;
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { AutoTooltip, StatusTag, TemplateCard } from '@gpustack/core-ui';
|
import FileSkeleton from '@/pages/llmodels/components/model-source/file-skeleton';
|
||||||
import { useIntl } from '@umijs/max';
|
import { TemplateCard } from '@gpustack/core-ui';
|
||||||
import { Flex, Tag } from 'antd';
|
import { Empty, Spin } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import { InstanceTypePhaseValueMap } from '../config';
|
||||||
InstanceTypePhaseLabelMap,
|
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||||
InstanceTypePhaseStatus,
|
import InstanceTypeItem from './instance-type-item';
|
||||||
InstanceTypePhaseValueMap
|
|
||||||
} from '../config';
|
|
||||||
import { InstanceTypeItem } from '../config/types';
|
|
||||||
|
|
||||||
const TypeGrid = styled.div`
|
const TypeGrid = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -15,53 +13,42 @@ const TypeGrid = styled.div`
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TypeName = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
font-weight: 500;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TypeMeta = styled.div`
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
.meta-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--ant-color-text-quaternary);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface InstanceTypeListProps {
|
interface InstanceTypeListProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (item: InstanceTypeItem) => void;
|
onChange?: (item: InstanceTypeItemModel) => void;
|
||||||
dataList?: InstanceTypeItem[];
|
dataList?: InstanceTypeItemModel[];
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAvailable = (item: InstanceTypeItem) =>
|
const isAvailable = (item: InstanceTypeItemModel) =>
|
||||||
item.status?.phase === InstanceTypePhaseValueMap.Active;
|
item.status?.phase === InstanceTypePhaseValueMap.Active;
|
||||||
|
|
||||||
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
dataList = []
|
dataList = [],
|
||||||
|
loading
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const handleSelect = (item: InstanceTypeItemModel) => {
|
||||||
const handleSelect = (item: InstanceTypeItem) => {
|
|
||||||
if (!isAvailable(item)) return;
|
if (!isAvailable(item)) return;
|
||||||
onChange?.(item);
|
onChange?.(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!dataList.length) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Spin spinning size="middle">
|
||||||
|
<TypeGrid style={{ minHeight: 200 }}>
|
||||||
|
{_.times(6, (index: number) => (
|
||||||
|
<FileSkeleton key={index} counts={3} itemHeight={106} />
|
||||||
|
))}
|
||||||
|
</TypeGrid>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypeGrid>
|
<TypeGrid>
|
||||||
{dataList.map((item) => {
|
{dataList.map((item) => {
|
||||||
@@ -78,58 +65,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => handleSelect(item)}
|
onClick={() => handleSelect(item)}
|
||||||
>
|
>
|
||||||
<TypeName>
|
<InstanceTypeItem item={item} />
|
||||||
<Flex gap={8} align="center">
|
|
||||||
<AutoTooltip ghost minWidth={20}>
|
|
||||||
{name}
|
|
||||||
</AutoTooltip>
|
|
||||||
</Flex>
|
|
||||||
<Flex gap={8} align="center">
|
|
||||||
{item.status?.phase && (
|
|
||||||
<StatusTag
|
|
||||||
statusValue={{
|
|
||||||
status:
|
|
||||||
InstanceTypePhaseStatus[item.status.phase] ??
|
|
||||||
'inactive',
|
|
||||||
text:
|
|
||||||
InstanceTypePhaseLabelMap[item.status.phase] ??
|
|
||||||
item.status.phase,
|
|
||||||
message: ''
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{item.spec?.acceleratable && (
|
|
||||||
<Tag
|
|
||||||
style={{
|
|
||||||
fontWeight: 400,
|
|
||||||
color: 'var(--ant-color-text-tertiary)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
|
|
||||||
{item.status?.accelerator?.remaining ?? '-'}
|
|
||||||
</span>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</TypeName>
|
|
||||||
<TypeMeta>
|
|
||||||
<span style={{ display: 'flex', height: 15 }}>
|
|
||||||
{item.spec?.acceleratable && (
|
|
||||||
<span className="meta-row">
|
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
|
|
||||||
{item.spec?.memory ?? '-'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="meta-row gap-16">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
|
|
||||||
{item.status?.ram?.capacity ?? '-'}
|
|
||||||
</span>
|
|
||||||
<span>vCPU {item.status?.cpu?.capacity ?? '-'}</span>
|
|
||||||
</span>
|
|
||||||
</TypeMeta>
|
|
||||||
</TemplateCard>
|
</TemplateCard>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const status: Record<string, StatusType> = {
|
|||||||
[InstanceStatusValueMap.InitializeFailed]: StatusMaps.error,
|
[InstanceStatusValueMap.InitializeFailed]: StatusMaps.error,
|
||||||
[InstanceStatusValueMap.Initialized]: StatusMaps.transitioning,
|
[InstanceStatusValueMap.Initialized]: StatusMaps.transitioning,
|
||||||
[InstanceStatusValueMap.Preparing]: StatusMaps.transitioning,
|
[InstanceStatusValueMap.Preparing]: StatusMaps.transitioning,
|
||||||
[InstanceStatusValueMap.NotReady]: StatusMaps.warning,
|
[InstanceStatusValueMap.NotReady]: StatusMaps.error,
|
||||||
[InstanceStatusValueMap.Ready]: StatusMaps.success
|
[InstanceStatusValueMap.Ready]: StatusMaps.success
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,13 +90,19 @@ export const StorageModeValueMap = {
|
|||||||
// Constant SSH public key resource name used when SSH is enabled
|
// Constant SSH public key resource name used when SSH is enabled
|
||||||
export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default';
|
export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default';
|
||||||
|
|
||||||
const KI_TO_GI = 1024 * 1024;
|
const GI_DIVISOR: Record<string, number> = {
|
||||||
|
Ki: 1024 * 1024,
|
||||||
|
Mi: 1024,
|
||||||
|
Gi: 1
|
||||||
|
};
|
||||||
|
|
||||||
export const convertKiToGi = (value?: string): string | undefined => {
|
export const convertKiToGi = (value?: string): string | undefined => {
|
||||||
if (!value) return value;
|
if (!value) return value;
|
||||||
const match = /^(-?\d+(?:\.\d+)?)Ki$/.exec(value);
|
const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)$/.exec(value);
|
||||||
if (!match) return value;
|
if (!match) return value;
|
||||||
return `${_.round(Number(match[1]) / KI_TO_GI, 2)}Gi`;
|
const [, num, unit] = match;
|
||||||
|
if (unit === 'Ti') return `${_.round(Number(num), 2)}Ti`;
|
||||||
|
return `${_.round(Number(num) / GI_DIVISOR[unit], 2)}Gi`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformInstanceTypeResource = (
|
export const transformInstanceTypeResource = (
|
||||||
|
|||||||
@@ -82,7 +82,12 @@ export interface ListItem extends Omit<FormData, 'metadata'> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type Quality = `${number}Ki` | `${number}Gi`;
|
type Quality =
|
||||||
|
| `${number}`
|
||||||
|
| `${number}Ki`
|
||||||
|
| `${number}Mi`
|
||||||
|
| `${number}Gi`
|
||||||
|
| `${number}Ti`;
|
||||||
|
|
||||||
export interface InstanceTypeResource {
|
export interface InstanceTypeResource {
|
||||||
capacity?: Quality;
|
capacity?: Quality;
|
||||||
@@ -90,30 +95,42 @@ export interface InstanceTypeResource {
|
|||||||
remaining?: Quality;
|
remaining?: Quality;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InstanceTypeStatus {
|
||||||
|
accelerator: InstanceTypeResource;
|
||||||
|
cpu: InstanceTypeResource;
|
||||||
|
localStorage: InstanceTypeResource;
|
||||||
|
phase: string;
|
||||||
|
phaseMessage?: string;
|
||||||
|
ram: InstanceTypeResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceTypeMetadata {
|
||||||
|
name: string;
|
||||||
|
namespace?: string;
|
||||||
|
uid?: string;
|
||||||
|
resourceVersion?: string;
|
||||||
|
creationTimestamp?: string;
|
||||||
|
generateName?: string;
|
||||||
|
generation?: number;
|
||||||
|
finalizers?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceTypeSpec {
|
||||||
|
acceleratable: boolean;
|
||||||
|
group: string;
|
||||||
|
computeCapability?: string;
|
||||||
|
family?: string;
|
||||||
|
manufacturer?: string;
|
||||||
|
memory?: string;
|
||||||
|
product?: string;
|
||||||
|
sliced?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InstanceTypeItem {
|
export interface InstanceTypeItem {
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
metadata: {
|
|
||||||
name: string;
|
|
||||||
namespace: string;
|
|
||||||
};
|
|
||||||
id: number;
|
id: number;
|
||||||
spec: {
|
metadata: InstanceTypeMetadata;
|
||||||
acceleratable: boolean;
|
spec: InstanceTypeSpec;
|
||||||
computeCapability?: string;
|
status: InstanceTypeStatus;
|
||||||
family?: string;
|
|
||||||
group: string;
|
|
||||||
manufacturer?: string;
|
|
||||||
memory?: string;
|
|
||||||
product?: string;
|
|
||||||
sliced?: number;
|
|
||||||
};
|
|
||||||
status: {
|
|
||||||
accelerator: InstanceTypeResource;
|
|
||||||
cpu: InstanceTypeResource;
|
|
||||||
localStorage: InstanceTypeResource;
|
|
||||||
phase: string;
|
|
||||||
phaseMessage?: string;
|
|
||||||
ram: InstanceTypeResource;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import useGetSshkey from '../../public-keys/services/use-get-sshkey';
|
import useGetSshkey from '../../public-keys/services/use-get-sshkey';
|
||||||
import { DefaultImagePullPolicy } from '../../templates/config';
|
import { DefaultImagePullPolicy } from '../../templates/config';
|
||||||
import TemplateBasicForm from '../../templates/forms/basic';
|
import TemplateBasicForm from '../../templates/forms/basic';
|
||||||
import { DEFAULT_SSH_PUBLIC_KEY_NAME } from '../config';
|
|
||||||
import { FormData, InstancePort, ListItem } from '../config/types';
|
import { FormData, InstancePort, ListItem } from '../config/types';
|
||||||
import Basic from './basic';
|
import Basic from './basic';
|
||||||
import InstanceTypeFormItem from './instance-type';
|
import InstanceTypeFormItem from './instance-type';
|
||||||
@@ -53,19 +52,19 @@ const TABKeysMap = {
|
|||||||
const requiredFields = {
|
const requiredFields = {
|
||||||
[TABKeysMap.BASIC]: {
|
[TABKeysMap.BASIC]: {
|
||||||
sort: 1,
|
sort: 1,
|
||||||
fields: ['name']
|
fields: ['metadata.name']
|
||||||
},
|
},
|
||||||
[TABKeysMap.INSTANCE_TYPE]: {
|
[TABKeysMap.INSTANCE_TYPE]: {
|
||||||
sort: 2,
|
sort: 2,
|
||||||
fields: ['type']
|
fields: ['spec.type', 'spec.resources.accelerator']
|
||||||
},
|
},
|
||||||
[TABKeysMap.TEMPLATE]: {
|
[TABKeysMap.TEMPLATE]: {
|
||||||
sort: 3,
|
sort: 3,
|
||||||
fields: ['image', 'ports']
|
fields: ['spec.image', 'spec.ports', 'spec.env']
|
||||||
},
|
},
|
||||||
[TABKeysMap.STORAGE]: {
|
[TABKeysMap.STORAGE]: {
|
||||||
sort: 4,
|
sort: 4,
|
||||||
fields: ['volume']
|
fields: ['spec.volume.persistent.name', 'spec.volume.ephemeral.capacity']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,7 +142,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
const ports = (Form.useWatch(['spec', 'ports'], form) ||
|
const ports = (Form.useWatch(['spec', 'ports'], form) ||
|
||||||
[]) as InstancePort[];
|
[]) as InstancePort[];
|
||||||
const hasSshPort = useMemo(
|
const hasSshPort = useMemo(
|
||||||
() => ports.some((p) => Number(p?.port) === SSH_PORT),
|
() =>
|
||||||
|
ports.some((p) => Number(p?.port) === SSH_PORT && p.protocol === 'TCP'),
|
||||||
[ports]
|
[ports]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -151,12 +151,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
scrollTabsRef.current?.handleTargetChange(key);
|
scrollTabsRef.current?.handleTargetChange(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handleOnFinishFailed } = useFinishFailed({
|
const { handleOnFinishFailed: rawHandleOnFinishFailed } = useFinishFailed({
|
||||||
requiredFields,
|
requiredFields,
|
||||||
onTargetChange,
|
onTargetChange,
|
||||||
updateActiveKey
|
updateActiveKey
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// useFinishFailed only matches errorFields[].name[0] against `fields`,
|
||||||
|
// but our form fields are nested (e.g. ['spec','type']), so collapse each
|
||||||
|
// error path into a single dotted string before delegating.
|
||||||
|
const handleOnFinishFailed = (errorInfo: any) => {
|
||||||
|
const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({
|
||||||
|
...field,
|
||||||
|
name: [
|
||||||
|
Array.isArray(field.name) ? field.name.join('.') : String(field.name)
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
rawHandleOnFinishFailed({ ...errorInfo, errorFields });
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
@@ -180,43 +193,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
}, [action, currentData, form, open, namespace]);
|
}, [action, currentData, form, open, namespace]);
|
||||||
|
|
||||||
const buildPayload = (values: InstanceFormValues): FormData => {
|
|
||||||
const acceleratorVal = values.spec?.resources?.accelerator;
|
|
||||||
const acceleratorStr =
|
|
||||||
acceleratorVal !== undefined &&
|
|
||||||
acceleratorVal !== null &&
|
|
||||||
(acceleratorVal as unknown) !== ''
|
|
||||||
? String(acceleratorVal)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const spec: FormData['spec'] = {
|
|
||||||
...values.spec,
|
|
||||||
resources: {
|
|
||||||
...values.spec?.resources,
|
|
||||||
accelerator: acceleratorStr
|
|
||||||
}
|
|
||||||
} as FormData['spec'];
|
|
||||||
|
|
||||||
// SSH public key — include only when checkbox is checked AND port 22 is in ports
|
|
||||||
const portsList = (values.spec?.ports || []) as InstancePort[];
|
|
||||||
const has22 = portsList.some((p) => Number(p?.port) === SSH_PORT);
|
|
||||||
if (values.enable_ssh && has22) {
|
|
||||||
spec.sshPublicKey = { name: DEFAULT_SSH_PUBLIC_KEY_NAME };
|
|
||||||
} else {
|
|
||||||
delete (spec as any).sshPublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
name: values.metadata?.name,
|
|
||||||
namespace: values.metadata?.namespace || namespace
|
|
||||||
},
|
|
||||||
spec
|
|
||||||
} as FormData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async (values: InstanceFormValues) => {
|
const handleFinish = async (values: InstanceFormValues) => {
|
||||||
const payload = buildPayload(values);
|
|
||||||
await onFinish(values);
|
await onFinish(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,9 +242,16 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
resources: {
|
resources: {
|
||||||
accelerator: '1'
|
accelerator: '1'
|
||||||
},
|
},
|
||||||
volume: { persistent: { name: '' } },
|
|
||||||
sshPublicKey: {
|
sshPublicKey: {
|
||||||
name: 'gpustack-organization-ssh-public-key'
|
name: 'gpustack-organization-ssh-public-key'
|
||||||
|
},
|
||||||
|
volume: {
|
||||||
|
ephemeral: {
|
||||||
|
capacity: '20Gi'
|
||||||
|
},
|
||||||
|
persistent: {
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enable_ssh: false
|
enable_ssh: false
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { AutoTooltip, InputNumber as CInputNumber } from '@gpustack/core-ui';
|
import { InputNumber as CInputNumber } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Flex, Form, Tag } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import InstanceTypeItem from '../components/instance-type-item';
|
||||||
import { InstanceTypePhaseValueMap } from '../config';
|
import { InstanceTypePhaseValueMap } from '../config';
|
||||||
import { FormData, InstanceTypeItem } from '../config/types';
|
import {
|
||||||
|
FormData,
|
||||||
|
InstanceTypeItem as InstanceTypeItemModel
|
||||||
|
} from '../config/types';
|
||||||
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
import useQueryInstanceTypes from '../services/use-query-instance-types';
|
||||||
|
|
||||||
const FieldBlock = styled.div`
|
const FieldBlock = styled.div`
|
||||||
@@ -12,87 +16,31 @@ const FieldBlock = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const SelectedCard = styled.div`
|
const SelectedCard = styled.div`
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) max-content;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
border-radius: var(--ant-border-radius-lg);
|
border-radius: var(--ant-border-radius-lg);
|
||||||
background: var(--ant-color-bg-container);
|
background: var(--ant-color-bg-container);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SummaryTitle = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
font-weight: 500;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SummaryMeta = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface InstanceTypePickerProps {
|
interface InstanceTypePickerProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
dataList: InstanceTypeItem[];
|
dataList: InstanceTypeItemModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
|
const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
|
||||||
value,
|
value,
|
||||||
dataList
|
dataList
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
|
||||||
const selected = useMemo(() => {
|
const selected = useMemo(() => {
|
||||||
return dataList.find((item) => (item.metadata?.name || '') === value);
|
return dataList.find((item) => (item.metadata?.name || '') === value);
|
||||||
}, [value, dataList]);
|
}, [value, dataList]);
|
||||||
|
|
||||||
|
if (!selected) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SelectedCard>
|
||||||
<SelectedCard>
|
<InstanceTypeItem item={selected} showStatus={false} />
|
||||||
<div>
|
</SelectedCard>
|
||||||
<SummaryTitle>
|
|
||||||
<Flex gap={16} align="center">
|
|
||||||
<AutoTooltip ghost minWidth={20}>
|
|
||||||
{selected?.metadata?.name || '-'}
|
|
||||||
</AutoTooltip>
|
|
||||||
{selected && selected.spec?.acceleratable && (
|
|
||||||
<Tag
|
|
||||||
style={{
|
|
||||||
fontWeight: 400,
|
|
||||||
color: 'var(--ant-color-text-tertiary)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
|
|
||||||
{selected?.status?.accelerator?.remaining ?? '-'}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</SummaryTitle>
|
|
||||||
<SummaryMeta>
|
|
||||||
{selected?.spec?.acceleratable && (
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
|
|
||||||
{selected?.spec?.memory ?? '-'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<Flex gap={16}>
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
|
|
||||||
{selected?.status?.ram?.capacity ?? '-'}
|
|
||||||
</span>
|
|
||||||
<span>vCPU {selected?.status?.cpu?.capacity ?? '-'}</span>
|
|
||||||
</Flex>
|
|
||||||
</SummaryMeta>
|
|
||||||
</div>
|
|
||||||
</SelectedCard>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,8 +62,9 @@ const InstanceTypeFormItem = () => {
|
|||||||
}, [dataList, typeName]);
|
}, [dataList, typeName]);
|
||||||
|
|
||||||
const maxGpuCount = useMemo(() => {
|
const maxGpuCount = useMemo(() => {
|
||||||
const remaining = selectedInstanceType?.status?.accelerator?.remaining;
|
const onceMaxRequest =
|
||||||
const num = remaining ? Number(remaining) : NaN;
|
selectedInstanceType?.status?.accelerator?.onceMaxRequest;
|
||||||
|
const num = onceMaxRequest ? Number(onceMaxRequest) : NaN;
|
||||||
return Number.isFinite(num) && num > 0 ? num : 1;
|
return Number.isFinite(num) && num > 0 ? num : 1;
|
||||||
}, [selectedInstanceType]);
|
}, [selectedInstanceType]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useModel } from '@@/plugin-model';
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
|
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Flex, Form, Radio } from 'antd';
|
import { Button, Flex, Form, Radio } from 'antd';
|
||||||
@@ -8,7 +8,7 @@ import { FormData as StorageFormData } from '../../storage/config/types';
|
|||||||
import useCreateStorage from '../../storage/services/use-create-storage';
|
import useCreateStorage from '../../storage/services/use-create-storage';
|
||||||
import useQueryStorage from '../../storage/services/use-query-storage';
|
import useQueryStorage from '../../storage/services/use-query-storage';
|
||||||
import { StorageModeValueMap } from '../config';
|
import { StorageModeValueMap } from '../config';
|
||||||
import { FormData, InstanceVolume } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
import StorageOverlay from './storage-overlay';
|
import StorageOverlay from './storage-overlay';
|
||||||
|
|
||||||
const FieldBlock = styled.div`
|
const FieldBlock = styled.div`
|
||||||
@@ -19,33 +19,22 @@ const DEFAULT_TEMP_CAPACITY_GB = 50;
|
|||||||
|
|
||||||
const StorageVolume = () => {
|
const StorageVolume = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { initialState } = useModel('@@initialState');
|
|
||||||
const { fetchData: createStorage } = useCreateStorage();
|
const { fetchData: createStorage } = useCreateStorage();
|
||||||
const { detailData: storageData, fetchData: fetchStorage } =
|
const { detailData: storageData, fetchData: fetchStorage } =
|
||||||
useQueryStorage();
|
useQueryStorage();
|
||||||
const [overlayOpen, setOverlayOpen] = useState(false);
|
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||||
const [storageMode, setStorageMode] = useState<string>(
|
const [storageMode, setStorageMode] = useState<string>(
|
||||||
StorageModeValueMap.Existing
|
StorageModeValueMap.Temporary
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const volume = Form.useWatch(['spec', 'volume'], form) as
|
|
||||||
| InstanceVolume
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
const namespace = getCurrentOrganizationId();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStorage({});
|
fetchStorage({});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const storageMode = useMemo(() => {
|
|
||||||
// if (volume?.ephemeral && !volume?.persistent) {
|
|
||||||
// return StorageModeValueMap.Temporary;
|
|
||||||
// }
|
|
||||||
// return StorageModeValueMap.Existing;
|
|
||||||
// }, [volume?.ephemeral, volume?.persistent]);
|
|
||||||
|
|
||||||
const storageOptions = useMemo(
|
const storageOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(storageData?.items || []).map((item) => ({
|
(storageData?.items || []).map((item) => ({
|
||||||
@@ -94,15 +83,15 @@ const StorageVolume = () => {
|
|||||||
value={storageMode}
|
value={storageMode}
|
||||||
onChange={(e) => handleModeChange(e.target.value)}
|
onChange={(e) => handleModeChange(e.target.value)}
|
||||||
options={[
|
options={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: 'gpuservice.storage.temporary' }),
|
||||||
|
value: StorageModeValueMap.Temporary
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: 'gpuservice.storage.persistent'
|
id: 'gpuservice.storage.persistent'
|
||||||
}),
|
}),
|
||||||
value: StorageModeValueMap.Existing
|
value: StorageModeValueMap.Existing
|
||||||
},
|
|
||||||
{
|
|
||||||
label: intl.formatMessage({ id: 'gpuservice.storage.temporary' }),
|
|
||||||
value: StorageModeValueMap.Temporary
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,50 @@
|
|||||||
import { tableSorter } from '@/config/settings';
|
import { ExportOutlined } from '@ant-design/icons';
|
||||||
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
|
import {
|
||||||
|
AutoTooltip,
|
||||||
|
CopyButton,
|
||||||
|
DropdownButtons,
|
||||||
|
StatusTag
|
||||||
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Button, Flex } from 'antd';
|
||||||
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 { InstanceStatusLabelMap, rowActionList, status } from '../config';
|
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
|
type ConnectEntry =
|
||||||
|
| { type: 'ssh'; key: string; command: string }
|
||||||
|
| { type: 'http'; key: string; port: number; url: string };
|
||||||
|
|
||||||
|
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
||||||
|
const ip = record.status?.hostIPs?.[0]?.ip;
|
||||||
|
const ports = record.status?.ports || [];
|
||||||
|
const hasSshKey = !!record.spec?.sshPublicKey?.name;
|
||||||
|
|
||||||
|
if (!ip) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports
|
||||||
|
.filter((p) => p.nodePort)
|
||||||
|
.map<ConnectEntry>((p) => {
|
||||||
|
const isSsh = hasSshKey && p.protocol === 'TCP' && p.port === 22;
|
||||||
|
return isSsh
|
||||||
|
? {
|
||||||
|
type: 'ssh',
|
||||||
|
key: `ssh-${p.nodePort}`,
|
||||||
|
command: `ssh root@${ip} -p ${p.nodePort}`
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: 'http',
|
||||||
|
key: `http-${p.nodePort}`,
|
||||||
|
port: p.port,
|
||||||
|
url: `http://${ip}:${p.nodePort}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
handleSelect: (val: string, record: ListItem) => void;
|
handleSelect: (val: string, record: ListItem) => void;
|
||||||
sortOrder: string[];
|
sortOrder: string[];
|
||||||
@@ -23,7 +61,7 @@ const useInstancesColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
dataIndex: ['metadata', 'name'],
|
dataIndex: ['metadata', 'name'],
|
||||||
key: 'name',
|
key: 'name',
|
||||||
sorter: tableSorter(1),
|
sorter: false,
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
@@ -33,39 +71,101 @@ const useInstancesColumns = ({
|
|||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({ id: 'gpuservice.instance.connect' }),
|
||||||
|
key: 'connect',
|
||||||
|
render: (_text, record: ListItem) => {
|
||||||
|
const entries = getConnectEntries(record);
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap={6} vertical>
|
||||||
|
{entries.map((entry) =>
|
||||||
|
entry.type === 'ssh' ? (
|
||||||
|
<Flex key={entry.key} align="center" style={{ height: 22 }}>
|
||||||
|
<span
|
||||||
|
className="text-tertiary"
|
||||||
|
style={{ fontSize: 12, width: 34, flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
SSH
|
||||||
|
</span>
|
||||||
|
<CopyButton
|
||||||
|
text={entry.command}
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
tips={intl.formatMessage({
|
||||||
|
id: 'gpuservice.instance.connect.copySshCommand'
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
padding: '0 6px',
|
||||||
|
height: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: 'var(--ant-color-fill-tertiary)',
|
||||||
|
fontSize: 12
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Flex key={entry.key} align="center" style={{ height: 22 }}>
|
||||||
|
<span
|
||||||
|
className="text-tertiary"
|
||||||
|
style={{ fontSize: 12, width: 34, flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
HTTP
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
href={entry.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{
|
||||||
|
padding: '0 6px',
|
||||||
|
height: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
backgroundColor: 'var(--ant-color-fill-tertiary)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{entry.port}
|
||||||
|
<ExportOutlined style={{ fontSize: 10, marginLeft: 4 }} />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
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: tableSorter(2),
|
sorter: false,
|
||||||
render: (value: string, record: ListItem) => {
|
render: (value: string, record: ListItem) => {
|
||||||
return (
|
return (
|
||||||
<StatusTag
|
<StatusTag
|
||||||
statusValue={{
|
statusValue={{
|
||||||
status: status[value],
|
status: status[value],
|
||||||
text: InstanceStatusLabelMap[value] || value
|
text: InstanceStatusLabelMap[value] || value,
|
||||||
|
message: record?.status?.phaseMessage || ''
|
||||||
}}
|
}}
|
||||||
></StatusTag>
|
></StatusTag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: intl.formatMessage({ id: 'gpuservice.template.image' }),
|
|
||||||
dataIndex: ['spec', 'image'],
|
|
||||||
key: 'image',
|
|
||||||
sorter: tableSorter(3),
|
|
||||||
ellipsis: {
|
|
||||||
showTitle: false
|
|
||||||
},
|
|
||||||
render: (value: string) => (
|
|
||||||
<AutoTooltip ghost>{value || '-'}</AutoTooltip>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: ['metadata', 'creationTimestamp'],
|
dataIndex: ['metadata', 'creationTimestamp'],
|
||||||
key: 'creationTimestamp',
|
key: 'creationTimestamp',
|
||||||
sorter: tableSorter(5),
|
sorter: false,
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||||
import type { PageActionType } from '@/config/types';
|
import type { PageActionType } from '@/config/types';
|
||||||
import useTableFetch from '@/hooks/use-table-fetch';
|
import useTableFetch from '@/hooks/use-table-fetch';
|
||||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||||
import { useModel } from '@@/plugin-model';
|
|
||||||
import {
|
import {
|
||||||
BaseSelect,
|
BaseSelect,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
@@ -33,8 +33,7 @@ import useUpdateInstance from './services/use-update-instance';
|
|||||||
|
|
||||||
const GPUService: React.FC = () => {
|
const GPUService: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { initialState } = useModel('@@initialState');
|
const namespace = getCurrentOrganizationId();
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
|
||||||
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
@@ -43,12 +42,13 @@ const GPUService: React.FC = () => {
|
|||||||
[namespace, clusterID]
|
[namespace, clusterID]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchInstances = useCallback(
|
const fetchInstances = useMemoizedFn(
|
||||||
async (
|
async (
|
||||||
params: any,
|
params: any,
|
||||||
options?: any
|
options?: any
|
||||||
): Promise<Global.PageResponse<ListItem>> => {
|
): Promise<Global.PageResponse<ListItem>> => {
|
||||||
if (!clusterID) {
|
const effectiveClusterID = params.cluster_id ?? clusterID;
|
||||||
|
if (!effectiveClusterID) {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -60,7 +60,7 @@ const GPUService: React.FC = () => {
|
|||||||
} as Global.PageResponse<ListItem>;
|
} as Global.PageResponse<ListItem>;
|
||||||
}
|
}
|
||||||
const res = await queryGPUServiceInstances(
|
const res = await queryGPUServiceInstances(
|
||||||
{ ...params, namespace, clusterID: params.cluster_id ?? clusterID },
|
{ ...params, namespace, clusterID: effectiveClusterID },
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
const total = res.items?.length ?? 0;
|
const total = res.items?.length ?? 0;
|
||||||
@@ -74,8 +74,7 @@ const GPUService: React.FC = () => {
|
|||||||
perPage
|
perPage
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
[namespace, clusterID]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -96,7 +95,8 @@ const GPUService: React.FC = () => {
|
|||||||
key: PaginationKey.Instances,
|
key: PaginationKey.Instances,
|
||||||
fetchAPI: fetchInstances,
|
fetchAPI: fetchInstances,
|
||||||
deleteAPI: deleteInstance,
|
deleteAPI: deleteInstance,
|
||||||
watch: true,
|
watch: false,
|
||||||
|
polling: true,
|
||||||
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
|
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
|
||||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
|
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
|
||||||
});
|
});
|
||||||
@@ -134,18 +134,24 @@ const GPUService: React.FC = () => {
|
|||||||
const k8sClusters = clusters.filter(
|
const k8sClusters = clusters.filter(
|
||||||
(item: any) => item.provider === ProviderValueMap.Kubernetes
|
(item: any) => item.provider === ProviderValueMap.Kubernetes
|
||||||
);
|
);
|
||||||
if (k8sClusters.length > 0) {
|
if (k8sClusters.length === 0) {
|
||||||
const firstCluster = k8sClusters[0];
|
return;
|
||||||
|
}
|
||||||
|
const storedCluster = k8sClusters.find(
|
||||||
|
(item: any) => item.id === currentCluster?.id
|
||||||
|
);
|
||||||
|
const targetCluster = storedCluster ?? k8sClusters[0];
|
||||||
|
if (!storedCluster) {
|
||||||
setCurrentCluster({
|
setCurrentCluster({
|
||||||
...firstCluster,
|
...targetCluster,
|
||||||
label: firstCluster.name,
|
label: targetCluster.name,
|
||||||
value: firstCluster.id
|
value: targetCluster.id
|
||||||
});
|
|
||||||
handleQueryChange({
|
|
||||||
cluster_id: firstCluster.id,
|
|
||||||
page: 1
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
handleQueryChange({
|
||||||
|
cluster_id: targetCluster.id,
|
||||||
|
page: 1
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelClusterRequest();
|
cancelClusterRequest();
|
||||||
@@ -202,7 +208,11 @@ const GPUService: React.FC = () => {
|
|||||||
if (val === 'edit') {
|
if (val === 'edit') {
|
||||||
handleEditInstance(row);
|
handleEditInstance(row);
|
||||||
} else if (val === 'delete') {
|
} else if (val === 'delete') {
|
||||||
handleDelete({ ...row, name: row.metadata?.name });
|
handleDelete({
|
||||||
|
...row,
|
||||||
|
name: row.metadata?.name,
|
||||||
|
id: row.metadata?.name as any
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +235,7 @@ const GPUService: React.FC = () => {
|
|||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
image={<IconFont type="icon-cloud-outlined" />}
|
image={<IconFont type="icon-cloud-outlined" />}
|
||||||
filters={_.omit(queryParams, ['sort_by'])}
|
filters={_.pick(queryParams, ['search', 'manufacturer'])}
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.gpuservice.instance.nofound'
|
id: 'noresult.gpuservice.instance.nofound'
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { useQueryData } from '@gpustack/core-ui';
|
import { useQueryData } from '@gpustack/core-ui';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -12,8 +12,7 @@ interface CreateInstanceParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useCreateInstance() {
|
export default function useCreateInstance() {
|
||||||
const { initialState } = useModel('@@initialState');
|
const namespace = getCurrentOrganizationId();
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
|
||||||
const currentCluster = useAtomValue(currentClusterAtom);
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { useQueryData } from '@gpustack/core-ui';
|
import { useQueryData } from '@gpustack/core-ui';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -13,8 +13,7 @@ interface UpdateInstanceParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useUpdateInstance() {
|
export default function useUpdateInstance() {
|
||||||
const { initialState } = useModel('@@initialState');
|
const namespace = getCurrentOrganizationId();
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
|
||||||
const currentCluster = useAtomValue(currentClusterAtom);
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { INPUT_WIDTH } from '@/constants';
|
|
||||||
import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui';
|
import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Form } from 'antd';
|
import { Button, Form, message } from 'antd';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PageBox from '../../_components/page-box';
|
import PageBox from '../../_components/page-box';
|
||||||
import useGetSshkey from './services/use-get-sshkey';
|
import useGetSshkey from './services/use-get-sshkey';
|
||||||
@@ -36,6 +35,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
await updateSshkey({
|
await updateSshkey({
|
||||||
data: values
|
data: values
|
||||||
});
|
});
|
||||||
|
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<PageBox>
|
<PageBox>
|
||||||
<Form
|
<Form
|
||||||
style={{ width: INPUT_WIDTH.large }}
|
style={{ width: 700 }}
|
||||||
name="gpuServicePublicKeyForm"
|
name="gpuServicePublicKeyForm"
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
@@ -76,7 +76,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
trim={false}
|
trim={false}
|
||||||
alwaysFocus
|
alwaysFocus
|
||||||
autoSize={{ minRows: 8, maxRows: 16 }}
|
autoSize={{ minRows: 8, maxRows: 16 }}
|
||||||
style={{ width: INPUT_WIDTH.large, minHeight: 186 }}
|
style={{ width: 700, minHeight: 186 }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
import { omitPathParams } from '../../utils';
|
||||||
import { ListItem, StorageClassItem } from '../config/types';
|
import { ListItem, StorageClassItem } from '../config/types';
|
||||||
|
|
||||||
export const GPU_SERVICE_STORAGE_API = (params: {
|
export const GPU_SERVICE_STORAGE_API = (params: {
|
||||||
@@ -17,6 +18,9 @@ export async function queryGPUServiceStorage(
|
|||||||
},
|
},
|
||||||
options?: any
|
options?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<Global.K8sPageResponse<ListItem>>(
|
return request<Global.K8sPageResponse<ListItem>>(
|
||||||
GPU_SERVICE_STORAGE_API({
|
GPU_SERVICE_STORAGE_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -24,7 +28,7 @@ export async function queryGPUServiceStorage(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params: omitPathParams(params),
|
||||||
cancelToken: options?.token
|
cancelToken: options?.token
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -38,6 +42,9 @@ export async function createGPUServiceStorage(
|
|||||||
},
|
},
|
||||||
option?: any
|
option?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<ListItem>(
|
return request<ListItem>(
|
||||||
GPU_SERVICE_STORAGE_API({
|
GPU_SERVICE_STORAGE_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -60,6 +67,9 @@ export async function updateGPUServiceStorage(
|
|||||||
},
|
},
|
||||||
option?: any
|
option?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<ListItem>(
|
return request<ListItem>(
|
||||||
`${GPU_SERVICE_STORAGE_API({
|
`${GPU_SERVICE_STORAGE_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -78,6 +88,9 @@ export async function deleteGPUServiceStorage(params: {
|
|||||||
clusterID?: number;
|
clusterID?: number;
|
||||||
id: number;
|
id: number;
|
||||||
}) {
|
}) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request(
|
return request(
|
||||||
`${GPU_SERVICE_STORAGE_API({
|
`${GPU_SERVICE_STORAGE_API({
|
||||||
namespace: params.namespace,
|
namespace: params.namespace,
|
||||||
@@ -95,11 +108,14 @@ export async function queryStorageClass(
|
|||||||
},
|
},
|
||||||
options?: any
|
options?: any
|
||||||
) {
|
) {
|
||||||
|
if (!params.clusterID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return request<Global.K8sPageResponse<StorageClassItem>>(
|
return request<Global.K8sPageResponse<StorageClassItem>>(
|
||||||
STORAGE_CLASS_API({ clusterID: params.clusterID }),
|
STORAGE_CLASS_API({ clusterID: params.clusterID }),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params: omitPathParams(params),
|
||||||
cancelToken: options?.token
|
cancelToken: options?.token
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
|
import { StatusMaps } from '@/config';
|
||||||
|
import { StatusType } from '@/config/types';
|
||||||
import { icons } from '@gpustack/core-ui';
|
import { icons } from '@gpustack/core-ui';
|
||||||
|
|
||||||
|
export const StoragePhaseValueMap = {
|
||||||
|
Available: 'Available',
|
||||||
|
Unavailable: 'Unavailable',
|
||||||
|
Pending: 'Pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StoragePhaseLabelMap: Record<string, string> = {
|
||||||
|
[StoragePhaseValueMap.Available]: 'Available',
|
||||||
|
[StoragePhaseValueMap.Unavailable]: 'Unavailable',
|
||||||
|
[StoragePhaseValueMap.Pending]: 'Pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const status: Record<string, StatusType> = {
|
||||||
|
[StoragePhaseValueMap.Available]: StatusMaps.success,
|
||||||
|
[StoragePhaseValueMap.Unavailable]: StatusMaps.error,
|
||||||
|
[StoragePhaseValueMap.Pending]: StatusMaps.transitioning
|
||||||
|
};
|
||||||
|
|
||||||
export const StorageTypeValueMap = {
|
export const StorageTypeValueMap = {
|
||||||
Local: 'local',
|
Local: 'local',
|
||||||
Shared: 'shared',
|
Shared: 'shared',
|
||||||
|
|||||||
@@ -32,12 +32,23 @@ export interface Metadata {
|
|||||||
resourceVersion: string;
|
resourceVersion: string;
|
||||||
creationTimestamp: string;
|
creationTimestamp: string;
|
||||||
annotations?: Record<string, string>;
|
annotations?: Record<string, string>;
|
||||||
|
finalizers?: string[];
|
||||||
managedFields?: ManagedField[];
|
managedFields?: ManagedField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListItem extends Omit<FormData, 'metadata'> {
|
export interface ListItem {
|
||||||
id: number;
|
|
||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
|
spec: {
|
||||||
|
type: string;
|
||||||
|
capacity: string;
|
||||||
|
accessMode: AccessModeType;
|
||||||
|
};
|
||||||
|
status: {
|
||||||
|
phase: 'Available' | 'Unavailable' | 'Pending';
|
||||||
|
volume: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StorageClassItem {
|
export interface StorageClassItem {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { tableSorter } from '@/config/settings';
|
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, StorageTypeLabelMap } from '../config';
|
import {
|
||||||
|
rowActionList,
|
||||||
|
status,
|
||||||
|
StoragePhaseLabelMap,
|
||||||
|
StorageTypeLabelMap
|
||||||
|
} from '../config';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
@@ -23,7 +27,7 @@ const useStorageColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
dataIndex: ['metadata', 'name'],
|
dataIndex: ['metadata', 'name'],
|
||||||
key: 'name',
|
key: 'name',
|
||||||
sorter: tableSorter(1),
|
sorter: false,
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
@@ -37,7 +41,7 @@ const useStorageColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'common.table.type' }),
|
title: intl.formatMessage({ id: 'common.table.type' }),
|
||||||
dataIndex: ['spec', 'type'],
|
dataIndex: ['spec', 'type'],
|
||||||
key: 'type',
|
key: 'type',
|
||||||
sorter: tableSorter(2),
|
sorter: false,
|
||||||
render: (value: string) =>
|
render: (value: string) =>
|
||||||
StorageTypeLabelMap[value]
|
StorageTypeLabelMap[value]
|
||||||
? intl.formatMessage({ id: StorageTypeLabelMap[value] })
|
? intl.formatMessage({ id: StorageTypeLabelMap[value] })
|
||||||
@@ -47,21 +51,28 @@ const useStorageColumns = ({
|
|||||||
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
|
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
|
||||||
dataIndex: ['spec', 'capacity'],
|
dataIndex: ['spec', 'capacity'],
|
||||||
key: 'capacity',
|
key: 'capacity',
|
||||||
sorter: tableSorter(3),
|
sorter: false,
|
||||||
render: (value: string) => value ?? '-'
|
render: (value: string) => value ?? '-'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'gpuservice.storage.accessMode' }),
|
title: intl.formatMessage({ id: 'common.table.status' }),
|
||||||
dataIndex: ['spec', 'accessMode'],
|
dataIndex: ['status', 'phase'],
|
||||||
key: 'accessMode',
|
key: 'status',
|
||||||
sorter: tableSorter(4),
|
sorter: false,
|
||||||
render: (value: string) => value || '-'
|
render: (value: string) => (
|
||||||
|
<StatusTag
|
||||||
|
statusValue={{
|
||||||
|
status: status[value],
|
||||||
|
text: StoragePhaseLabelMap[value] || value
|
||||||
|
}}
|
||||||
|
></StatusTag>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
title: intl.formatMessage({ id: 'common.table.createTime' }),
|
||||||
dataIndex: ['metadata', 'creationTimestamp'],
|
dataIndex: ['metadata', 'creationTimestamp'],
|
||||||
key: 'creationTimestamp',
|
key: 'creationTimestamp',
|
||||||
sorter: tableSorter(5),
|
sorter: false,
|
||||||
ellipsis: {
|
ellipsis: {
|
||||||
showTitle: false
|
showTitle: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||||
import type { PageActionType } from '@/config/types';
|
import type { PageActionType } from '@/config/types';
|
||||||
import useTableFetch from '@/hooks/use-table-fetch';
|
import useTableFetch from '@/hooks/use-table-fetch';
|
||||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||||
import { useModel } from '@@/plugin-model';
|
|
||||||
import {
|
import {
|
||||||
BaseSelect,
|
BaseSelect,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
@@ -34,8 +34,7 @@ import useUpdateStorage from './services/use-update-storage';
|
|||||||
|
|
||||||
const GPUServiceStorage: React.FC = () => {
|
const GPUServiceStorage: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { initialState } = useModel('@@initialState');
|
const namespace = getCurrentOrganizationId();
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
|
||||||
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
@@ -49,7 +48,8 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
params: any,
|
params: any,
|
||||||
options?: any
|
options?: any
|
||||||
): Promise<Global.PageResponse<ListItem>> => {
|
): Promise<Global.PageResponse<ListItem>> => {
|
||||||
if (!clusterID) {
|
const effectiveClusterID = params.cluster_id ?? clusterID;
|
||||||
|
if (!effectiveClusterID) {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -61,7 +61,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
} as Global.PageResponse<ListItem>;
|
} as Global.PageResponse<ListItem>;
|
||||||
}
|
}
|
||||||
const res = await queryGPUServiceStorage(
|
const res = await queryGPUServiceStorage(
|
||||||
{ ...params, namespace, clusterID: params.cluster_id ?? clusterID },
|
{ ...params, namespace, clusterID: effectiveClusterID },
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
const total = res.items?.length ?? 0;
|
const total = res.items?.length ?? 0;
|
||||||
@@ -98,6 +98,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
fetchAPI: fetchStorage,
|
fetchAPI: fetchStorage,
|
||||||
deleteAPI: deleteStorage,
|
deleteAPI: deleteStorage,
|
||||||
watch: false,
|
watch: false,
|
||||||
|
polling: true,
|
||||||
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
|
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
|
||||||
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
|
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
|
||||||
});
|
});
|
||||||
@@ -184,7 +185,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
if (openAddModalStatus.action === PageAction.EDIT) {
|
if (openAddModalStatus.action === PageAction.EDIT) {
|
||||||
await updateStorage({
|
await updateStorage({
|
||||||
id: openAddModalStatus.currentData!.id,
|
id: openAddModalStatus.currentData!.metadata?.name as any,
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -203,7 +204,11 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
if (val === 'edit') {
|
if (val === 'edit') {
|
||||||
handleEditStorage(row);
|
handleEditStorage(row);
|
||||||
} else if (val === 'delete') {
|
} else if (val === 'delete') {
|
||||||
handleDelete({ ...row, name: row.metadata?.name });
|
handleDelete({
|
||||||
|
...row,
|
||||||
|
name: row.metadata?.name,
|
||||||
|
id: row.metadata?.name as any
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,7 +231,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
image={<IconFont type="icon-storage-outlined" />}
|
image={<IconFont type="icon-storage-outlined" />}
|
||||||
filters={_.omit(queryParams, ['sort_by'])}
|
filters={_.pick(queryParams, ['search'])}
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.gpuservice.storage.nofound'
|
id: 'noresult.gpuservice.storage.nofound'
|
||||||
})}
|
})}
|
||||||
@@ -296,7 +301,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
sortDirections={TABLE_SORT_DIRECTIONS}
|
sortDirections={TABLE_SORT_DIRECTIONS}
|
||||||
showSorterTooltip={false}
|
showSorterTooltip={false}
|
||||||
rowKey="id"
|
rowKey={(record) => record.metadata?.name as any}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
pagination={{
|
||||||
size: 'middle',
|
size: 'middle',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { useQueryData } from '@gpustack/core-ui';
|
import { useQueryData } from '@gpustack/core-ui';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -12,8 +12,7 @@ interface CreateStorageParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useCreateStorage() {
|
export default function useCreateStorage() {
|
||||||
const { initialState } = useModel('@@initialState');
|
const namespace = getCurrentOrganizationId();
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
|
||||||
const currentCluster = useAtomValue(currentClusterAtom);
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { useQueryData } from '@gpustack/core-ui';
|
import { useQueryData } from '@gpustack/core-ui';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -7,8 +7,7 @@ import { queryGPUServiceStorage } from '../apis';
|
|||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
export default function useQueryStorage() {
|
export default function useQueryStorage() {
|
||||||
const { initialState } = useModel('@@initialState');
|
const namespace = getCurrentOrganizationId();
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
|
||||||
const currentCluster = useAtomValue(currentClusterAtom);
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||||
import { useModel } from '@@/plugin-model';
|
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||||
import { useQueryData } from '@gpustack/core-ui';
|
import { useQueryData } from '@gpustack/core-ui';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -13,8 +13,7 @@ interface UpdateStorageParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useUpdateStorage() {
|
export default function useUpdateStorage() {
|
||||||
const { initialState } = useModel('@@initialState');
|
const namespace = getCurrentOrganizationId();
|
||||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
|
||||||
const currentCluster = useAtomValue(currentClusterAtom);
|
const currentCluster = useAtomValue(currentClusterAtom);
|
||||||
const clusterID = currentCluster?.id;
|
const clusterID = currentCluster?.id;
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,49 @@ export const ImagePullPolicyOptions: {
|
|||||||
|
|
||||||
export const DefaultImagePullPolicy: ImagePullPolicy = 'IfNotPresent';
|
export const DefaultImagePullPolicy: ImagePullPolicy = 'IfNotPresent';
|
||||||
|
|
||||||
|
export const normalizeCommand = (value: string): string[] => {
|
||||||
|
if (!value) return [];
|
||||||
|
const tokens: string[] = [];
|
||||||
|
let current = '';
|
||||||
|
let inQuote: '"' | "'" | null = null;
|
||||||
|
let quoteStart = -1;
|
||||||
|
for (const ch of value) {
|
||||||
|
if (inQuote) {
|
||||||
|
if (ch === inQuote) {
|
||||||
|
inQuote = null;
|
||||||
|
quoteStart = -1;
|
||||||
|
} else {
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
} else if (ch === '"' || ch === "'") {
|
||||||
|
inQuote = ch;
|
||||||
|
quoteStart = current.length;
|
||||||
|
} else if (/\s/.test(ch)) {
|
||||||
|
tokens.push(current);
|
||||||
|
current = '';
|
||||||
|
} else {
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inQuote && quoteStart >= 0) {
|
||||||
|
current =
|
||||||
|
current.slice(0, quoteStart) + inQuote + current.slice(quoteStart);
|
||||||
|
}
|
||||||
|
tokens.push(current);
|
||||||
|
return tokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stringifyCommand = (tokens?: string[]): string => {
|
||||||
|
if (!tokens?.length) return '';
|
||||||
|
return tokens
|
||||||
|
.map((token) => {
|
||||||
|
if (!/\s/.test(token)) return token;
|
||||||
|
if (token.startsWith('"') || token.startsWith("'")) return token;
|
||||||
|
return `"${token}"`;
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
export const templateActions = [
|
export const templateActions = [
|
||||||
{
|
{
|
||||||
label: 'common.button.edit',
|
label: 'common.button.edit',
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import {
|
|||||||
} 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 _ from 'lodash';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { GPUsConfigs } from '../../../resources/config/gpu-driver';
|
import { GPUsConfigs } from '../../../resources/config/gpu-driver';
|
||||||
import { ImagePullPolicyOptions } from '../config';
|
import {
|
||||||
|
ImagePullPolicyOptions,
|
||||||
|
normalizeCommand,
|
||||||
|
stringifyCommand
|
||||||
|
} from '../config';
|
||||||
import { FormData } from '../config/types';
|
import { FormData } from '../config/types';
|
||||||
import Env from './env';
|
import Env from './env';
|
||||||
import Ports from './ports';
|
import Ports from './ports';
|
||||||
@@ -96,8 +99,8 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name={['spec', 'command']}
|
name={['spec', 'command']}
|
||||||
normalize={(value: string) => _.split(value, '\n').map(_.trim)}
|
normalize={normalizeCommand}
|
||||||
getValueProps={(value) => ({ value: _.join(value, '\n') })}
|
getValueProps={(value) => ({ value: stringifyCommand(value) })}
|
||||||
>
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
|
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
|
||||||
|
|||||||
@@ -61,13 +61,20 @@ const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
spec: {
|
spec: {
|
||||||
imagePullPolicy: DefaultImagePullPolicy,
|
imagePullPolicy: DefaultImagePullPolicy,
|
||||||
command: [],
|
command: ['tail', '-f', '/dev/null'],
|
||||||
ports: [
|
ports: [
|
||||||
{
|
{
|
||||||
protocol: 'TCP',
|
protocol: 'TCP',
|
||||||
port: 22
|
port: 22
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
volumeMount: '/workspace',
|
||||||
|
resources: {
|
||||||
|
cpu: '1',
|
||||||
|
ram: '2Gi',
|
||||||
|
localStorage: '15Gi',
|
||||||
|
accelerator: ''
|
||||||
|
},
|
||||||
env: []
|
env: []
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const omitPathParams = <T extends Record<string, any>>(
|
||||||
|
params: T
|
||||||
|
): Omit<T, 'namespace' | 'clusterID'> => {
|
||||||
|
return _.omit(params, [
|
||||||
|
'namespace',
|
||||||
|
'clusterID',
|
||||||
|
'page',
|
||||||
|
'perPage',
|
||||||
|
'cluster_id'
|
||||||
|
]) as Omit<T, 'namespace' | 'clusterID'>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user