chore: display available clusters

This commit is contained in:
jialin
2025-09-16 20:32:26 +08:00
parent c9c9401b0b
commit d063dfd652
14 changed files with 121 additions and 39 deletions
+7
View File
@@ -16,3 +16,10 @@ export const getRequestId = () => {
const store = getDefaultStore(); const store = getDefaultStore();
return store.get(requestIdAtom); return store.get(requestIdAtom);
}; };
export const clusterListAtom = atom<
{
label: string;
value: number;
}[]
>([]);
+4 -1
View File
@@ -167,5 +167,8 @@ export default {
'models.form.gpuCount': 'GPUs per Replica', 'models.form.gpuCount': 'GPUs per Replica',
'models.form.gpuType': 'GPU Type', 'models.form.gpuType': 'GPU Type',
'models.form.optimizeLongPrompt': 'Optimize Long Prompt', 'models.form.optimizeLongPrompt': 'Optimize Long Prompt',
'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding' 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding',
'models.form.check.clusterUnavailable': 'Current cluster is unavailable',
'models.form.check.otherClustersAvailable':
'Available clusters: {clusters}. Please switch cluster.'
}; };
+7 -2
View File
@@ -164,11 +164,16 @@ export default {
'models.form.gpuCount': '各レプリカのGPU数', 'models.form.gpuCount': '各レプリカのGPU数',
'models.form.gpuType': 'GPU タイプ', 'models.form.gpuType': 'GPU タイプ',
'models.form.optimizeLongPrompt': '長いプロンプトを最適化', 'models.form.optimizeLongPrompt': '長いプロンプトを最適化',
'models.form.enableSpeculativeDecoding': '推測デコーディングを有効にする' 'models.form.enableSpeculativeDecoding': '推測デコーディングを有効にする',
'models.form.check.clusterUnavailable': 'Current cluster is unavailable',
'models.form.check.otherClustersAvailable':
'Available clusters: {clusters}. Please switch cluster.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'models.ollama.deprecated.title': 'Deprecation Notice', // 1. 'models.ollama.deprecated.title': 'Deprecation Notice',
// 2. 'models.ollama.deprecated.notice': `The Ollama model source has been deprecated as of v0.6.1. For more information, see the <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">related GitHub issue</a>.` // 2. 'models.ollama.deprecated.notice': `The Ollama model source has been deprecated as of v0.6.1. For more information, see the <a href="https://github.com/gpustack/gpustack/issues/1979" target="_blank">related GitHub issue</a>.`
// 3. 'models.backend.mindie.310p':'Ascend 310P only supports FP16, so you need to set --dtype=float16.' // 3. 'models.backend.mindie.310p':'Ascend 310P only supports FP16, so you need to set --dtype=float16.',
// 4. 'models.form.check.clusterUnavailable': 'Current cluster is unavailable',
// 5. 'models.form.check.otherClustersAvailable': 'Available clusters: {clusters}. Please switch cluster.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+7 -2
View File
@@ -166,7 +166,10 @@ export default {
'models.form.gpuCount': 'GPUs per Replica', 'models.form.gpuCount': 'GPUs per Replica',
'models.form.gpuType': 'GPU Type', 'models.form.gpuType': 'GPU Type',
'models.form.optimizeLongPrompt': 'Optimize Long Prompt', 'models.form.optimizeLongPrompt': 'Optimize Long Prompt',
'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding' 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding',
'models.form.check.clusterUnavailable': 'Current cluster is unavailable',
'models.form.check.otherClustersAvailable':
'Available clusters: {clusters}. Please switch cluster.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -176,5 +179,7 @@ export default {
// 4. 'models.form.gpuCount': 'GPUs per Replica', // 4. 'models.form.gpuCount': 'GPUs per Replica',
// 5. 'models.form.gpuType': 'GPU Type', // 5. 'models.form.gpuType': 'GPU Type',
// 6. 'models.form.optimizeLongPrompt': 'Optimize Long Prompt', // 6. 'models.form.optimizeLongPrompt': 'Optimize Long Prompt',
// 7. 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding' // 7. 'models.form.enableSpeculativeDecoding': 'Enable Speculative Decoding',
// 8. 'models.form.check.clusterUnavailable': 'Current cluster is unavailable',
// 9. 'models.form.check.otherClustersAvailable': 'Available clusters: {clusters}. Please switch cluster.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+4 -1
View File
@@ -157,5 +157,8 @@ export default {
'models.form.gpuCount': '每副本 GPU 数量', 'models.form.gpuCount': '每副本 GPU 数量',
'models.form.gpuType': 'GPU 类型', 'models.form.gpuType': 'GPU 类型',
'models.form.optimizeLongPrompt': '优化长提示', 'models.form.optimizeLongPrompt': '优化长提示',
'models.form.enableSpeculativeDecoding': '启用推测解码' 'models.form.enableSpeculativeDecoding': '启用推测解码',
'models.form.check.clusterUnavailable': '当前集群不可用',
'models.form.check.otherClustersAvailable':
'可用的集群有: {clusters}。请切换集群。'
}; };
+19 -19
View File
@@ -376,23 +376,23 @@ export async function evaluationsModelSpec(
}, },
options: { token: any } options: { token: any }
) { ) {
return request<{ results: EvaluateResult[] }>(`${MODEL_EVALUATIONS}`, { const result = await request<{ results: EvaluateResult[] }>(
method: 'POST', `${MODEL_EVALUATIONS}`,
data, {
cancelToken: options?.token method: 'POST',
}); data,
} cancelToken: options?.token
}
);
// export const evaluationsModelSpec = async ( const resultList = result?.results || [];
// data: {
// model_specs: EvaluateSpec[]; return {
// }, results: resultList.map((item) => {
// options: { token: any } return {
// ) => { ...item,
// const response = await fetch(`v1/${MODEL_EVALUATIONS}`, { cluster_id: data.model_specs?.[0]?.cluster_id || undefined
// method: 'POST', };
// headers: { 'Content-Type': 'application/json' }, })
// body: JSON.stringify(data) };
// }); }
// return response.json();
// };
+1 -2
View File
@@ -169,7 +169,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
getGPUOptionList(params); getGPUOptionList(params);
} }
}; };
}, []); });
return ( return (
<FormInnerContext.Provider <FormInnerContext.Provider
@@ -242,7 +242,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
<HuggingFaceForm></HuggingFaceForm> <HuggingFaceForm></HuggingFaceForm>
<LocalPathForm></LocalPathForm> <LocalPathForm></LocalPathForm>
<Form.Item<FormData> <Form.Item<FormData>
name="cluster_id" name="cluster_id"
rules={[ rules={[
@@ -486,6 +486,9 @@ const AddModal: React.FC<AddModalProps> = (props) => {
if (open) { if (open) {
setTimeout(() => { setTimeout(() => {
fetchSpecData(); fetchSpecData();
form.current?.getGPUOptionList?.({
clusterId: initClusterId()
});
}, 100); }, 100);
} }
return () => { return () => {
@@ -477,6 +477,10 @@ const AddModal: FC<AddModalProps> = (props) => {
onSelectModelAfterEvaluate={ onSelectModelAfterEvaluate={
handleOnSelectModelAfterEvaluate handleOnSelectModelAfterEvaluate
} }
clusterId={
form.current?.getFieldValue?.('cluster_id') ||
initClusterId()
}
displayEvaluateStatus={displayEvaluateStatus} displayEvaluateStatus={displayEvaluateStatus}
gpuOptions={[]} gpuOptions={[]}
></SearchModel> ></SearchModel>
@@ -40,19 +40,12 @@ const PaginationMain = styled(Pagination)`
} }
`; `;
const IconFontWrapper = styled.div`
.ant-pagination-item-link {
display: flex !important;
align-items: center;
justify-content: center;
}
`;
interface SearchInputProps { interface SearchInputProps {
hasLinuxWorker?: boolean; hasLinuxWorker?: boolean;
modelSource: string; modelSource: string;
isDownload?: boolean; isDownload?: boolean;
gpuOptions?: any[]; gpuOptions?: any[];
clusterId: number;
setLoadingModel?: (flag: boolean) => void; setLoadingModel?: (flag: boolean) => void;
onSourceChange?: (source: string) => void; onSourceChange?: (source: string) => void;
onSelectModel: (model: any, manul?: boolean) => void; onSelectModel: (model: any, manul?: boolean) => void;
@@ -70,6 +63,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
isDownload, isDownload,
hasLinuxWorker, hasLinuxWorker,
gpuOptions, gpuOptions,
clusterId,
setLoadingModel, setLoadingModel,
onSelectModel, onSelectModel,
onSelectModelAfterEvaluate, onSelectModelAfterEvaluate,
@@ -295,6 +289,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
return { return {
...backendObj, ...backendObj,
cluster_id: clusterId,
source: modelSource, source: modelSource,
...(modelSource === modelSourceMap.huggingface_value ...(modelSource === modelSourceMap.huggingface_value
? { ? {
@@ -256,6 +256,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
useEffect(() => { useEffect(() => {
if (open && formData) { if (open && formData) {
setOriginalFormData(); setOriginalFormData();
getGPUOptionList({ clusterId: formData.cluster_id });
} }
if (!open) { if (!open) {
checkTokenRef.current?.cancel?.(); checkTokenRef.current?.cancel?.();
+8
View File
@@ -192,6 +192,7 @@ export interface CatalogSpec {
export interface EvaluateSpec { export interface EvaluateSpec {
source?: string; source?: string;
cluster_id?: number;
huggingface_repo_id?: string; huggingface_repo_id?: string;
huggingface_filename?: string; huggingface_filename?: string;
ollama_library_model_name?: string; ollama_library_model_name?: string;
@@ -231,4 +232,11 @@ export interface EvaluateResult {
ram: number; ram: number;
vram: number; vram: number;
}; };
cluster_id?: number;
resource_claim_by_cluster_id?: {
[key: number]: {
ram: number;
vram: number;
};
};
} }
+48 -4
View File
@@ -1,8 +1,10 @@
import { clusterListAtom } from '@/atoms/models';
import { createAxiosToken } from '@/hooks/use-chunk-request'; import { createAxiosToken } from '@/hooks/use-chunk-request';
import { queryModelFilesList } from '@/pages/resources/apis'; import { queryModelFilesList } from '@/pages/resources/apis';
import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
import { convertFileSize } from '@/utils'; import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useAtomValue } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { evaluationsModelSpec } from '../apis'; import { evaluationsModelSpec } from '../apis';
@@ -148,6 +150,7 @@ export const useCheckCompatibility = () => {
const requestIdRef = useRef(0); const requestIdRef = useRef(0);
const updateStatusTimer = useRef<any>(null); const updateStatusTimer = useRef<any>(null);
const isLockWarningStatus = useRef<boolean>(false); const isLockWarningStatus = useRef<boolean>(false);
const clusterList = useAtomValue(clusterListAtom);
const [warningStatus, setWarningStatus] = useState<MessageStatus>({ const [warningStatus, setWarningStatus] = useState<MessageStatus>({
show: false, show: false,
title: '', title: '',
@@ -220,10 +223,25 @@ export const useCheckCompatibility = () => {
} }
}; };
const getAvailableClusters = (ids: string[]) => {
const clusterNames: string[] = [];
clusterList.forEach?.((item: { value: number; label: string }) => {
if (ids.includes(item.value.toString())) {
clusterNames.push(item.label);
}
});
return clusterNames.join(', ');
};
const getCurrentCluster = (id: number) => {
const cluster = clusterList.find?.((item) => item.value === id);
return cluster?.label || '';
};
const handleCheckCompatibility = ( const handleCheckCompatibility = (
evaluateResult: EvaluateResult | null evaluateResult: EvaluateResult | null
): MessageStatus => { ): MessageStatus => {
console.log('handleCheckCompatibility', evaluateResult); console.log('handleCheckCompatibility', clusterList, evaluateResult);
if (!evaluateResult) { if (!evaluateResult) {
return { return {
show: false, show: false,
@@ -234,7 +252,8 @@ export const useCheckCompatibility = () => {
compatible, compatible,
compatibility_messages = [], compatibility_messages = [],
scheduling_messages = [], scheduling_messages = [],
resource_claim, resource_claim_by_cluster_id,
cluster_id,
error, error,
error_message error_message
} = evaluateResult || {}; } = evaluateResult || {};
@@ -248,8 +267,18 @@ export const useCheckCompatibility = () => {
}; };
} }
const resourceClaimMap = new Map(
Object.entries(resource_claim_by_cluster_id || {})
);
// current cluster resource claim
const resource_claim = resourceClaimMap.get(`${cluster_id}`);
const hasClaim = !!resource_claim?.ram || !!resource_claim?.vram; const hasClaim = !!resource_claim?.ram || !!resource_claim?.vram;
// current cluster is not available, but other clusters are available
const othersAvailable = !hasClaim && resourceClaimMap.size > 0;
let msgData = { let msgData = {
title: title:
scheduling_messages?.length > 0 scheduling_messages?.length > 0
@@ -260,6 +289,7 @@ export const useCheckCompatibility = () => {
? scheduling_messages ? scheduling_messages
: compatibility_messages?.join(' ') : compatibility_messages?.join(' ')
}; };
if (hasClaim) { if (hasClaim) {
const ram = convertFileSize(resource_claim.ram, 2); const ram = convertFileSize(resource_claim.ram, 2);
const vram = convertFileSize(resource_claim.vram, 2); const vram = convertFileSize(resource_claim.vram, 2);
@@ -274,11 +304,25 @@ export const useCheckCompatibility = () => {
title: intl.formatMessage({ id: 'models.form.check.passed' }), title: intl.formatMessage({ id: 'models.form.check.passed' }),
message: intl.formatMessage({ id: messageId }, { ram, vram }) message: intl.formatMessage({ id: messageId }, { ram, vram })
}; };
} else if (othersAvailable) {
msgData = {
title: intl.formatMessage({
id: 'models.form.check.clusterUnavailable'
}),
message: intl.formatMessage(
{
id: 'models.form.check.otherClustersAvailable'
},
{
clusters: getAvailableClusters(Array.from(resourceClaimMap.keys()))
}
)
};
} }
return { return {
show: !compatible || hasClaim, show: !compatible || hasClaim || othersAvailable,
type: !compatible ? 'warning' : 'success', type: !compatible || othersAvailable ? 'warning' : 'success',
...msgData ...msgData
}; };
}; };
@@ -1,3 +1,4 @@
import { clusterListAtom } from '@/atoms/models';
import { queryClusterList } from '@/pages/cluster-management/apis'; import { queryClusterList } from '@/pages/cluster-management/apis';
import { ClusterListItem } from '@/pages/cluster-management/config/types'; import { ClusterListItem } from '@/pages/cluster-management/config/types';
import { queryWorkersList } from '@/pages/resources/apis'; import { queryWorkersList } from '@/pages/resources/apis';
@@ -6,6 +7,7 @@ import {
WorkerStatusMapValue WorkerStatusMapValue
} from '@/pages/resources/config'; } from '@/pages/resources/config';
import { ListItem as WorkerListItem } from '@/pages/resources/config/types'; import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import { useState } from 'react'; import { useState } from 'react';
import { queryGPUList } from '../apis'; import { queryGPUList } from '../apis';
@@ -188,6 +190,7 @@ export const useGenerateWorkerOptions = () => {
export default function useFormInitialValues() { export default function useFormInitialValues() {
const { getGPUOptionList } = useGenerateGPUOptions(); const { getGPUOptionList } = useGenerateGPUOptions();
const [, setClusterListAtom] = useAtom(clusterListAtom);
const [clusterList, setClusterList] = useState< const [clusterList, setClusterList] = useState<
Global.BaseOption<number, { provider: string; state: string | number }>[] Global.BaseOption<number, { provider: string; state: string | number }>[]
@@ -206,10 +209,12 @@ export default function useFormInitialValues() {
state: item.state state: item.state
})); }));
setClusterList(list); setClusterList(list);
setClusterListAtom(list);
return list; return list;
} catch (error) { } catch (error) {
console.error('Failed to fetch cluster list:', error); console.error('Failed to fetch cluster list:', error);
setClusterList([]); setClusterList([]);
setClusterListAtom([]);
return []; return [];
} }
}; };