diff --git a/src/atoms/clusters.ts b/src/atoms/clusters.ts index 9bfb5be3..cccce836 100644 --- a/src/atoms/clusters.ts +++ b/src/atoms/clusters.ts @@ -60,4 +60,5 @@ export const fromClusterCreationAtom = atom(false); */ export const clusterSessionAtom = atom<{ firstAddWorker: boolean; + firstAddCluster: boolean; } | null>(null); diff --git a/src/components/seal-form/password.tsx b/src/components/seal-form/password.tsx index 14f9fc0b..3d9a5e19 100644 --- a/src/components/seal-form/password.tsx +++ b/src/components/seal-form/password.tsx @@ -64,6 +64,7 @@ const SealPassword: React.FC = (props) => { required={required} description={description} disabled={props.disabled} + labelExtra={props.labelExtra} hasPrefix={!!props.prefix} onClick={handleClickWrapper} > diff --git a/src/config/settings.ts b/src/config/settings.ts index fb76dc40..e5814673 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -2,7 +2,7 @@ export const HEADER_HEIGHT = 56; export const DEFAULT_ENTER_PAGE = { adminForNormal: '/dashboard', - adminForFirst: '/models/deployments', + adminForFirst: '/resources/workers', user: '/models/user-models', login: '/login' }; diff --git a/src/locales/en-US/clusters.ts b/src/locales/en-US/clusters.ts index d5578220..c4ace08d 100644 --- a/src/locales/en-US/clusters.ts +++ b/src/locales/en-US/clusters.ts @@ -80,5 +80,6 @@ Same applies to the /opt/dtk directory.`, 'clusters.addworker.autoDetect': 'Auto-detect', 'clusters.addworker.extraVolume.holder': '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' }; diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index fdfbd0b8..cc62d249 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -261,5 +261,11 @@ export default { 'models.accessControlModal.includeusers': 'Include Users', 'models.table.genericProxy': 'Use the following path prefix, and set the model name in either the X-GPUStack-Model 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.' }; diff --git a/src/locales/en-US/no-result.ts b/src/locales/en-US/no-result.ts index c1e92e65..476d75b8 100644 --- a/src/locales/en-US/no-result.ts +++ b/src/locales/en-US/no-result.ts @@ -33,5 +33,11 @@ export default { 'noresult.keys.nofound': 'No matching API keys found.', 'noresult.catalog.title': 'No Models', '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' }; diff --git a/src/locales/ja-JP/clusters.ts b/src/locales/ja-JP/clusters.ts index bd43e598..bf993677 100644 --- a/src/locales/ja-JP/clusters.ts +++ b/src/locales/ja-JP/clusters.ts @@ -80,7 +80,8 @@ Same applies to the /opt/dtk directory.`, 'clusters.addworker.autoDetect': 'Auto-detect', 'clusters.addworker.extraVolume.holder': '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) ========== @@ -151,5 +152,6 @@ Same applies to the /opt/dtk directory.`, // 65. 'clusters.addworker.notSpecified': 'Not Specified', // 66. 'clusters.addworker.autoDetect': 'Auto-detect', // 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 ========== diff --git a/src/locales/ja-JP/models.ts b/src/locales/ja-JP/models.ts index 740676d7..51612f3e 100644 --- a/src/locales/ja-JP/models.ts +++ b/src/locales/ja-JP/models.ts @@ -261,7 +261,13 @@ export default { 'models.accessControlModal.includeusers': 'Include Users', 'models.table.genericProxy': 'Use the following path prefix, and set the model name in either the X-GPUStack-Model 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) ========== @@ -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.`, // 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.',, -// 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 ========== diff --git a/src/locales/ja-JP/no-result.ts b/src/locales/ja-JP/no-result.ts index c1e92e65..476d75b8 100644 --- a/src/locales/ja-JP/no-result.ts +++ b/src/locales/ja-JP/no-result.ts @@ -33,5 +33,11 @@ export default { 'noresult.keys.nofound': 'No matching API keys found.', 'noresult.catalog.title': 'No Models', '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' }; diff --git a/src/locales/ru-RU/clusters.ts b/src/locales/ru-RU/clusters.ts index c2693f88..0263f4d1 100644 --- a/src/locales/ru-RU/clusters.ts +++ b/src/locales/ru-RU/clusters.ts @@ -36,21 +36,34 @@ export default { 'clusters.create.noImages': 'Нет доступных образов', 'clusters.create.noInstanceTypes': 'Нет доступных типов инстансов', 'clusters.create.noRegions': 'Нет доступных регионов', - 'clusters.workerpool.batchSize.desc': 'Количество воркеров, создаваемых одновременно в пуле воркеров', - 'clusters.create.addworker.tips': 'Пожалуйста, убедитесь, что выполнены предварительные условия для {label} перед выполнением следующей команды.', - 'clusters.create.addCommand.tips': 'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.', - 'cluster.create.checkEnv.tips': 'Используйте следующую команду для проверки готовности окружения', - 'clusters.create.register.tips': 'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.', + 'clusters.workerpool.batchSize.desc': + 'Количество воркеров, создаваемых одновременно в пуле воркеров', + 'clusters.create.addworker.tips': + 'Пожалуйста, убедитесь, что выполнены предварительные условия для {label} перед выполнением следующей команды.', + 'clusters.create.addCommand.tips': + 'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.', + 'cluster.create.checkEnv.tips': + 'Используйте следующую команду для проверки готовности окружения', + 'clusters.create.register.tips': + 'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.', 'cluster.provider.comingsoon': 'Скоро будет', - 'clusters.addworker.nvidiaNotes-01': 'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью hostname -I | xargs -n1.', - 'clusters.addworker.nvidiaNotes-02': 'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.', - 'clusters.addworker.hygonNotes': 'Если директория /opt/hyhal не существует, создайте символическую ссылку на путь установки Hygon: /opt/hyhal. Аналогично для директории /opt/dtk.', - 'clusters.addworker.corexNotes': 'Если директория /lib/modules не существует, создайте символическую ссылку на путь установки Iluvatar: ln -s /path/to/corex /lib/modules. Аналогично для директории /usr/local/corex.', - 'clusters.addworker.metaxNotes': 'Если директория /opt/mxdriver не существует, создайте символическую ссылку на путь установки MetaX: ln -s /path/to/metax /opt/mxdriver. Аналогично для директории /opt/maca.', - 'clusters.addworker.cambriconNotes': 'Если директория /usr/local/neuware не существует, создайте символическую ссылку на путь установки Cambricon: ln -s /path/to/neuware /usr/local/neuware.', - 'clusters.addworker.hygonNotes-02': 'Если не удается обнаружить устройства, попробуйте удалить --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.', + 'clusters.addworker.nvidiaNotes-01': + 'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью hostname -I | xargs -n1.', + 'clusters.addworker.nvidiaNotes-02': + 'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.', + 'clusters.addworker.hygonNotes': + 'Если директория /opt/hyhal не существует, создайте символическую ссылку на путь установки Hygon: /opt/hyhal. Аналогично для директории /opt/dtk.', + 'clusters.addworker.corexNotes': + 'Если директория /lib/modules не существует, создайте символическую ссылку на путь установки Iluvatar: ln -s /path/to/corex /lib/modules. Аналогично для директории /usr/local/corex.', + 'clusters.addworker.metaxNotes': + 'Если директория /opt/mxdriver не существует, создайте символическую ссылку на путь установки MetaX: ln -s /path/to/metax /opt/mxdriver. Аналогично для директории /opt/maca.', + 'clusters.addworker.cambriconNotes': + 'Если директория /usr/local/neuware не существует, создайте символическую ссылку на путь установки Cambricon: ln -s /path/to/neuware /usr/local/neuware.', + 'clusters.addworker.hygonNotes-02': + 'Если не удается обнаружить устройства, попробуйте удалить --env ROCM_SMI_LIB_PATH=/opt/hyhal/lib.', 'clusters.addworker.selectCluster': 'Выбрать кластер', - 'clusters.addworker.selectCluster.tips': 'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.', + 'clusters.addworker.selectCluster.tips': + 'Для не-Docker кластеров, пожалуйста, регистрируйте кластеры или управляйте пулами воркеров на странице Кластеры.', 'clusters.addworker.selectGPU': 'Выбрать производителя GPU', 'clusters.addworker.checkEnv': 'Проверить окружение', 'clusters.addworker.specifyArgs': 'Указать аргументы', @@ -65,6 +78,8 @@ export default { 'clusters.addworker.workerIP': 'IP воркера', 'clusters.addworker.notSpecified': 'Не указано', 'clusters.addworker.autoDetect': 'Автоопределение', - 'clusters.addworker.extraVolume.holder': 'напр. /data/models (путь должен начинаться с /)', - 'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}' + 'clusters.addworker.extraVolume.holder': + 'напр. /data/models (путь должен начинаться с /)', + 'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}', + 'clusters.button.genToken': 'Сгенерировать токен' }; diff --git a/src/locales/ru-RU/models.ts b/src/locales/ru-RU/models.ts index ed09f113..98f83408 100644 --- a/src/locales/ru-RU/models.ts +++ b/src/locales/ru-RU/models.ts @@ -266,5 +266,17 @@ export default { 'models.accessControlModal.includeusers': 'Включить пользователей', 'models.table.genericProxy': 'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса X-GPUStack-Model, либо в поле 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 ========== diff --git a/src/locales/ru-RU/no-result.ts b/src/locales/ru-RU/no-result.ts index 9d58c267..ed8ec86e 100644 --- a/src/locales/ru-RU/no-result.ts +++ b/src/locales/ru-RU/no-result.ts @@ -1,10 +1,12 @@ export default { 'noresult.button.add': 'Добавить сейчас', 'noresult.mymodels.title': 'Нет доступных моделей', - 'noresult.mymodels.subTitle': 'Обратитесь к администратору для получения доступа.', + 'noresult.mymodels.subTitle': + 'Обратитесь к администратору для получения доступа.', 'noresult.mymodels.nofound': 'Подходящие модели не найдены', 'noresult.deployments.title': 'Нет развернутых моделей', - 'noresult.deployments.subTitle': 'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.', + 'noresult.deployments.subTitle': + 'Вы еще не развернули ни одной модели. Нажмите кнопку ниже, чтобы начать.', 'noresult.gpus.title': 'GPU устройства не обнаружены', 'noresult.gpus.subTitle': 'Проверьте, что статус Worker READY.', 'noresult.gpus.nofound': 'Подходящие GPU устройства не найдены.', @@ -32,5 +34,18 @@ export default { 'noresult.keys.nofound': 'Подходящие API-ключи не найдены.', 'noresult.catalog.title': 'Нет моделей', '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' +// ======================================================================== diff --git a/src/locales/zh-CN/clusters.ts b/src/locales/zh-CN/clusters.ts index 60538173..ea79fc91 100644 --- a/src/locales/zh-CN/clusters.ts +++ b/src/locales/zh-CN/clusters.ts @@ -78,5 +78,6 @@ export default { 'clusters.addworker.autoDetect': '自动检测', 'clusters.addworker.extraVolume.holder': '例如:/data/models(路径需以 / 开头)', - 'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项' + 'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项', + 'clusters.button.genToken': '生成令牌' }; diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index b95db55c..1ebbab93 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -248,5 +248,8 @@ export default { 'models.accessControlModal.includeusers': '显示用户', 'models.table.genericProxy': '使用以下路径前缀,并在请求头 X-GPUStack-Model 或请求体中的 model 字段设置模型名称后访问该模型。该路径的所有子路径请求会被转发到推理后端。', - 'models.form.backendVersion.deprecated': '已弃用' + 'models.form.backendVersion.deprecated': '已弃用', + 'models.accessSettings.public.desc': '任何人无需认证即可访问。', + 'models.accessSettings.authed.tips': '平台内所有已认证用户可访问。', + 'models.accessSettings.allowedUsers.tips': '仅允许选定的特定用户访问。' }; diff --git a/src/locales/zh-CN/no-result.ts b/src/locales/zh-CN/no-result.ts index a9870e62..840ac4a4 100644 --- a/src/locales/zh-CN/no-result.ts +++ b/src/locales/zh-CN/no-result.ts @@ -33,5 +33,9 @@ export default { 'noresult.keys.nofound': '未找到匹配的 API 密钥', 'noresult.catalog.title': '暂无模型', 'noresult.catalog.subTitle': '尚未配置任何模型。', - 'noresult.catalog.nofound': '未找到匹配的模型' + 'noresult.catalog.nofound': '未找到匹配的模型', + 'noresult.resources.cluster': '暂无可用集群,请添加集群以开始使用。', + 'noresult.resources.worker': '暂无可用节点,请添加节点以开始使用。', + 'noresult.resources.gotocluster': '创建 Docker 或其他类型的集群', + 'noresult.resources.gotoworker': '添加节点' }; diff --git a/src/pages/cluster-management/cluster-create.tsx b/src/pages/cluster-management/cluster-create.tsx index 75d7872b..58c0a42a 100644 --- a/src/pages/cluster-management/cluster-create.tsx +++ b/src/pages/cluster-management/cluster-create.tsx @@ -1,7 +1,9 @@ +import { clusterSessionAtom } from '@/atoms/clusters'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import PageBreadcrumb from '@/pages/_components/page-breadcrumb'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; +import { useAtom } from 'jotai'; import _ from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; @@ -39,6 +41,7 @@ const ClusterCreate = () => { const action = (searchParams.get('action') as PageActionType) || PageAction.CREATE; const navigate = useNavigate(); + const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom); const [credentialList, setCredentialList] = useState< Global.BaseOption[] >([]); @@ -253,6 +256,14 @@ const ClusterCreate = () => { }; const handleCancel = () => { + if (clusterSession?.firstAddCluster) { + setClusterSession({ + firstAddCluster: false, + firstAddWorker: false + }); + navigate(`/cluster-management/clusters/list`); + return; + } navigate(-1); }; diff --git a/src/pages/cluster-management/clusters.tsx b/src/pages/cluster-management/clusters.tsx index f5b7663e..e1a66e79 100644 --- a/src/pages/cluster-management/clusters.tsx +++ b/src/pages/cluster-management/clusters.tsx @@ -1,4 +1,4 @@ -import { expandKeysAtom } from '@/atoms/clusters'; +import { clusterSessionAtom, expandKeysAtom } from '@/atoms/clusters'; import DeleteModal from '@/components/delete-modal'; import IconFont from '@/components/icon-font'; import { FilterBar } from '@/components/page-tools'; @@ -33,7 +33,11 @@ import { K8sStepsFromCluter } from './components/add-worker/config'; import PoolRows from './components/pool-rows'; -import { ProviderType, ProviderValueMap } from './config'; +import { + ClusterStatusValueMap, + ProviderType, + ProviderValueMap +} from './config'; import { ClusterListItem, CredentialListItem, @@ -65,6 +69,7 @@ const Clusters: React.FC = () => { }); const { watchDataList: allWorkerPoolList } = useWatchList(WORKER_POOLS_API); const [expandAtom] = useAtom(expandKeysAtom); + const [clusterSession, setClusterSession] = useAtom(clusterSessionAtom); const { handleExpandChange, handleExpandAll, expandedRowKeys } = useExpandedRowKeys(expandAtom); const navigate = useNavigate(); @@ -241,6 +246,27 @@ const Clusters: React.FC = () => { 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 = ( list: any, options: { parent?: any; [key: string]: any } diff --git a/src/pages/cluster-management/components/add-credential.tsx b/src/pages/cluster-management/components/add-credential.tsx index 5157cf07..fe18b7e3 100644 --- a/src/pages/cluster-management/components/add-credential.tsx +++ b/src/pages/cluster-management/components/add-credential.tsx @@ -4,8 +4,9 @@ import SealInput from '@/components/seal-form/seal-input'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import useAppUtils from '@/hooks/use-app-utils'; +import { ExportOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; +import { Form, Typography } from 'antd'; import React, { useEffect } from 'react'; import { ProviderType, ProviderValueMap } from '../config'; import { @@ -100,8 +101,21 @@ const AddModal: React.FC = ({ ]} > + {intl.formatMessage({ id: 'clusters.button.genToken' })}{' '} + + + } > diff --git a/src/pages/llmodels/components/access-control-modal/form.tsx b/src/pages/llmodels/components/access-control-modal/form.tsx index ac9ef96d..bee4d8e5 100644 --- a/src/pages/llmodels/components/access-control-modal/form.tsx +++ b/src/pages/llmodels/components/access-control-modal/form.tsx @@ -1,4 +1,5 @@ import AlertBlockInfo from '@/components/alert-info/block'; +import TooltipList from '@/components/tooltip-list'; import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import TransferInner from '@/pages/_components/transfer'; @@ -29,7 +30,34 @@ import { AccessControlFormData, ListItem } from '../../config/types'; 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` + display: flex; + align-items: center; + gap: 4px; font-weight: 500; margin-block: 8px 12px; font-size: 14px; @@ -263,7 +291,13 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { access_policy: action === PageAction.CREATE ? 'authed' : undefined }} > - + + name="access_policy" noStyle> { id: 'models.table.userSelection.tips' })} > - + name="users"> diff --git a/src/pages/llmodels/components/deploy-modal.tsx b/src/pages/llmodels/components/deploy-modal.tsx index f8022295..f99279c0 100644 --- a/src/pages/llmodels/components/deploy-modal.tsx +++ b/src/pages/llmodels/components/deploy-modal.tsx @@ -110,7 +110,6 @@ const AddModal: FC = (props) => { const { checkOnlyAscendNPU } = useCheckBackend(); const { - handleShowCompatibleAlert, setWarningStatus, handleBackendChangeBefore, cancelEvaluate, diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index e9e681b0..38db9af2 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -429,6 +429,7 @@ const Models: React.FC = ({ data: row }); }, []); + const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => { try { if (val === 'edit') { diff --git a/src/pages/llmodels/forms/basic.tsx b/src/pages/llmodels/forms/basic.tsx index d655ceea..091a5aa1 100644 --- a/src/pages/llmodels/forms/basic.tsx +++ b/src/pages/llmodels/forms/basic.tsx @@ -1,14 +1,16 @@ +import { clusterSessionAtom } from '@/atoms/clusters'; import SealInput from '@/components/seal-form/seal-input'; 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 useAppUtils from '@/hooks/use-app-utils'; import { ClusterStatusLabelMap, ClusterStatusValueMap } from '@/pages/cluster-management/config'; -import { useIntl } from '@umijs/max'; +import { Link, useIntl } from '@umijs/max'; import { Form } from 'antd'; +import { useAtom } from 'jotai'; import { useMemo } from 'react'; import { sourceOptions } from '../config'; import { FormData } from '../config/types'; @@ -28,6 +30,26 @@ interface BasicFormProps { onSourceChange?: (value: string) => void; } +const NotFoundCredentialContent: React.FC = () => { + const [, setFromClusterCreation] = useAtom(clusterSessionAtom); + const intl = useIntl(); + const handleOnClick = () => { + setFromClusterCreation({ + firstAddWorker: false, + firstAddCluster: true + }); + }; + + return ( + + {intl.formatMessage({ id: 'noresult.resources.gotocluster' })} + + ); +}; + const BasicForm: React.FC = (props) => { const { fields = [], @@ -118,6 +140,9 @@ const BasicForm: React.FC = (props) => { > { + } onChange={handleClusterChange} label={intl.formatMessage({ id: 'clusters.title' })} options={clusterOptions} diff --git a/src/pages/llmodels/hooks/index.ts b/src/pages/llmodels/hooks/index.ts index a98605f8..14633641 100644 --- a/src/pages/llmodels/hooks/index.ts +++ b/src/pages/llmodels/hooks/index.ts @@ -173,6 +173,16 @@ export const useCheckCompatibility = () => { const handleEvaluate = async (data: any) => { 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 = createAxiosToken(); setWarningStatus({ @@ -407,6 +417,7 @@ export const useCheckCompatibility = () => { return; } + // when custom backend, and no run_command or image_name, skip evaluate if ( backendOptionsMap.custom === allValues.backend && (!allValues.run_command || !allValues.image_name) diff --git a/src/pages/llmodels/hooks/use-no-resource-result.tsx b/src/pages/llmodels/hooks/use-no-resource-result.tsx new file mode 100644 index 00000000..840b78c5 --- /dev/null +++ b/src/pages/llmodels/hooks/use-no-resource-result.tsx @@ -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; + 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 = ( + } + filters={queryParams} + noFoundText={statusContent.noFoundText} + title={title} + subTitle={statusContent.subTitle} + > + + + ); + return { + noResourceResult + }; +}; + +export default useNoResourceResult; diff --git a/src/pages/playground/index.tsx b/src/pages/playground/index.tsx index 19ab31a9..8301ff25 100644 --- a/src/pages/playground/index.tsx +++ b/src/pages/playground/index.tsx @@ -156,11 +156,15 @@ const Playground: React.FC = () => { activeKey={activeKey} key="view-code-buttons" />, - , + <> + {activeKey === 'chat' && ( + + )} + , ]} > diff --git a/src/pages/resources/components/workers.tsx b/src/pages/resources/components/workers.tsx index 357701e2..6c06bb30 100644 --- a/src/pages/resources/components/workers.tsx +++ b/src/pages/resources/components/workers.tsx @@ -1,5 +1,4 @@ import DeleteModal from '@/components/delete-modal'; -import IconFont from '@/components/icon-font'; import { FilterBar } from '@/components/page-tools'; import useTableFetch from '@/hooks/use-table-fetch'; import PageBox from '@/pages/_components/page-box'; @@ -11,11 +10,11 @@ import { } from '@/pages/cluster-management/config'; import { ClusterListItem } from '@/pages/cluster-management/config/types'; 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 { useMemoizedFn } from 'ahooks'; -import { Button, ConfigProvider, Table, message } from 'antd'; +import { ConfigProvider, Table, message } from 'antd'; import React, { useEffect, useState } from 'react'; -import NoResult from '../../_components/no-result'; import { WORKERS_API, deleteWorker, @@ -191,24 +190,26 @@ const Workers: React.FC = () => { 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) => { if (type !== 'Table') return; - return ( - } - filters={queryParams} - noFoundText={intl.formatMessage({ id: 'noresult.workers.nofound' })} - title={intl.formatMessage({ id: 'noresult.workers.title' })} - subTitle={intl.formatMessage({ id: 'noresult.workers.subTitle' })} - > - - - ); + return noResourceResult; }; const handleClusterChange = (value: number) => {