chore: instance creationg UE
This commit is contained in:
@@ -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 }),
|
if (sessionRef.current !== session) return;
|
||||||
fetchClusterList({ page: -1 })
|
applyAutoSelection(
|
||||||
]).then(([instanceResItems, templatesRes, clusters]) => {
|
instanceResItems || [],
|
||||||
if (sessionRef.current !== session) return;
|
templatesRes?.items || [],
|
||||||
applyAutoSelection(
|
orgId
|
||||||
instanceResItems || [],
|
);
|
||||||
templatesRes?.items || [],
|
setInitialized(true);
|
||||||
(Array.isArray(clusters) ? clusters : (clusters as any)?.items) || [],
|
}
|
||||||
orgId
|
);
|
||||||
);
|
|
||||||
initializedRef.current = 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user