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<{
firstAddWorker: boolean;
firstAddCluster: boolean;
} | null>(null);
+1
View File
@@ -64,6 +64,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
required={required}
description={description}
disabled={props.disabled}
labelExtra={props.labelExtra}
hasPrefix={!!props.prefix}
onClick={handleClickWrapper}
>
+1 -1
View File
@@ -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'
};
+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.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'
};
+7 -1
View File
@@ -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 <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.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'
};
+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.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 <span class="bold-text">/opt/dtk</span> 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 ==========
+11 -2
View File
@@ -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 <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) ==========
@@ -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 ==========
+7 -1
View File
@@ -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'
};
+30 -15
View File
@@ -36,21 +36,34 @@ export default {
'clusters.create.noImages': 'Нет доступных образов',
'clusters.create.noInstanceTypes': 'Нет доступных типов инстансов',
'clusters.create.noRegions': 'Нет доступных регионов',
'clusters.workerpool.batchSize.desc': 'Количество воркеров, создаваемых одновременно в пуле воркеров',
'clusters.create.addworker.tips': 'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.',
'clusters.create.addCommand.tips': 'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
'cluster.create.checkEnv.tips': 'Используйте следующую команду для проверки готовности окружения',
'clusters.create.register.tips': 'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.',
'clusters.workerpool.batchSize.desc':
'Количество воркеров, создаваемых одновременно в пуле воркеров',
'clusters.create.addworker.tips':
'Пожалуйста, убедитесь, что выполнены <a href={link} target="_blank">предварительные условия</a> для {label} перед выполнением следующей команды.',
'clusters.create.addCommand.tips':
'На воркере, который необходимо добавить, выполните следующую команду, чтобы присоединить его к кластеру.',
'cluster.create.checkEnv.tips':
'Используйте следующую команду для проверки готовности окружения',
'clusters.create.register.tips':
'На Kubernetes кластере, который необходимо добавить, выполните следующую команду, чтобы присоединить его узлы к кластеру.',
'cluster.provider.comingsoon': 'Скоро будет',
'clusters.addworker.nvidiaNotes-01': 'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.',
'clusters.addworker.nvidiaNotes-02': 'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
'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.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.nvidiaNotes-01':
'Если существует несколько исходящих IP-адресов, укажите тот, который должен использовать воркер. Пожалуйста, перепроверьте с помощью <span class="bold-text">hostname -I | xargs -n1</span>.',
'clusters.addworker.nvidiaNotes-02':
'Если директория с моделями уже существует на воркере, вы можете указать путь для её монтирования.',
'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.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.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': 'Сгенерировать токен'
};
+13 -1
View File
@@ -266,5 +266,17 @@ export default {
'models.accessControlModal.includeusers': 'Включить пользователей',
'models.table.genericProxy':
'Используйте следующий префикс пути и укажите имя модели либо в заголовке запроса <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 {
'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'
// ========================================================================
+2 -1
View File
@@ -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': '生成令牌'
};
+4 -1
View File
@@ -248,5 +248,8 @@ export default {
'models.accessControlModal.includeusers': '显示用户',
'models.table.genericProxy':
'使用以下路径前缀,并在请求头 <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.catalog.title': '暂无模型',
'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 { 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<number, { provider: ProviderType }>[]
>([]);
@@ -253,6 +256,14 @@ const ClusterCreate = () => {
};
const handleCancel = () => {
if (clusterSession?.firstAddCluster) {
setClusterSession({
firstAddCluster: false,
firstAddWorker: false
});
navigate(`/cluster-management/clusters/list`);
return;
}
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 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 }
@@ -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<AddModalProps> = ({
]}
>
<SealInput.Password
label={intl.formatMessage({ id: 'clusters.credential.token' })}
label={intl.formatMessage({
id: 'clusters.credential.token'
})}
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>
</Form.Item>
</>
@@ -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
}}
>
<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>
<Radio.Group
onChange={handleOnPolicyChange}
@@ -307,7 +341,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
id: 'models.table.userSelection.tips'
})}
>
<QuestionCircleOutlined style={{ marginLeft: 4 }} />
<QuestionCircleOutlined />
</Tooltip>
</Label>
<Form.Item<AccessControlFormData> name="users">
@@ -110,7 +110,6 @@ const AddModal: FC<AddModalProps> = (props) => {
const { checkOnlyAscendNPU } = useCheckBackend();
const {
handleShowCompatibleAlert,
setWarningStatus,
handleBackendChangeBefore,
cancelEvaluate,
@@ -429,6 +429,7 @@ const Models: React.FC<ModelsProps> = ({
data: row
});
}, []);
const handleSelect = useMemoizedFn(async (val: any, row: ListItem) => {
try {
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 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 (
<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 {
fields = [],
@@ -118,6 +140,9 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
>
{
<SealSelect
notFoundContent={
<NotFoundCredentialContent></NotFoundCredentialContent>
}
onChange={handleClusterChange}
label={intl.formatMessage({ id: 'clusters.title' })}
options={clusterOptions}
+11
View File
@@ -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)
@@ -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}
key="view-code-buttons"
/>,
<Divider
key="divider"
type="vertical"
style={{ height: 24, marginInline: 16 }}
/>,
<>
{activeKey === 'chat' && (
<Divider
key="divider"
type="vertical"
style={{ height: 24, marginInline: 16 }}
/>
)}
</>,
<ExtraContent key="extra-content" />
]}
>
+20 -19
View File
@@ -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 (
<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>
);
return noResourceResult;
};
const handleClusterChange = (value: number) => {