refactor: gpu service API alignment

This commit is contained in:
jialin
2026-05-25 22:42:02 +08:00
committed by jialin
parent 81c7e2ee61
commit 88ab927755
90 changed files with 3247 additions and 2023 deletions
@@ -101,7 +101,8 @@ const AddModal: React.FC<AddModalProps> = ({
const instanceTypeList = detailData?.items || [];
const templateList = templatesData?.items || [];
const readonly = action === PageAction.VIEW;
// const readonly = action === PageAction.VIEW;
const readonly = false;
const isRecreate = realAction === PageAction.CREATE;
const showResourceSelectors = action === PageAction.CREATE || isRecreate;
const shouldAutoSelectResource = action === PageAction.CREATE && !isRecreate;
@@ -119,27 +120,33 @@ const AddModal: React.FC<AddModalProps> = ({
instanceType: InstanceTypeItem,
template: TemplateItem | undefined
) => {
const name = instanceType.metadata?.name;
const manufacturer = instanceType.spec?.manufacturer;
setInstanceTypeSelection({ instanceType: name, manufacturer });
setInstanceTypeSelection({
instanceType: instanceType.name,
manufacturer
});
setTemplateId(template?.id);
const currentSpec = form.current?.getFieldsValue()?.spec || {};
const updatedSpec = {
...currentSpec,
type: name,
resources: { ...(currentSpec.resources || {}), accelerator: '1' }
};
if (template) {
const currentSpec = form.current?.getFieldsValue()?.spec || {};
form.current?.setFieldsValue({
manufacturer: template.manufacturer,
description: JSON.stringify(
{
name: instanceType.name,
spec: {
...instanceType.spec
}
},
null,
2
),
spec: {
...updatedSpec,
...currentSpec,
...template.spec,
resources: {
...(updatedSpec.resources || {}),
...(currentSpec.resources || {}),
...(template.spec?.resources || {})
}
}
@@ -147,16 +154,46 @@ const AddModal: React.FC<AddModalProps> = ({
} else {
form.current?.setFieldsValue({
manufacturer: undefined,
spec: updatedSpec
description: JSON.stringify(instanceType.spec, null, 2)
});
}
form.current?.applyInstanceType?.(instanceType);
};
const findAggregateOf = (
candidateName: string | undefined,
clusterId: number | null | undefined,
instanceTypes: InstanceTypeItem[]
): InstanceTypeItem | undefined => {
if (!candidateName) return undefined;
return instanceTypes.find((item) =>
(item.status?.acceleratorTiers ?? []).some((tier) =>
(tier.candidates ?? []).some(
(c) => c.name === candidateName && Number(c.cluster) === clusterId
)
)
);
};
const applyAutoSelection = (
instanceTypes: InstanceTypeItem[],
templates: TemplateItem[]
) => {
if (!shouldAutoSelectResource) return;
// On edit / view, surface the persisted selection in the card list.
if (!shouldAutoSelectResource) {
const aggregate = findAggregateOf(
data?.spec?.type,
data?.clusterId,
instanceTypes
);
if (aggregate) {
setInstanceTypeSelection({
instanceType: aggregate.name,
manufacturer: aggregate.spec?.manufacturer
});
}
return;
}
if (!instanceTypes.length) return;
const first = instanceTypes[0];
@@ -178,18 +215,21 @@ const AddModal: React.FC<AddModalProps> = ({
return;
}
const session = ++sessionRef.current;
Promise.all([fetchData({}), fetchTemplates({ page: -1 })]).then(
([instanceRes, templatesRes]) => {
if (action === PageAction.CREATE) {
const session = ++sessionRef.current;
Promise.all([
fetchData({ page: 1, perPage: 100 }),
fetchTemplates({ page: -1 })
]).then(([instanceRes, templatesRes]) => {
if (sessionRef.current !== session) return;
applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []);
}
);
}, [open, shouldAutoSelectResource]);
});
}
}, [open, shouldAutoSelectResource, action]);
// filter instance types
const filteredInstanceTypes = instanceTypeList.filter((item) =>
matchKeyword([item.metadata?.name], instanceKeyword)
matchKeyword([item.name], instanceKeyword)
);
// filter templates based on selection and keyword
@@ -370,7 +410,7 @@ const AddModal: React.FC<AddModalProps> = ({
}
>
<>
{action !== PageAction.VIEW && (
{action !== PageAction.EDIT && (
<ColTitle>
{intl.formatMessage({ id: 'common.title.config' })}
</ColTitle>
@@ -1,16 +1,12 @@
import { AutoTooltip, IconFont, StatusTag, ThemeTag } from '@gpustack/core-ui';
import { AutoTooltip, IconFont, ThemeTag } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex } from 'antd';
import styled from 'styled-components';
import { manufactureColorMap } from '../../templates/config';
import {
convertKiToGi,
InstanceTypePhaseLabelMap,
InstanceTypePhaseStatus
} from '../config';
import { convertKiToGi } from '../config';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
const toDisplayUnit = (value?: string) =>
const toDisplayUnit = (value?: string | null) =>
value ? value.replace(/Gi$/, 'GB').replace(/Ti$/, 'TB') : value;
const Title = styled.div`
@@ -60,24 +56,48 @@ interface InstanceTypeItemProps {
showStatus?: boolean;
}
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
item,
showStatus = true
}) => {
const intl = useIntl();
const name = item.metadata?.name;
const acceleratable = item.spec?.acceleratable;
const MetaItem: React.FC<{
icon: string;
label: string;
value?: string | null;
showDot?: boolean;
show?: boolean;
}> = ({ icon, label, value, showDot = true, show = true }) => {
if (!show) return null;
return (
<>
{showDot && <span className="dot"></span>}
<span className="meta-item">
<IconFont type={icon} className="meta-icon" />
<Flex align="center" gap={4}>
<span>{label}</span>
<span>{value || '-'}</span>
</Flex>
</span>
</>
);
};
const manufacturerKey = item.spec?.manufacturer;
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const intl = useIntl();
const name = item.name;
const specData = item.spec || {};
const acceleratable = specData.acceleratable;
const manufacturerKey = specData.manufacturer || (acceleratable ? '' : 'cpu');
const manufacturer = manufacturerKey?.toUpperCase();
const manufacturerColor =
(manufacturerKey && manufactureColorMap[manufacturerKey]) ?? 'purple';
// resource remaining status
const remainingData = item.status?.remaining || {};
const renderName = () => {
const product = item.spec?.product;
const product = specData.product;
const cpuCapacity = remainingData.cpu;
const displayName =
product ||
(!acceleratable ? `${item.status?.cpu?.capacity} vCPUs` : name);
(!acceleratable && cpuCapacity ? `${cpuCapacity} vCPUs` : name);
return displayName;
};
@@ -85,8 +105,8 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
<>
<Title>
<Flex gap={8} align="center">
<AutoTooltip ghost minWidth={20} maxWidth={180}>
{item.spec ? renderName() : '-'}
<AutoTooltip ghost minWidth={20} maxWidth={200}>
{renderName() || '-'}
</AutoTooltip>
{acceleratable && manufacturer && (
<span
@@ -95,99 +115,59 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
fontWeight: 400
}}
>
<ThemeTag color={manufacturerColor}>{manufacturer}</ThemeTag>
<ThemeTag color={manufacturerColor} disabled={false}>
{manufacturer}
</ThemeTag>
</span>
)}
</Flex>
{showStatus && (
<Flex gap={8} align="center">
{item.status?.phase && (
<StatusTag
statusValue={{
status:
InstanceTypePhaseStatus[item.status.phase] ?? 'inactive',
text:
InstanceTypePhaseLabelMap[item.status.phase] ??
item.status.phase,
message: ''
}}
/>
)}
</Flex>
)}
</Title>
<Meta>
<span style={{ display: 'flex', height: 15 }}>
{acceleratable && (
<span className="meta-row">
<span className="meta-item">
<IconFont type="icon-gpu1" className="meta-icon" />
<Flex align="center" gap={4}>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}
</span>
<span>
{toDisplayUnit(convertKiToGi(item.spec?.memory)) ?? '-'}
</span>
</Flex>
</span>
{item.spec?.sliced && (
<>
<span className="dot"></span>
<span className="meta-item">
<IconFont type="icon-sliced" className="meta-icon" />
<Flex align="center" gap={4}>
<span>
{intl.formatMessage({
id: 'gpuservice.instance.sliced'
})}
</span>
<span>{item.spec?.sliced ?? '-'}</span>
</Flex>
</span>
</>
)}
{item.status?.accelerator?.remaining && (
<>
<span className="dot"></span>
<span className="meta-item">
<IconFont type="icon-database" className="meta-icon" />
<Flex align="center" gap={4}>
<span>
{intl.formatMessage({
id: 'gpuservice.instance.stock'
})}
</span>
<span>{item.status?.accelerator?.remaining ?? '-'}</span>
</Flex>
</span>
</>
)}
<MetaItem
showDot={false}
icon="icon-gpu1"
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
value={
toDisplayUnit(convertKiToGi(specData?.memory ?? undefined)) ??
'-'
}
></MetaItem>
<MetaItem
show={!!specData?.sliced}
icon="icon-sliced"
label={intl.formatMessage({
id: 'gpuservice.instance.sliced'
})}
value={specData?.sliced}
></MetaItem>
<MetaItem
show={!!remainingData.accelerator}
icon="icon-database"
label={intl.formatMessage({
id: 'gpuservice.instance.stock'
})}
value={remainingData.accelerator}
></MetaItem>
</span>
)}
</span>
<span className="meta-row">
<span className="meta-item">
<IconFont type="icon-ram-02" className="meta-icon" />
<Flex align="center" gap={4}>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}
</span>
<span>{toDisplayUnit(item.status?.ram?.capacity) ?? '-'}</span>
</Flex>
</span>
{acceleratable && (
<>
<span className="dot"></span>
<span className="meta-item">
<IconFont type="icon-cpu" className="meta-icon" />
<Flex align="center" gap={4}>
<span>vCPU</span>
<span>{item.status?.cpu?.capacity ?? '-'}</span>
</Flex>
</span>
</>
)}
<MetaItem
showDot={false}
icon="icon-ram-02"
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
value={toDisplayUnit(convertKiToGi(remainingData.ram)) ?? '-'}
></MetaItem>
<MetaItem
show={acceleratable}
showDot={true}
icon="icon-cpu"
label="vCPU"
value={remainingData.cpu ?? '-'}
></MetaItem>
</span>
</Meta>
</>
@@ -3,7 +3,7 @@ import { TemplateCard } from '@gpustack/core-ui';
import { Empty, Spin } from 'antd';
import _ from 'lodash';
import styled from 'styled-components';
import { InstanceTypePhaseValueMap } from '../config';
import { getAcceleratorMax } from '../config';
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
import InstanceTypeItem from './instance-type-item';
@@ -20,8 +20,12 @@ interface InstanceTypeListProps {
loading?: boolean;
}
const isAvailable = (item: InstanceTypeItemModel) =>
item.status?.phase === InstanceTypePhaseValueMap.Active;
// CPU types always have stock (no accelerator). Accelerated types are
// considered available when at least one tier offers onceMaxRequest > 0.
const isAvailable = (item: InstanceTypeItemModel) => {
if (!item.spec?.acceleratable) return true;
return getAcceleratorMax(item.status?.acceleratorTiers) > 0;
};
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
value,
@@ -53,7 +57,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
<TypeGrid>
{dataList.map((item) => {
const disabled = !isAvailable(item);
const name = item.metadata?.name;
const name = item.name;
return (
<TemplateCard
key={name}
@@ -31,7 +31,7 @@ const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
const intl = useIntl();
const { open, onCancel, name } = props || {};
const { open, onCancel, name, namespace, clusterID } = props || {};
const [activeKey, setActiveKey] = useState('instance');
const {
@@ -52,9 +52,9 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
const volumeEvents = volumeEventsData?.items ?? [];
const refreshAll = () => {
if (!name) return;
fetchInstanceEvents({ name });
fetchVolumeEvents({ name });
if (!name || !namespace || !clusterID) return;
fetchInstanceEvents({ name, namespace, clusterID });
fetchVolumeEvents({ name, namespace, clusterID });
};
useEffect(() => {