chore: instance creationg UE

This commit is contained in:
jialin
2026-06-14 19:14:18 +08:00
committed by jialin
parent 8c06862705
commit 6f56e614f3
8 changed files with 131 additions and 87 deletions
@@ -14,7 +14,7 @@ export const useQueryClusterList = (options?: { useStateData?: boolean }) => {
const { useStateData = true } = options || {}; const { useStateData = true } = options || {};
const axiosTokenRef = useRef<CancelTokenSource | null>(null); const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const [dataList, setDataList] = useState< const [dataList, setDataList] = useState<
Array<Partial<ClusterListItem> & { label: string; value: number }> Array<ClusterListItem & { label: string; value: number }>
>([]); >([]);
const { const {
@@ -1,7 +1,6 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock'; import useSubmitLock from '@/hooks/use-submit-lock';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory'; import useUserDirectory from '@/pages/gpu-service/hooks/use-user-directory';
import Separator from '@/pages/llmodels/components/separator'; import Separator from '@/pages/llmodels/components/separator';
import { getGPUStackPlugin } from '@/plugins'; import { getGPUStackPlugin } from '@/plugins';
@@ -13,7 +12,7 @@ import {
ModalFooter ModalFooter
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl, useModel } from '@umijs/max'; import { useIntl, useModel } from '@umijs/max';
import { Empty, Input, Typography } from 'antd'; import { Input, Typography } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { ListItem as TemplateItem } from '../../templates/config/types'; import { ListItem as TemplateItem } from '../../templates/config/types';
@@ -31,6 +30,12 @@ type AddModalProps = {
open: boolean; open: boolean;
width?: number | string; width?: number | string;
realAction?: string; realAction?: string;
clusterList?: Array<{
label: string;
value: number;
id: number;
owner_principal_id?: number;
}>;
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
data?: ListItem | null; data?: ListItem | null;
onCancel: () => void; onCancel: () => void;
@@ -78,6 +83,7 @@ const AddModal: React.FC<AddModalProps> = ({
data, data,
onCancel, onCancel,
width, width,
clusterList = [],
realAction realAction
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@@ -100,16 +106,18 @@ const AddModal: React.FC<AddModalProps> = ({
const [instanceKeyword, setInstanceKeyword] = useState(''); const [instanceKeyword, setInstanceKeyword] = useState('');
const [templateKeyword, setTemplateKeyword] = useState(''); const [templateKeyword, setTemplateKeyword] = useState('');
const { loading, guard, run, release } = useSubmitLock(); const { loading, guard, run, release } = useSubmitLock();
const initializedRef = useRef(false); const [initialized, setInitialized] = useState(false);
const { const {
detailData: instanceTypeList, detailData: instanceTypeList,
loading: instanceTypesLoading, loading: instanceTypesLoading,
fetchData fetchData
} = useQueryInstanceTypes(); } = useQueryInstanceTypes();
const { detailData: templatesData, fetchData: fetchTemplates } = const {
useQueryTemplates(); detailData: templatesData,
const { clusterList, fetchClusterList } = useQueryClusterList(); loading: templateLoading,
fetchData: fetchTemplates
} = useQueryTemplates();
// Set by the create-scope picker (admin "All" view) via onScopeChange. // Set by the create-scope picker (admin "All" view) via onScopeChange.
// undefined = no picker (org context) → no client-side scoping. // undefined = no picker (org context) → no client-side scoping.
const [scopeOrgId, setScopeOrgId] = useState<number | null | undefined>( const [scopeOrgId, setScopeOrgId] = useState<number | null | undefined>(
@@ -125,18 +133,13 @@ const AddModal: React.FC<AddModalProps> = ({
// fetched list client-side, so it doesn't rely on the request scope. // fetched list client-side, so it doesn't rely on the request scope.
const filterTypesByOwner = ( const filterTypesByOwner = (
types: InstanceTypeItem[], types: InstanceTypeItem[],
clusters: Array<{
id?: number;
value?: number;
owner_principal_id?: number;
}>,
orgId?: number | null orgId?: number | null
): InstanceTypeItem[] => { ): InstanceTypeItem[] => {
if (orgId == null) return types; if (orgId == null) return types;
const owned = new Set( const owned = new Set(
(clusters || []) (clusterList || [])
.filter((c) => c.owner_principal_id === orgId) .filter((c) => c.owner_principal_id === orgId)
.map((c) => c.id ?? c.value) .map((c) => c.id || c.value)
); );
return types return types
.map((it) => ({ .map((it) => ({
@@ -157,7 +160,7 @@ const AddModal: React.FC<AddModalProps> = ({
}; };
const ownedInstanceTypes = useMemo( const ownedInstanceTypes = useMemo(
() => filterTypesByOwner(instanceTypeList, clusterList as any, scopeOrgId), () => filterTypesByOwner(instanceTypeList, scopeOrgId),
[instanceTypeList, clusterList, scopeOrgId] [instanceTypeList, clusterList, scopeOrgId]
); );
@@ -279,11 +282,6 @@ const AddModal: React.FC<AddModalProps> = ({
const applyAutoSelection = ( const applyAutoSelection = (
instanceTypes: InstanceTypeItem[], instanceTypes: InstanceTypeItem[],
templates: TemplateItem[], templates: TemplateItem[],
clusters?: Array<{
id?: number;
value?: number;
owner_principal_id?: number;
}>,
orgId?: number | null orgId?: number | null
) => { ) => {
// On edit / view, surface the persisted selection in the card list. // On edit / view, surface the persisted selection in the card list.
@@ -303,7 +301,7 @@ const AddModal: React.FC<AddModalProps> = ({
} }
// Scope to clusters the chosen org owns (admin "All" view). // Scope to clusters the chosen org owns (admin "All" view).
const owned = filterTypesByOwner(instanceTypes, clusters || [], orgId); const owned = filterTypesByOwner(instanceTypes, orgId);
// On create, auto-select the first available instance type (clears the // On create, auto-select the first available instance type (clears the
// selection when the chosen org has none). // selection when the chosen org has none).
@@ -317,23 +315,19 @@ const AddModal: React.FC<AddModalProps> = ({
const loadCreateResources = (orgId?: number | null) => { const loadCreateResources = (orgId?: number | null) => {
const session = ++sessionRef.current; const session = ++sessionRef.current;
try { try {
Promise.all([ Promise.all([fetchData({ page: -1 }), fetchTemplates({ page: -1 })]).then(
fetchData({ page: -1 }), ([instanceResItems, templatesRes]) => {
fetchTemplates({ page: -1 }),
fetchClusterList({ page: -1 })
]).then(([instanceResItems, templatesRes, clusters]) => {
if (sessionRef.current !== session) return; if (sessionRef.current !== session) return;
applyAutoSelection( applyAutoSelection(
instanceResItems || [], instanceResItems || [],
templatesRes?.items || [], templatesRes?.items || [],
(Array.isArray(clusters) ? clusters : (clusters as any)?.items) || [],
orgId orgId
); );
initializedRef.current = true; setInitialized(true);
}); }
);
} catch (error) { } catch (error) {
} finally { setInitialized(true);
initializedRef.current = true;
} }
}; };
@@ -352,13 +346,13 @@ const AddModal: React.FC<AddModalProps> = ({
// owner-scoped auto-selection re-fills them from the new org, or leaves // owner-scoped auto-selection re-fills them from the new org, or leaves
// them empty (blocking submit) when the chosen org has no clusters. // them empty (blocking submit) when the chosen org has no clusters.
clearSelection(); clearSelection();
initializedRef.current = false; setInitialized(false);
loadCreateResources(orgId); loadCreateResources(orgId);
}; };
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
initializedRef.current = false; setInitialized(false);
sessionRef.current += 1; sessionRef.current += 1;
setInstanceTypeSelection({ setInstanceTypeSelection({
instanceType: undefined, instanceType: undefined,
@@ -619,15 +613,12 @@ const AddModal: React.FC<AddModalProps> = ({
onChange={(e) => setTemplateKeyword(e.target.value)} onChange={(e) => setTemplateKeyword(e.target.value)}
/> />
</div> </div>
{filteredTemplates.length > 0 && initializedRef.current ? (
<TemplateSelector <TemplateSelector
value={templateId} value={templateId}
loading={templateLoading || !initialized}
groups={templateGroups} groups={templateGroups}
onChange={handleTemplateChange} onChange={handleTemplateChange}
/> />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</div> </div>
</ColumnWrapper> </ColumnWrapper>
<Separator></Separator> <Separator></Separator>
@@ -1,17 +1,10 @@
import FileSkeleton from '@/pages/llmodels/components/model-source/file-skeleton'; import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
import { TemplateCard } from '@gpustack/core-ui'; import { TemplateCard } from '@gpustack/core-ui';
import { Empty, Spin } from 'antd'; import { Empty, Flex, Spin } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import styled from 'styled-components';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types'; import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
import InstanceTypeItem from './instance-type-item'; import InstanceTypeItem from './instance-type-item';
const TypeGrid = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
interface InstanceTypeListProps { interface InstanceTypeListProps {
value?: string; value?: string;
onChange?: (item: InstanceTypeItemModel) => void; onChange?: (item: InstanceTypeItemModel) => void;
@@ -34,11 +27,11 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
if (loading) { if (loading) {
return ( return (
<Spin spinning size="middle"> <Spin spinning size="middle">
<TypeGrid style={{ minHeight: 200 }}> <Flex orientation="vertical" gap={16} style={{ minHeight: 200 }}>
{_.times(6, (index: number) => ( {_.times(6, (index: number) => (
<FileSkeleton key={index} counts={3} itemHeight={106} /> <FileSkeletonRows key={index} counts={2} itemHeight={106} />
))} ))}
</TypeGrid> </Flex>
</Spin> </Spin>
); );
} }
@@ -46,7 +39,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
} }
return ( return (
<TypeGrid> <Flex orientation="vertical" gap={16}>
{dataList.map((item) => { {dataList.map((item) => {
const name = item.name; const name = item.name;
return ( return (
@@ -64,7 +57,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
</TemplateCard> </TemplateCard>
); );
})} })}
</TypeGrid> </Flex>
); );
}; };
+25 -14
View File
@@ -129,6 +129,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
// exists/changes when a platform admin retargets the form. Watch it // exists/changes when a platform admin retargets the form. Watch it
// so the parent can re-scope offerings (see onScopeChange). // so the parent can re-scope offerings (see onScopeChange).
const scopeOrgId = Form.useWatch('organization_id', form); const scopeOrgId = Form.useWatch('organization_id', form);
const ports = Form.useWatch(['spec', 'ports'], form) || [];
const scopeInitRef = useRef(true); const scopeInitRef = useRef(true);
// Keep the latest callback in a ref so the scope-change effect can call it // Keep the latest callback in a ref so the scope-change effect can call it
// without depending on its identity (parent may pass a new fn each render). // without depending on its identity (parent may pass a new fn each render).
@@ -151,17 +152,31 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
: [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE] : [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
}); });
const hasSSHPort = useMemo(
() =>
ports.some(
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
),
[ports]
);
const isGPUType = useMemo(() => { const isGPUType = useMemo(() => {
const spec = parseJsonSafe(description || '{}', {} as any)?.spec; const spec = parseJsonSafe(description || '{}', {} as any)?.spec;
console.log('derived spec from description', spec);
return spec?.acceleratable; return spec?.acceleratable;
}, [description]); }, [description]);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchSSHData({ page: 1, perPage: 100 }); const initSSHKeys = async () => {
// await 200 ms
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
fetchSSHData({ page: -1 });
};
initSSHKeys();
} }
}, [open, action]); }, [open]);
// Skip the first run (initial mount value); thereafter notify the // Skip the first run (initial mount value); thereafter notify the
// parent whenever the chosen create scope changes so it can reload // parent whenever the chosen create scope changes so it can reload
@@ -174,16 +189,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
onScopeChangeRef.current?.(scopeOrgId); onScopeChangeRef.current?.(scopeOrgId);
}, [scopeOrgId]); }, [scopeOrgId]);
const ports = Form.useWatch(['spec', 'ports'], form) || [];
const hasSSHPort = useMemo(
() =>
ports.some(
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
),
[ports]
);
useEffect(() => { useEffect(() => {
if ( if (
hasSSHPort && hasSSHPort &&
@@ -554,7 +559,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
image: '', image: '',
imagePullPolicy: DefaultImagePullPolicy, imagePullPolicy: DefaultImagePullPolicy,
command: [], command: [],
ports: [], ports: [
{
protocol: 'TCP',
port: SSH_PORT,
name: 'SSH'
}
],
env: [], env: [],
volumeMount: '', volumeMount: '',
resources: { resources: {
@@ -36,7 +36,13 @@ const StorageVolume = ({
const [overlayOpen, setOverlayOpen] = useState(false); const [overlayOpen, setOverlayOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetchStorage({ page: 1, perPage: 100 }); const initStorage = async () => {
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
fetchStorage({ page: -1 });
};
initStorage();
}, []); }, []);
const storageOptions = useMemo( const storageOptions = useMemo(
@@ -1,15 +1,12 @@
import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton';
import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui'; import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Empty, Flex, Spin } from 'antd';
import _ from 'lodash';
import { Fragment } from 'react'; import { Fragment } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ListItem as TemplateItem } from '../../templates/config/types'; import { ListItem as TemplateItem } from '../../templates/config/types';
const TemplateGrid = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
const GroupTitle = styled.div` const GroupTitle = styled.div`
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
@@ -49,6 +46,12 @@ const TemplateContent = styled.div`
} }
`; `;
const TypeGrid = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
export interface TemplateGroup { export interface TemplateGroup {
key: string; key: string;
label: React.ReactNode; label: React.ReactNode;
@@ -57,6 +60,7 @@ export interface TemplateGroup {
interface TemplateSelectorProps { interface TemplateSelectorProps {
value?: number; value?: number;
loading?: boolean;
onChange?: (value: number, item: TemplateItem) => void; onChange?: (value: number, item: TemplateItem) => void;
groups?: TemplateGroup[]; groups?: TemplateGroup[];
} }
@@ -64,6 +68,7 @@ interface TemplateSelectorProps {
const TemplateSelector: React.FC<TemplateSelectorProps> = ({ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
value, value,
onChange, onChange,
loading,
groups = [] groups = []
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@@ -73,6 +78,22 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
onChange?.(item.id, item); onChange?.(item.id, item);
}; };
if (loading) {
return (
<Spin spinning size="middle">
<Flex orientation="vertical" gap={16} style={{ minHeight: 200 }}>
{_.times(6, (index: number) => (
<FileSkeletonRows key={index} counts={2} itemHeight={106} />
))}
</Flex>
</Spin>
);
}
if (!groups.length) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
}
const renderItem = (item: TemplateItem) => ( const renderItem = (item: TemplateItem) => (
<TemplateCard <TemplateCard
key={item.id} key={item.id}
@@ -120,14 +141,14 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
const showGroupTitles = groups.length > 1; const showGroupTitles = groups.length > 1;
return ( return (
<TemplateGrid> <Flex orientation="vertical" gap={16}>
{groups.map((group) => ( {groups.map((group) => (
<Fragment key={group.key}> <Fragment key={group.key}>
{showGroupTitles && <GroupTitle>{group.label}</GroupTitle>} {showGroupTitles && <GroupTitle>{group.label}</GroupTitle>}
{group.items.map(renderItem)} {group.items.map(renderItem)}
</Fragment> </Fragment>
))} ))}
</TemplateGrid> </Flex>
); );
}; };
+4 -2
View File
@@ -111,7 +111,7 @@ const GPUService: React.FC = () => {
useEffect(() => { useEffect(() => {
fetchClusterList({ page: -1 }); fetchClusterList({ page: -1 });
(async () => { const fetchPVCapacities = async () => {
try { try {
const res = await queryGPUServiceStorage({ page: -1 } as any); const res = await queryGPUServiceStorage({ page: -1 } as any);
const map: Record<string, string> = {}; const map: Record<string, string> = {};
@@ -124,7 +124,8 @@ const GPUService: React.FC = () => {
} catch { } catch {
// best-effort; the popover falls back to the PV name // best-effort; the popover falls back to the PV name
} }
})(); };
fetchPVCapacities();
}, []); }, []);
const hasK8sCluster = useMemo( const hasK8sCluster = useMemo(
@@ -387,6 +388,7 @@ const GPUService: React.FC = () => {
data={openInstanceModalStatus.currentData} data={openInstanceModalStatus.currentData}
width={openInstanceModalStatus.width} width={openInstanceModalStatus.width}
realAction={openInstanceModalStatus.realAction} realAction={openInstanceModalStatus.realAction}
clusterList={clusterList}
onCancel={closeInstanceModal} onCancel={closeInstanceModal}
onOk={handleModalOk} onOk={handleModalOk}
/> />
@@ -31,4 +31,24 @@ const FileSkeleton: React.FC<{ counts: number; itemHeight?: number }> = ({
); );
}; };
export const FileSkeletonRows: React.FC<{
counts: number;
itemHeight?: number;
}> = ({ counts = 2, itemHeight }) => {
return (
<Wrapper orientation="vertical" style={{ height: itemHeight || 'auto' }}>
<Skeleton paragraph={{ rows: 1, width: '100%' }} title={false}></Skeleton>
<Flex orientation="vertical" gap={12}>
<Skeleton
paragraph={{
rows: counts,
width: Array.from({ length: counts }, () => '60%')
}}
title={false}
></Skeleton>
</Flex>
</Wrapper>
);
};
export default FileSkeleton; export default FileSkeleton;