chore: get started for add cluster

This commit is contained in:
jialin
2025-11-27 11:35:45 +08:00
parent 97601280d1
commit 962df859f1
26 changed files with 385 additions and 64 deletions
+1
View File
@@ -60,4 +60,5 @@ export const fromClusterCreationAtom = atom(false);
*/ */
export const clusterSessionAtom = atom<{ export const clusterSessionAtom = atom<{
firstAddWorker: boolean; firstAddWorker: boolean;
firstAddCluster: boolean;
} | null>(null); } | null>(null);
+1
View File
@@ -64,6 +64,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
required={required} required={required}
description={description} description={description}
disabled={props.disabled} disabled={props.disabled}
labelExtra={props.labelExtra}
hasPrefix={!!props.prefix} hasPrefix={!!props.prefix}
onClick={handleClickWrapper} onClick={handleClickWrapper}
> >
+1 -1
View File
@@ -2,7 +2,7 @@ export const HEADER_HEIGHT = 56;
export const DEFAULT_ENTER_PAGE = { export const DEFAULT_ENTER_PAGE = {
adminForNormal: '/dashboard', adminForNormal: '/dashboard',
adminForFirst: '/models/deployments', adminForFirst: '/resources/workers',
user: '/models/user-models', user: '/models/user-models',
login: '/login' login: '/login'
}; };
+2 -1
View File
@@ -80,5 +80,6 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.autoDetect': 'Auto-detect', 'clusters.addworker.autoDetect': 'Auto-detect',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)', 'e.g. /data/models (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device' 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken': 'Generate Token'
}; };
+7 -1
View File
@@ -261,5 +261,11 @@ export default {
'models.accessControlModal.includeusers': 'Include Users', 'models.accessControlModal.includeusers': 'Include Users',
'models.table.genericProxy': 'models.table.genericProxy':
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.', 'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
'models.form.backendVersion.deprecated': 'Deprecated' 'models.form.backendVersion.deprecated': 'Deprecated',
'models.accessSettings.public.desc':
'Accessible to anyone without authentication.',
'models.accessSettings.authed.tips':
'Accessible to all authenticated platform users.',
'models.accessSettings.allowedUsers.tips':
'Only designated users can access the model.'
}; };
+7 -1
View File
@@ -33,5 +33,11 @@ export default {
'noresult.keys.nofound': 'No matching API keys found.', 'noresult.keys.nofound': 'No matching API keys found.',
'noresult.catalog.title': 'No Models', 'noresult.catalog.title': 'No Models',
'noresult.catalog.subTitle': 'No models have been configured yet.', 'noresult.catalog.subTitle': 'No models have been configured yet.',
'noresult.catalog.nofound': 'No matching models found.' 'noresult.catalog.nofound': 'No matching models found.',
'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.',
'noresult.resources.worker':
'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create a Docker or Other Cluster',
'noresult.resources.gotoworker': 'Add Worker'
}; };
+4 -2
View File
@@ -80,7 +80,8 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.autoDetect': 'Auto-detect', 'clusters.addworker.autoDetect': 'Auto-detect',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)', 'e.g. /data/models (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device' 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken': 'Generate Token'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -151,5 +152,6 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 65. 'clusters.addworker.notSpecified': 'Not Specified', // 65. 'clusters.addworker.notSpecified': 'Not Specified',
// 66. 'clusters.addworker.autoDetect': 'Auto-detect', // 66. 'clusters.addworker.autoDetect': 'Auto-detect',
// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)' // 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)'
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device' // 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
// 69. 'clusters.button.genToken': 'Generate Token'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+11 -2
View File
@@ -261,7 +261,13 @@ export default {
'models.accessControlModal.includeusers': 'Include Users', 'models.accessControlModal.includeusers': 'Include Users',
'models.table.genericProxy': 'models.table.genericProxy':
'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.', 'Use the following path prefix, and set the model name in either the <span class="bold-text">X-GPUStack-Model</span> request header or the model field in the request body. All requests under this path prefix will be forwarded to the inference backend.',
'models.form.backendVersion.deprecated': 'Deprecated' 'models.form.backendVersion.deprecated': 'Deprecated',
'models.accessSettings.public.desc':
'Accessible to anyone without authentication.',
'models.accessSettings.authed.tips':
'Accessible to all authenticated platform users.',
'models.accessSettings.allowedUsers.tips':
'Only designated users can access the model.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -349,5 +355,8 @@ export default {
// 65. 'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`, // 65. 'models.form.partialoffload.tips': `When CPU offloading is enabled, GPUStack will allocate CPU memory if GPU resources are insufficient. You must correctly configure the inference backend to use hybrid CPU+GPU or full CPU inference.`,
// 66. 'models.form.backend.warning': 'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.', // 66. 'models.form.backend.warning': 'The selected backend does not support GGUF models. Please add a backend with GGUF support in the Inference Backend.',
// 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',, // 67. 'models.form.backend.warning.gguf': 'Please ensure that the selected custom backend supports GGUF models.',,
// 68. 'models.form.backendVersion.deprecated': 'Deprecated' // 68. 'models.form.backendVersion.deprecated': 'Deprecated',
// 69. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
// 70. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
// 71.'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+7 -1
View File
@@ -33,5 +33,11 @@ export default {
'noresult.keys.nofound': 'No matching API keys found.', 'noresult.keys.nofound': 'No matching API keys found.',
'noresult.catalog.title': 'No Models', 'noresult.catalog.title': 'No Models',
'noresult.catalog.subTitle': 'No models have been configured yet.', 'noresult.catalog.subTitle': 'No models have been configured yet.',
'noresult.catalog.nofound': 'No matching models found.' 'noresult.catalog.nofound': 'No matching models found.',
'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.',
'noresult.resources.worker':
'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create a Docker or Other Cluster',
'noresult.resources.gotoworker': 'Add Worker'
}; };
+30 -15
View File
@@ -36,21 +36,34 @@ export default {
'clusters.create.noImages': 'Нет доступных образов', 'clusters.create.noImages': 'Нет доступных образов',
'clusters.create.noInstanceTypes': 'Нет доступных типов инстансов', 'clusters.create.noInstanceTypes': 'Нет доступных типов инстансов',
'clusters.create.noRegions': 'Нет доступных регионов', 'clusters.create.noRegions': 'Нет доступных регионов',
'clusters.workerpool.batchSize.desc': 'Количество воркеров, создаваемых одновременно в пуле воркеров', 'clusters.workerpool.batchSize.desc':
'clusters.create.addworker.tips': 'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.', 'Количество воркеров, создаваемых одновременно в пуле воркеров',
'clusters.create.addCommand.tips': 'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.', 'clusters.create.addworker.tips':
'cluster.create.checkEnv.tips': 'Используйте следующую команду для проверки готовности окружения', 'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.',
'clusters.create.register.tips': 'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.', 'clusters.create.addCommand.tips':
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
'cluster.create.checkEnv.tips':
'Используйте следующую команду для проверки готовности окружения',
'clusters.create.register.tips':
'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.',
'cluster.provider.comingsoon': 'Скоро будет', 'cluster.provider.comingsoon': 'Скоро будет',
'clusters.addworker.nvidiaNotes-01': 'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.', 'clusters.addworker.nvidiaNotes-01':
'clusters.addworker.nvidiaNotes-02': 'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.', 'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.',
'clusters.addworker.hygonNotes': 'Если директория <span class="bold-text">/opt/hyhal</span> не существует, создайте символическую ссылку на путь установки Hygon: <span class="bold-text">/opt/hyhal</span>. Аналогично для директории <span class="bold-text">/opt/dtk</span>.', 'clusters.addworker.nvidiaNotes-02':
'clusters.addworker.corexNotes': 'Если директория <span class="bold-text">/lib/modules</span> не существует, создайте символическую ссылку на путь установки Iluvatar: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Аналогично для директории <span class="bold-text">/usr/local/corex</span>.', 'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
'clusters.addworker.metaxNotes': 'Если директория <span class="bold-text">/opt/mxdriver</span> не существует, создайте символическую ссылку на путь установки MetaX: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Аналогично для директории <span class="bold-text">/opt/maca</span>.', 'clusters.addworker.hygonNotes':
'clusters.addworker.cambriconNotes': 'Если директория <span class="bold-text">/usr/local/neuware</span> не существует, создайте символическую ссылку на путь установки Cambricon: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.', 'Если директория <span class="bold-text">/opt/hyhal</span> не существует, создайте символическую ссылку на путь установки Hygon: <span class="bold-text">/opt/hyhal</span>. Аналогично для директории <span class="bold-text">/opt/dtk</span>.',
'clusters.addworker.hygonNotes-02': 'Если не удается обнаружить устройства, попробуйте удалить <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.', 'clusters.addworker.corexNotes':
'Если директория <span class="bold-text">/lib/modules</span> не существует, создайте символическую ссылку на путь установки Iluvatar: <span class="bold-text">ln -s /path/to/corex /lib/modules</span>. Аналогично для директории <span class="bold-text">/usr/local/corex</span>.',
'clusters.addworker.metaxNotes':
'Если директория <span class="bold-text">/opt/mxdriver</span> не существует, создайте символическую ссылку на путь установки MetaX: <span class="bold-text">ln -s /path/to/metax /opt/mxdriver</span>. Аналогично для директории <span class="bold-text">/opt/maca</span>.',
'clusters.addworker.cambriconNotes':
'Если директория <span class="bold-text">/usr/local/neuware</span> не существует, создайте символическую ссылку на путь установки Cambricon: <span class="bold-text">ln -s /path/to/neuware /usr/local/neuware</span>.',
'clusters.addworker.hygonNotes-02':
'Если не удается обнаружить устройства, попробуйте удалить <span class="bold-text">--env ROCM_SMI_LIB_PATH=/opt/hyhal/lib</span>.',
'clusters.addworker.selectCluster': 'Выбрать кластер', 'clusters.addworker.selectCluster': 'Выбрать кластер',
'clusters.addworker.selectCluster.tips': 'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.', 'clusters.addworker.selectCluster.tips':
'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.',
'clusters.addworker.selectGPU': 'Выбрать производителя GPU', 'clusters.addworker.selectGPU': 'Выбрать производителя GPU',
'clusters.addworker.checkEnv': 'Проверить окружение', 'clusters.addworker.checkEnv': 'Проверить окружение',
'clusters.addworker.specifyArgs': 'Указать аргументы', 'clusters.addworker.specifyArgs': 'Указать аргументы',
@@ -65,6 +78,8 @@ export default {
'clusters.addworker.workerIP': 'IP воркера', 'clusters.addworker.workerIP': 'IP воркера',
'clusters.addworker.notSpecified': 'Не указано', 'clusters.addworker.notSpecified': 'Не указано',
'clusters.addworker.autoDetect': 'Автоопределение', 'clusters.addworker.autoDetect': 'Автоопределение',
'clusters.addworker.extraVolume.holder': 'напр. /data/models (путь должен начинаться с /)', 'clusters.addworker.extraVolume.holder':
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}' 'напр. /data/models (путь должен начинаться с /)',
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}',
'clusters.button.genToken': 'Сгенерировать токен'
}; };
+13 -1
View File
@@ -266,5 +266,17 @@ export default {
'models.accessControlModal.includeusers': 'Включить пользователей', 'models.accessControlModal.includeusers': 'Включить пользователей',
'models.table.genericProxy': 'models.table.genericProxy':
'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <span class="bold-text">X-GPUStack-Model</span>, либо в поле model в теле запроса. Все запросы с этим префиксом пути будут перенаправлены в бэкенд вывода.', 'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <span class="bold-text">X-GPUStack-Model</span>, либо в поле model в теле запроса. Все запросы с этим префиксом пути будут перенаправлены в бэкенд вывода.',
'models.form.backendVersion.deprecated': 'Устаревший' 'models.form.backendVersion.deprecated': 'Устаревший',
'models.accessSettings.public.desc':
'Accessible to anyone without authentication.',
'models.accessSettings.authed.tips':
'Accessible to all authenticated platform users.',
'models.accessSettings.allowedUsers.tips':
'Only designated users can access the model.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.accessSettings.public.desc': 'Accessible to anyone without authentication.',
// 2. 'models.accessSettings.authed.tips': 'Accessible to all authenticated platform users.',
// 3. 'models.accessSettings.allowedUsers.tips': 'Only designated users can access the model.'
// ========== End of To-Do List ==========
+18 -3
View File
@@ -1,10 +1,12 @@
export default { export default {
'noresult.button.add': 'Добавить сейчас', 'noresult.button.add': 'Добавить сейчас',
'noresult.mymodels.title': 'Нет доступных моделей', 'noresult.mymodels.title': 'Нет доступных моделей',
'noresult.mymodels.subTitle': 'Обратитесь к администратору для получения доступа.', 'noresult.mymodels.subTitle':
'Обратитесь к администратору для получения доступа.',
'noresult.mymodels.nofound': 'Подходящие модели не найдены', 'noresult.mymodels.nofound': 'Подходящие модели не найдены',
'noresult.deployments.title': 'Нет развернутых моделей', 'noresult.deployments.title': 'Нет развернутых моделей',
'noresult.deployments.subTitle': 'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.', 'noresult.deployments.subTitle':
'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.',
'noresult.gpus.title': 'GPU устройства не обнаружены', 'noresult.gpus.title': 'GPU устройства не обнаружены',
'noresult.gpus.subTitle': 'Проверьте, что статус Worker READY.', 'noresult.gpus.subTitle': 'Проверьте, что статус Worker READY.',
'noresult.gpus.nofound': 'Подходящие GPU устройства не найдены.', 'noresult.gpus.nofound': 'Подходящие GPU устройства не найдены.',
@@ -32,5 +34,18 @@ export default {
'noresult.keys.nofound': 'Подходящие API-ключи не найдены.', 'noresult.keys.nofound': 'Подходящие API-ключи не найдены.',
'noresult.catalog.title': 'Нет моделей', 'noresult.catalog.title': 'Нет моделей',
'noresult.catalog.subTitle': 'Модели еще не настроены.', 'noresult.catalog.subTitle': 'Модели еще не настроены.',
'noresult.catalog.nofound': 'Подходящие модели не найдены.' 'noresult.catalog.nofound': 'Подходящие модели не найдены.',
'noresult.resources.cluster':
'No clusters available. Add a cluster to get started.',
'noresult.resources.worker':
'No workers available. Add a worker to get started.',
'noresult.resources.gotocluster': 'Create a Docker or Other Cluster',
'noresult.resources.gotoworker': 'Add Worker'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'noresult.resources.cluster': 'No clusters available. Add a cluster to get started.',
// 2. 'noresult.resources.worker': 'No workers available. Add a worker to get started.',
// 3. 'noresult.resources.gotocluster': 'Create a Docker or Other Cluster',
// 4. 'noresult.resources.gotoworker': 'Add Worker'
// ========================================================================
+2 -1
View File
@@ -78,5 +78,6 @@ export default {
'clusters.addworker.autoDetect': '自动检测', 'clusters.addworker.autoDetect': '自动检测',
'clusters.addworker.extraVolume.holder': 'clusters.addworker.extraVolume.holder':
'例如:/data/models(路径需以 / 开头)', '例如:/data/models(路径需以 / 开头)',
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项' 'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项',
'clusters.button.genToken': '生成令牌'
}; };
+4 -1
View File
@@ -248,5 +248,8 @@ export default {
'models.accessControlModal.includeusers': '显示用户', 'models.accessControlModal.includeusers': '显示用户',
'models.table.genericProxy': 'models.table.genericProxy':
'使用以下路径前缀,并在请求头 <span class="bold-text">X-GPUStack-Model</span> 或请求体中的 model 字段设置模型名称后访问该模型。该路径的所有子路径请求会被转发到推理后端。', '使用以下路径前缀,并在请求头 <span class="bold-text">X-GPUStack-Model</span> 或请求体中的 model 字段设置模型名称后访问该模型。该路径的所有子路径请求会被转发到推理后端。',
'models.form.backendVersion.deprecated': '已弃用' 'models.form.backendVersion.deprecated': '已弃用',
'models.accessSettings.public.desc': '任何人无需认证即可访问。',
'models.accessSettings.authed.tips': '平台内所有已认证用户可访问。',
'models.accessSettings.allowedUsers.tips': '仅允许选定的特定用户访问。'
}; };
+5 -1
View File
@@ -33,5 +33,9 @@ export default {
'noresult.keys.nofound': '未找到匹配的 API 密钥', 'noresult.keys.nofound': '未找到匹配的 API 密钥',
'noresult.catalog.title': '暂无模型', 'noresult.catalog.title': '暂无模型',
'noresult.catalog.subTitle': '尚未配置任何模型。', 'noresult.catalog.subTitle': '尚未配置任何模型。',
'noresult.catalog.nofound': '未找到匹配的模型' 'noresult.catalog.nofound': '未找到匹配的模型',
'noresult.resources.cluster': '暂无可用集群,请添加集群以开始使用。',
'noresult.resources.worker': '暂无可用节点,请添加节点以开始使用。',
'noresult.resources.gotocluster': '创建 Docker 或其他类型的集群',
'noresult.resources.gotoworker': '添加节点'
}; };
@@ -1,7 +1,9 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import PageBreadcrumb from '@/pages/_components/page-breadcrumb'; import PageBreadcrumb from '@/pages/_components/page-breadcrumb';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -39,6 +41,7 @@ const ClusterCreate = () => {
const action = const action =
(searchParams.get('action') as PageActionType) || PageAction.CREATE; (searchParams.get('action') as PageActionType) || PageAction.CREATE;
const navigate = useNavigate(); const navigate = useNavigate();
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
const [credentialList, setCredentialList] = useState< const [credentialList, setCredentialList] = useState<
Global.BaseOption<number, { provider: ProviderType }>[] Global.BaseOption<number, { provider: ProviderType }>[]
>([]); >([]);
@@ -253,6 +256,14 @@ const ClusterCreate = () => {
}; };
const handleCancel = () => { const handleCancel = () => {
if (clusterSession?.firstAddCluster) {
setClusterSession({
firstAddCluster: false,
firstAddWorker: false
});
navigate(`/cluster-management/clusters/list`);
return;
}
navigate(-1); navigate(-1);
}; };
+28 -2
View File
@@ -1,4 +1,4 @@
import { expandKeysAtom } from '@/atoms/clusters'; import { clusterSessionAtom, expandKeysAtom } from '@/atoms/clusters';
import DeleteModal from '@/components/delete-modal'; import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools'; import { FilterBar } from '@/components/page-tools';
@@ -33,7 +33,11 @@ import {
K8sStepsFromCluter K8sStepsFromCluter
} from './components/add-worker/config'; } from './components/add-worker/config';
import PoolRows from './components/pool-rows'; import PoolRows from './components/pool-rows';
import { ProviderType, ProviderValueMap } from './config'; import {
ClusterStatusValueMap,
ProviderType,
ProviderValueMap
} from './config';
import { import {
ClusterListItem, ClusterListItem,
CredentialListItem, CredentialListItem,
@@ -65,6 +69,7 @@ const Clusters: React.FC = () => {
}); });
const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API); const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API);
const [expandAtom] = useAtom(expandKeysAtom); const [expandAtom] = useAtom(expandKeysAtom);
const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom);
const { handleExpandChange, handleExpandAll, expandedRowKeys } = const { handleExpandChange, handleExpandAll, expandedRowKeys } =
useExpandedRowKeys(expandAtom); useExpandedRowKeys(expandAtom);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -241,6 +246,27 @@ const Clusters: React.FC = () => {
fetchCredentialList(); fetchCredentialList();
}, []); }, []);
useEffect(() => {
if (
clusterSession?.firstAddWorker &&
dataSource.loadend &&
dataSource.dataList?.length > 0
) {
const targetCluster = dataSource.dataList.find(
(cluster) =>
cluster.state === ClusterStatusValueMap.Ready &&
!cluster.workers &&
!cluster.worker_pools?.length
);
if (targetCluster) {
handleAddWorker(targetCluster);
// reset session
setClusterSession(null);
}
}
}, [clusterSession, dataSource.loadend, dataSource.dataList]);
const renderChildren = ( const renderChildren = (
list: any, list: any,
options: { parent?: any; [key: string]: any } options: { parent?: any; [key: string]: any }
@@ -4,8 +4,9 @@ import SealInput from '@/components/seal-form/seal-input';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import { ExportOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form, Typography } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { ProviderType, ProviderValueMap } from '../config'; import { ProviderType, ProviderValueMap } from '../config';
import { import {
@@ -100,8 +101,21 @@ const AddModal: React.FC<AddModalProps> = ({
]} ]}
> >
<SealInput.Password <SealInput.Password
label={intl.formatMessage({ id: 'clusters.credential.token' })} label={intl.formatMessage({
id: 'clusters.credential.token'
})}
required={action === PageAction.CREATE} required={action === PageAction.CREATE}
labelExtra={
<Typography.Link
href="https://cloud.digitalocean.com/account/api/tokens"
target="_blank"
rel="noopener noreferrer"
style={{ marginLeft: 8, lineHeight: 1 }}
>
{intl.formatMessage({ id: 'clusters.button.genToken' })}{' '}
<ExportOutlined style={{ fontSize: 12 }} />
</Typography.Link>
}
></SealInput.Password> ></SealInput.Password>
</Form.Item> </Form.Item>
</> </>
@@ -1,4 +1,5 @@
import AlertBlockInfo from '@/components/alert-info/block'; import AlertBlockInfo from '@/components/alert-info/block';
import TooltipList from '@/components/tooltip-list';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import TransferInner from '@/pages/_components/transfer'; import TransferInner from '@/pages/_components/transfer';
@@ -29,7 +30,34 @@ import { AccessControlFormData, ListItem } from '../../config/types';
type TransferKey = string | number | bigint; type TransferKey = string | number | bigint;
const accessScopeTips = [
{
title: {
text: 'models.accessSettings.authed',
locale: true
},
tips: 'models.accessSettings.authed.tips'
},
{
title: {
text: 'models.accessSettings.allowedUsers',
locale: true
},
tips: 'models.accessSettings.allowedUsers.tips'
},
{
title: {
text: 'models.accessSettings.public',
locale: true
},
tips: 'models.accessSettings.public.desc'
}
];
const Label = styled.div` const Label = styled.div`
display: flex;
align-items: center;
gap: 4px;
font-weight: 500; font-weight: 500;
margin-block: 8px 12px; margin-block: 8px 12px;
font-size: 14px; font-size: 14px;
@@ -263,7 +291,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
access_policy: action === PageAction.CREATE ? 'authed' : undefined access_policy: action === PageAction.CREATE ? 'authed' : undefined
}} }}
> >
<Label>{intl.formatMessage({ id: 'models.table.accessScope' })}</Label> <Label>
{intl.formatMessage({ id: 'models.table.accessScope' })}
<Tooltip title={<TooltipList list={accessScopeTips}></TooltipList>}>
<QuestionCircleOutlined />
</Tooltip>
</Label>
<Form.Item<AccessControlFormData> name="access_policy" noStyle> <Form.Item<AccessControlFormData> name="access_policy" noStyle>
<Radio.Group <Radio.Group
onChange={handleOnPolicyChange} onChange={handleOnPolicyChange}
@@ -307,7 +341,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
id: 'models.table.userSelection.tips' id: 'models.table.userSelection.tips'
})} })}
> >
<QuestionCircleOutlined style={{ marginLeft: 4 }} /> <QuestionCircleOutlined />
</Tooltip> </Tooltip>
</Label> </Label>
<Form.Item<AccessControlFormData> name="users"> <Form.Item<AccessControlFormData> name="users">
@@ -110,7 +110,6 @@ const AddModal: FC<AddModalProps> = (props) => {
const { checkOnlyAscendNPU } = useCheckBackend(); const { checkOnlyAscendNPU } = useCheckBackend();
const { const {
handleShowCompatibleAlert,
setWarningStatus, setWarningStatus,
handleBackendChangeBefore, handleBackendChangeBefore,
cancelEvaluate, cancelEvaluate,
@@ -429,6 +429,7 @@ const Models: React.FC<ModelsProps> = ({
data: row data: row
}); });
}, []); }, []);
const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => { const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => {
try { try {
if (val === 'edit') { if (val === 'edit') {
+27 -2
View File
@@ -1,14 +1,16 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { modelNameReg } from '@/config'; import { modelNameReg, PageAction } from '@/config';
import { GPUSTACK_API_BASE_URL } from '@/config/settings'; import { GPUSTACK_API_BASE_URL } from '@/config/settings';
import useAppUtils from '@/hooks/use-app-utils'; import useAppUtils from '@/hooks/use-app-utils';
import { import {
ClusterStatusLabelMap, ClusterStatusLabelMap,
ClusterStatusValueMap ClusterStatusValueMap
} from '@/pages/cluster-management/config'; } from '@/pages/cluster-management/config';
import { useIntl } from '@umijs/max'; import { Link, useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { useAtom } from 'jotai';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { sourceOptions } from '../config'; import { sourceOptions } from '../config';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
@@ -28,6 +30,26 @@ interface BasicFormProps {
onSourceChange?: (value: string) => void; onSourceChange?: (value: string) => void;
} }
const NotFoundCredentialContent: React.FC = () => {
const [, setFromClusterCreation] = useAtom(clusterSessionAtom);
const intl = useIntl();
const handleOnClick = () => {
setFromClusterCreation({
firstAddWorker: false,
firstAddCluster: true
});
};
return (
<Link
to={`/cluster-management/clusters/create?action=${PageAction.CREATE}`}
onClick={handleOnClick}
>
{intl.formatMessage({ id: 'noresult.resources.gotocluster' })}
</Link>
);
};
const BasicForm: React.FC<BasicFormProps> = (props) => { const BasicForm: React.FC<BasicFormProps> = (props) => {
const { const {
fields = [], fields = [],
@@ -118,6 +140,9 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
> >
{ {
<SealSelect <SealSelect
notFoundContent={
<NotFoundCredentialContent></NotFoundCredentialContent>
}
onChange={handleClusterChange} onChange={handleClusterChange}
label={intl.formatMessage({ id: 'clusters.title' })} label={intl.formatMessage({ id: 'clusters.title' })}
options={clusterOptions} options={clusterOptions}
+11
View File
@@ -173,6 +173,16 @@ export const useCheckCompatibility = () => {
const handleEvaluate = async (data: any) => { const handleEvaluate = async (data: any) => {
try { try {
// when no cluster selected, show warning and prompt user to add cluster first
if (!data.cluster_id) {
setWarningStatus({
show: true,
title: '',
type: 'warning',
message: intl.formatMessage({ id: 'noresult.resources.cluster' })
});
return;
}
checkTokenRef.current?.cancel(); checkTokenRef.current?.cancel();
checkTokenRef.current = createAxiosToken(); checkTokenRef.current = createAxiosToken();
setWarningStatus({ setWarningStatus({
@@ -407,6 +417,7 @@ export const useCheckCompatibility = () => {
return; return;
} }
// when custom backend, and no run_command or image_name, skip evaluate
if ( if (
backendOptionsMap.custom === allValues.backend && backendOptionsMap.custom === allValues.backend &&
(!allValues.run_command || !allValues.image_name) (!allValues.run_command || !allValues.image_name)
@@ -0,0 +1,113 @@
import { clusterSessionAtom } from '@/atoms/clusters';
import IconFont from '@/components/icon-font';
import { PageAction } from '@/config';
import NoResult from '@/pages/_components/no-result';
import { useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button } from 'antd';
import { useAtom } from 'jotai';
import React, { useMemo } from 'react';
const useNoResourceResult = (props: {
iconType: string;
loading?: boolean;
loadend?: boolean;
dataSource?: any[];
queryParams?: Record<string, any>;
title: React.ReactNode;
noClusters?: boolean;
noWorkers?: boolean;
subTitle?: React.ReactNode;
defaultContent?: {
subTitle: string;
noFoundText: string;
buttonText: string;
onClick: () => void;
};
}) => {
const intl = useIntl();
const navigate = useNavigate();
const {
noClusters,
noWorkers,
defaultContent,
loading,
loadend,
dataSource,
queryParams,
iconType,
title,
subTitle
} = props;
const [, setClusterSession] = useAtom(clusterSessionAtom);
const handleClick = useMemoizedFn(() => {
if (noClusters) {
setClusterSession({
firstAddWorker: false,
firstAddCluster: true
});
navigate(
`/cluster-management/clusters/create?action=${PageAction.CREATE}`
);
}
if (noWorkers) {
setClusterSession({
firstAddWorker: true,
firstAddCluster: false
});
navigate(`/cluster-management/clusters/list`);
}
});
const statusContent = useMemo(() => {
if (noClusters) {
return {
subTitle:
subTitle || intl.formatMessage({ id: 'noresult.resources.cluster' }),
noFoundText: defaultContent?.noFoundText || '',
buttonText: intl.formatMessage({
id: 'noresult.resources.gotocluster'
}),
onClick: handleClick
};
}
if (noWorkers) {
return {
subTitle:
subTitle || intl.formatMessage({ id: 'noresult.resources.worker' }),
noFoundText: defaultContent?.noFoundText || '',
buttonText: intl.formatMessage({ id: 'noresult.resources.gotoworker' }),
onClick: handleClick
};
}
return {
...defaultContent
};
}, [noClusters, noWorkers, intl]);
const noResourceResult = (
<NoResult
loading={loading}
loadend={loadend}
dataSource={dataSource}
image={<IconFont type={iconType} />}
filters={queryParams}
noFoundText={statusContent.noFoundText}
title={title}
subTitle={statusContent.subTitle}
>
<Button type="primary" iconPosition="end" onClick={statusContent.onClick}>
{statusContent.buttonText}
</Button>
</NoResult>
);
return {
noResourceResult
};
};
export default useNoResourceResult;
+9 -5
View File
@@ -156,11 +156,15 @@ const Playground: React.FC = () => {
activeKey={activeKey} activeKey={activeKey}
key="view-code-buttons" key="view-code-buttons"
/>, />,
<Divider <>
key="divider" {activeKey === 'chat' && (
type="vertical" <Divider
style={{ height: 24, marginInline: 16 }} key="divider"
/>, type="vertical"
style={{ height: 24, marginInline: 16 }}
/>
)}
</>,
<ExtraContent key="extra-content" /> <ExtraContent key="extra-content" />
]} ]}
> >
+20 -19
View File
@@ -1,5 +1,4 @@
import DeleteModal from '@/components/delete-modal'; import DeleteModal from '@/components/delete-modal';
import IconFont from '@/components/icon-font';
import { FilterBar } from '@/components/page-tools'; import { FilterBar } from '@/components/page-tools';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import PageBox from '@/pages/_components/page-box'; import PageBox from '@/pages/_components/page-box';
@@ -11,11 +10,11 @@ import {
} from '@/pages/cluster-management/config'; } from '@/pages/cluster-management/config';
import { ClusterListItem } from '@/pages/cluster-management/config/types'; import { ClusterListItem } from '@/pages/cluster-management/config/types';
import useAddWorker from '@/pages/cluster-management/hooks/use-add-worker'; import useAddWorker from '@/pages/cluster-management/hooks/use-add-worker';
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, ConfigProvider, Table, message } from 'antd'; import { ConfigProvider, Table, message } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import NoResult from '../../_components/no-result';
import { import {
WORKERS_API, WORKERS_API,
deleteWorker, deleteWorker,
@@ -191,24 +190,26 @@ const Workers: React.FC = () => {
handleAddWorker(currentData as ClusterListItem); handleAddWorker(currentData as ClusterListItem);
}; };
const { noResourceResult } = useNoResourceResult({
loadend: dataSource.loadend,
loading: dataSource.loading,
dataSource: dataSource.dataList,
queryParams: queryParams,
iconType: 'icon-resources',
title: intl.formatMessage({ id: 'noresult.workers.title' }),
noClusters: !clusterData.list.length,
noWorkers: dataSource.dataList.length === 0 && clusterData.list.length > 0,
defaultContent: {
subTitle: intl.formatMessage({ id: 'noresult.workers.subTitle' }),
noFoundText: intl.formatMessage({ id: 'noresult.workers.nofound' }),
buttonText: intl.formatMessage({ id: 'noresult.workers.button.add' }),
onClick: handleOnAddWorker
}
});
const renderEmpty = (type?: string) => { const renderEmpty = (type?: string) => {
if (type !== 'Table') return; if (type !== 'Table') return;
return ( return noResourceResult;
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-resources" />}
filters={queryParams}
noFoundText={intl.formatMessage({ id: 'noresult.workers.nofound' })}
title={intl.formatMessage({ id: 'noresult.workers.title' })}
subTitle={intl.formatMessage({ id: 'noresult.workers.subTitle' })}
>
<Button type="primary" onClick={handleOnAddWorker}>
{intl.formatMessage({ id: 'noresult.workers.button.add' })}
</Button>
</NoResult>
);
}; };
const handleClusterChange = (value: number) => { const handleClusterChange = (value: number) => {