fix: instance type api update

This commit is contained in:
jialin
2026-05-30 08:28:38 +08:00
committed by Lawrence Li
parent d33167d070
commit 16a97772d3
10 changed files with 312 additions and 179 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ export default {
itemColor: 'rgba(0,0,0,1)', itemColor: 'rgba(0,0,0,1)',
itemHoverBg: 'rgba(0,0,0,0.04)', itemHoverBg: 'rgba(0,0,0,0.04)',
itemActiveBg: 'rgba(0,0,0,0.04)', itemActiveBg: 'rgba(0,0,0,0.04)',
menuItemSelectedBg: 'rgba(0, 0, 0, 0.06)' menuItemSelectedBg: '#e8eaed'
}, },
Progress: { Progress: {
lineBorderRadius: 3 lineBorderRadius: 3
@@ -290,8 +290,8 @@ const AddModal: React.FC<AddModalProps> = ({
...item.spec, ...item.spec,
sshPublicKeys: formValues?.spec?.sshPublicKeys, sshPublicKeys: formValues?.spec?.sshPublicKeys,
resources: { resources: {
...item?.spec?.resources, ...formValues?.spec?.resources,
accelerator: formValues?.spec?.resources?.accelerator localStorage: item?.spec?.resources?.localStorage
}, },
volume: { volume: {
...formValues?.spec?.volume ...formValues?.spec?.volume
@@ -1,3 +1,4 @@
import { ceilMilliToCore } from '@/pages/gpu-service/utils';
import { AutoTooltip, IconFont, ThemeTag } from '@gpustack/core-ui'; import { AutoTooltip, IconFont, ThemeTag } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Flex } from 'antd'; import { Flex } from 'antd';
@@ -21,28 +22,30 @@ const Title = styled.div`
const Meta = styled.div` const Meta = styled.div`
display: grid; display: grid;
gap: 8px; grid-template-columns: repeat(7, auto);
justify-content: start;
column-gap: 4px;
row-gap: 8px;
align-items: center;
color: var(--ant-color-text-secondary); color: var(--ant-color-text-secondary);
font-size: 13px; font-size: 13px;
.meta-row { .meta-row {
display: flex; display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
align-items: center; align-items: center;
gap: 8px; min-height: 15px;
color: var(--ant-color-text-tertiary); color: var(--ant-color-text-tertiary);
} }
.dot { .dot {
width: 3px; width: 3px;
height: 3px; height: 3px;
border-radius: 50%; border-radius: 50%;
background-color: var(--ant-color-text-quaternary); background-color: var(--ant-color-text-quaternary);
flex: none; margin: 0 4px;
} justify-self: center;
.meta-item {
display: inline-flex;
align-items: center;
gap: 4px;
} }
.meta-icon { .meta-icon {
@@ -66,14 +69,10 @@ const MetaItem: React.FC<{
if (!show) return null; if (!show) return null;
return ( return (
<> <>
{showDot && <span className="dot"></span>} {showDot && <span className="dot" />}
<span className="meta-item">
<IconFont type={icon} className="meta-icon" /> <IconFont type={icon} className="meta-icon" />
<Flex align="center" gap={4}> <span className="meta-label">{label}</span>
<span>{label}</span> <span className="meta-value">{value || '-'}</span>
<span>{value || '-'}</span>
</Flex>
</span>
</> </>
); );
}; };
@@ -81,6 +80,8 @@ const MetaItem: React.FC<{
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => { const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const intl = useIntl(); const intl = useIntl();
const specData = item.spec || {}; const specData = item.spec || {};
// false: CPU; true: GPU
const acceleratable = specData.acceleratable; const acceleratable = specData.acceleratable;
const manufacturer = acceleratable ? specData.manufacturer || '' : 'cpu'; const manufacturer = acceleratable ? specData.manufacturer || '' : 'cpu';
@@ -89,6 +90,18 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
// resource once-max-request status // resource once-max-request status
const onceMaxRequestData = item.status?.onceMaxRequest || {}; const onceMaxRequestData = item.status?.onceMaxRequest || {};
// RAM resource
const ramRaw = acceleratable
? specData.unitResources?.ram
: onceMaxRequestData.ram;
console.log('InstanceTypeItem', item.name, 'ramRaw', ramRaw);
// CPU resource
const cpuRaw = acceleratable
? specData.unitResources?.cpu
: onceMaxRequestData.cpu;
const renderName = () => { const renderName = () => {
const displayName = specData.acceleratable const displayName = specData.acceleratable
? specData.product || item.name ? specData.product || item.name
@@ -118,9 +131,9 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
</Flex> </Flex>
</Title> </Title>
<Meta> <Meta>
<span style={{ display: 'flex', height: 15 }}>
{acceleratable && (
<span className="meta-row"> <span className="meta-row">
{acceleratable && (
<>
<MetaItem <MetaItem
showDot={false} showDot={false}
icon="icon-gpu1" icon="icon-gpu1"
@@ -129,7 +142,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
toDisplayUnit(convertKiToGi(specData?.memory ?? undefined)) ?? toDisplayUnit(convertKiToGi(specData?.memory ?? undefined)) ??
'-' '-'
} }
></MetaItem> />
<MetaItem <MetaItem
show={!!specData?.sliced} show={!!specData?.sliced}
icon="icon-sliced" icon="icon-sliced"
@@ -137,17 +150,18 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
id: 'gpuservice.instance.sliced' id: 'gpuservice.instance.sliced'
})} })}
value={specData?.sliced} value={specData?.sliced}
></MetaItem> />
<MetaItem <MetaItem
icon="icon-database" icon="icon-database"
value={intl.formatMessage( label={intl.formatMessage(
{ {
id: 'common.max' id: 'common.max'
}, },
{ count: item.maxAccelerator } { count: '' }
)} )}
></MetaItem> value={`${item.maxAccelerator || '-'}`}
</span> />
</>
)} )}
</span> </span>
<span className="meta-row"> <span className="meta-row">
@@ -155,15 +169,15 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
showDot={false} showDot={false}
icon="icon-ram-02" icon="icon-ram-02"
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })} label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
value={toDisplayUnit(convertKiToGi(onceMaxRequestData.ram)) ?? '-'} value={toDisplayUnit(convertKiToGi(ramRaw)) ?? '-'}
></MetaItem> />
<MetaItem <MetaItem
show={!!onceMaxRequestData.cpu} show={!!cpuRaw}
showDot={true} showDot={true}
icon="icon-cpu" icon="icon-cpu"
label="CPU" label="CPU"
value={onceMaxRequestData.cpu ?? '-'} value={ceilMilliToCore(cpuRaw) ?? '-'}
></MetaItem> />
</span> </span>
</Meta> </Meta>
</> </>
@@ -172,8 +172,8 @@ export const convertKiToGi = (value?: string): string | undefined => {
const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)$/.exec(value); const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)$/.exec(value);
if (!match) return value; if (!match) return value;
const [, num, unit] = match; const [, num, unit] = match;
if (unit === 'Ti') return `${_.round(Number(num), 2)}Ti`; if (unit === 'Ti') return `${_.floor(Number(num), 0)}Ti`;
return `${_.round(Number(num) / GI_DIVISOR[unit], 2)}Gi`; return `${_.floor(Number(num) / GI_DIVISOR[unit], 0)}Gi`;
}; };
const parseQuantity = (value?: string | null): number => { const parseQuantity = (value?: string | null): number => {
@@ -1,5 +1,21 @@
export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never'; export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never';
type QuanityCPU = `${number}m` | string;
type QuanityMemory =
| `${number}Ki`
| `${number}Mi`
| `${number}Gi`
| `${number}Ti`
| string;
type QuanityLocalStorage =
| `${number}Ki`
| `${number}Mi`
| `${number}Gi`
| `${number}Ti`
| string;
// instance form data // instance form data
export interface FormData { export interface FormData {
name: string; name: string;
@@ -116,9 +132,9 @@ export interface InstanceTypeCandidate {
export interface InstanceTypeTierOnceMaxRequestResource { export interface InstanceTypeTierOnceMaxRequestResource {
accelerator?: string; accelerator?: string;
cpu: string; cpu: QuanityCPU;
ram: string; ram: QuanityMemory;
localStorage: string; localStorage: QuanityLocalStorage;
} }
export interface InstanceTypeTier { export interface InstanceTypeTier {
@@ -127,10 +143,10 @@ export interface InstanceTypeTier {
} }
export interface InstanceTypeOnceMaxRequestResource { export interface InstanceTypeOnceMaxRequestResource {
accelerator?: string | null; accelerator?: `${number}` | null;
cpu: string; cpu: QuanityCPU;
ram: string; ram: QuanityMemory;
localStorage: string; localStorage: QuanityLocalStorage;
} }
export interface InstanceTypeSpec { export interface InstanceTypeSpec {
@@ -142,6 +158,10 @@ export interface InstanceTypeSpec {
family?: string | null; family?: string | null;
computeCapability?: string | null; computeCapability?: string | null;
sliced?: string | null; sliced?: string | null;
unitResources?: {
cpu: QuanityCPU;
ram: QuanityMemory;
};
} }
export interface InstanceTypeStatus { export interface InstanceTypeStatus {
+60 -33
View File
@@ -1,5 +1,10 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import {
ceilMilliToCore,
parseQuantityToGi,
parseQuantityToNumber
} from '@/pages/gpu-service/utils';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import {
CheckboxField, CheckboxField,
@@ -57,35 +62,6 @@ interface InstanceFormProps {
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
} }
const parseQuantityToNumber = (value?: string | null): number | null => {
if (!value) return null;
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
if (!match) return null;
const num = Number(match[1]);
return Number.isFinite(num) && num > 0 ? num : null;
};
// Converts a K8s-style quantity ("24314504Ki", "2Ti", "10Gi", or a bare
// number assumed to be Gi) to an integer GiB. memory and localStorage
// inputs are displayed in GB, so the max bound and the "Remaining {count}
// GB" label need the converted value.
const GI_FROM_UNIT: Record<string, number> = {
Ki: 1 / (1024 * 1024),
Mi: 1 / 1024,
Gi: 1,
Ti: 1024
};
const parseQuantityToGi = (value?: string | null): number | null => {
if (!value) return null;
const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)?$/.exec(String(value));
if (!match) return null;
const num = Number(match[1]);
if (!Number.isFinite(num) || num <= 0) return null;
const factor = match[2] ? GI_FROM_UNIT[match[2]] : 1;
return Math.floor(num * factor);
};
const TABKeysMap = { const TABKeysMap = {
BASIC: 'basic', BASIC: 'basic',
INSTANCE_TYPE: 'instanceType', INSTANCE_TYPE: 'instanceType',
@@ -238,18 +214,39 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
); );
setSelectedInstanceType(instanceType); setSelectedInstanceType(instanceType);
setOnceMaxRequest({ setOnceMaxRequest({
cpu: parseQuantityToNumber(candidate?.cpu?.onceMaxRequest), cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest),
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest), memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest),
localStorage: parseQuantityToGi(candidate?.localStorage?.onceMaxRequest) localStorage: parseQuantityToGi(candidate?.localStorage?.onceMaxRequest)
}); });
const unitResources = instanceType.spec?.unitResources;
const unitCpu = parseQuantityToNumber(unitResources?.cpu);
const unitRam = parseQuantityToNumber(unitResources?.ram);
const acceleratable = instanceType.spec?.acceleratable;
const cpuCores =
unitCpu != null ? ceilMilliToCore(`${count * unitCpu}m`) : null;
const ramGi =
unitRam != null ? parseQuantityToGi(`${count * unitRam}Mi`) : null;
form.setFieldsValue({ form.setFieldsValue({
clusterId: candidate?.cluster ? _.toNumber(candidate.cluster) : null, clusterId: candidate?.cluster ? _.toNumber(candidate.cluster) : null,
spec: { spec: {
type: candidate?.name || '', type: candidate?.name || '',
...(options.writeAccelerator ...(options.writeAccelerator
? { resources: { accelerator: _.toString(count) } } ? { resources: { accelerator: _.toString(count) } }
: {}),
...(acceleratable
? {
resources: {
...(options.writeAccelerator
? { accelerator: _.toString(count) }
: {}),
...(cpuCores != null ? { cpu: cpuCores } : {}),
...(ramGi != null ? { ram: `${ramGi}Gi` } : {})
}
}
: {}) : {})
} as any } as any
}); });
@@ -304,6 +301,20 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
} }
}, [action, currentData, form, open, realAction, instanceTypeList]); }, [action, currentData, form, open, realAction, instanceTypeList]);
const getUnitResources = () => {
if (selectedInstanceType?.spec?.unitResources) {
return selectedInstanceType.spec.unitResources;
}
try {
return (
JSON.parse(currentData?.description || '{}')?.spec?.unitResources ??
undefined
);
} catch {
return undefined;
}
};
const handleFinish = async (values: InstanceFormValues) => { const handleFinish = async (values: InstanceFormValues) => {
const submittedPorts = [...(values.spec?.ports ?? [])]; const submittedPorts = [...(values.spec?.ports ?? [])];
const submittedHasSSHPort = submittedPorts.some( const submittedHasSSHPort = submittedPorts.some(
@@ -317,11 +328,27 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
name: 'SSH' name: 'SSH'
}); });
} }
const accelerator = _.toNumber(values.spec?.resources?.accelerator);
const submittedResources = { ...(values.spec?.resources ?? {}) };
if (accelerator > 0) {
const unitResources = getUnitResources();
const unitCpu = parseQuantityToNumber(unitResources?.cpu);
const unitRam = parseQuantityToNumber(unitResources?.ram);
if (unitCpu != null) {
submittedResources.cpu = `${accelerator * unitCpu}m`;
}
if (unitRam != null) {
submittedResources.ram = `${accelerator * unitRam}Mi`;
}
}
await onFinish({ await onFinish({
..._.omit(values, ['enable_ssh']), ..._.omit(values, ['enable_ssh']),
spec: { spec: {
...values.spec, ...values.spec,
ports: submittedPorts ports: submittedPorts,
resources: submittedResources
} }
}); });
}; };
@@ -446,6 +473,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
disabled={disabled} disabled={disabled}
selectedInstanceType={selectedInstanceType} selectedInstanceType={selectedInstanceType}
currentData={currentData as any} currentData={currentData as any}
onceMaxRequest={onceMaxRequest}
onGPUCountChange={handleAcceleratorChange} onGPUCountChange={handleAcceleratorChange}
/> />
) )
@@ -505,7 +533,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
})} })}
</Button> </Button>
</Flex> </Flex>
{
<div className={instanceStyles.sshkeySelection}> <div className={instanceStyles.sshkeySelection}>
<Form.Item<FormData> <Form.Item<FormData>
name={['spec', 'sshPublicKeys']} name={['spec', 'sshPublicKeys']}
@@ -547,7 +575,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
></MultipleSelect> ></MultipleSelect>
</Form.Item> </Form.Item>
</div> </div>
}
</Form> </Form>
<PublicKeyOverlay <PublicKeyOverlay
open={sshOverlayOpen} open={sshOverlayOpen}
@@ -1,11 +1,14 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import NumberSelection from '@/pages/_components/number-selection'; import NumberSelection from '@/pages/_components/number-selection';
import { InputNumber } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Alert, Flex, Form } from 'antd'; import { Alert, Flex, Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { BasicResourceMax } from '../../templates/forms/basic';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
import InstanceTypeItem from '../components/instance-type-item'; import InstanceTypeItem from '../components/instance-type-item';
import { getAcceleratorMax } from '../config'; import { getAcceleratorMax } from '../config';
import { import {
@@ -51,6 +54,7 @@ interface InstanceTypeFormItemProps {
disabled?: boolean; disabled?: boolean;
currentData?: ListItem; currentData?: ListItem;
selectedInstanceType?: InstanceTypeItemModel; selectedInstanceType?: InstanceTypeItemModel;
onceMaxRequest?: BasicResourceMax;
onGPUCountChange?: (value: number) => void; onGPUCountChange?: (value: number) => void;
} }
@@ -59,6 +63,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
disabled, disabled,
currentData, currentData,
selectedInstanceType, selectedInstanceType,
onceMaxRequest,
onGPUCountChange onGPUCountChange
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@@ -68,17 +73,45 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
? _.toNumber(currentData?.spec?.resources?.accelerator) || 0 ? _.toNumber(currentData?.spec?.resources?.accelerator) || 0
: getAcceleratorMax(selectedInstanceType?.status?.acceleratorTiers); : getAcceleratorMax(selectedInstanceType?.status?.acceleratorTiers);
const handleOnGPUCountChange = (value: number) => { const isGPU = useMemo(() => {
onGPUCountChange?.(value);
};
const showGPUCount = useMemo(() => {
if (action === PageAction.EDIT) { if (action === PageAction.EDIT) {
return _.toNumber(currentData?.spec?.resources?.accelerator) > 0; return _.toNumber(currentData?.spec?.resources?.accelerator) > 0;
} }
return selectedInstanceType?.spec?.acceleratable; return selectedInstanceType?.spec?.acceleratable;
}, [selectedInstanceType, action]); }, [selectedInstanceType, action]);
const handleOnGPUCountChange = (value: number) => {
onGPUCountChange?.(value);
};
const renderMaxLabel = (
label: React.ReactNode,
max?: number | null
): React.ReactNode => {
if (max == null) return label;
return (
<Flex gap={4} align="center">
{label}
{!isGPU && (
<span>
({intl.formatMessage({ id: 'common.max' }, { count: max })})
</span>
)}
</Flex>
);
};
const renderMemoryLabel = (): React.ReactNode => {
if (isGPU) {
return intl.formatMessage({ id: 'gpuservice.template.memory' });
}
return intl.formatMessage(
{ id: 'gpuservice.instance.memory.remaining' },
{ count: onceMaxRequest?.memory }
);
};
const renderInstanceType = () => { const renderInstanceType = () => {
const description = JSON.parse(currentData?.description || '{}').spec || {}; const description = JSON.parse(currentData?.description || '{}').spec || {};
return ( return (
@@ -123,7 +156,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
{action === PageAction.EDIT && renderInstanceType()} {action === PageAction.EDIT && renderInstanceType()}
</Form.Item> </Form.Item>
</FieldBlock> </FieldBlock>
{showGPUCount && ( {isGPU && (
<Form.Item<FormData> <Form.Item<FormData>
name={['spec', 'resources', 'accelerator']} name={['spec', 'resources', 'accelerator']}
hidden={action === PageAction.EDIT} hidden={action === PageAction.EDIT}
@@ -198,6 +231,48 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
/> />
</Form.Item> </Form.Item>
)} )}
<Flex gap={12}>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'resources', 'ram']}
normalize={(value) => (value ? `${value}Gi` : undefined)}
getValueProps={(value) => {
if (!value) return { value: '' };
const str = String(value);
if (/Gi$/.test(str)) return { value: str.replace(/Gi$/, '') };
if (/(Ki|Mi|Ti)$/.test(str)) {
return { value: parseQuantityToGi(str) ?? '' };
}
return { value: str };
}}
>
<InputNumber
disabled={isGPU || disabled}
label={renderMemoryLabel()}
max={onceMaxRequest?.memory ?? undefined}
/>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'resources', 'cpu']}
normalize={(value) => (value ? `${value}` : '')}
getValueProps={(value) => {
if (!value) return { value: '' };
const str = String(value);
return {
value: /m$/.test(str) ? (ceilMilliToCore(str) ?? '') : str
};
}}
>
<InputNumber
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
max={onceMaxRequest?.cpu ?? undefined}
disabled={disabled || isGPU}
/>
</Form.Item>
</div>
</Flex>
</div> </div>
); );
}; };
@@ -11,10 +11,23 @@ import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import { Fragment, useMemo } from 'react'; import { Fragment, useMemo } from 'react';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
import { InstanceStatusLabelMap, rowActionList, status } from '../config'; import { InstanceStatusLabelMap, rowActionList, status } from '../config';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
import tableSyles from '../styles/table.module.less'; import tableSyles from '../styles/table.module.less';
const formatCpu = (cpu?: string | null): string => {
if (!cpu) return '-';
return /m$/.test(cpu) ? (ceilMilliToCore(cpu) ?? cpu) : cpu;
};
const formatMemoryGB = (value?: string | null): string => {
if (!value) return '-';
if (/Gi$/.test(value)) return value.replace('Gi', 'GB');
const gi = parseQuantityToGi(value);
return gi != null ? `${gi}GB` : value;
};
type ConnectEntry = type ConnectEntry =
| { | {
type: 'ssh'; type: 'ssh';
@@ -106,16 +119,16 @@ const useInstancesColumns = ({
align="center" align="center"
style={{ fontSize: 13, color: 'var(--ant-color-text-tertiary)' }} style={{ fontSize: 13, color: 'var(--ant-color-text-tertiary)' }}
> >
<span>{record.spec.resources?.cpu}C</span> <span>{formatCpu(record.spec.resources?.cpu)}C</span>
<span className={tableSyles.dot} /> <span className={tableSyles.dot} />
<span> <span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}:{' '} {intl.formatMessage({ id: 'gpuservice.instance.ram' })}:{' '}
{record.spec.resources?.ram?.replace('Gi', 'GB')} {formatMemoryGB(record.spec.resources?.ram)}
</span> </span>
<span className={tableSyles.dot} /> <span className={tableSyles.dot} />
<span> <span>
{intl.formatMessage({ id: 'gpuservice.instance.disk' })}:{' '} {intl.formatMessage({ id: 'gpuservice.instance.disk' })}:{' '}
{record.spec.resources?.localStorage?.replace('Gi', 'GB')} {formatMemoryGB(record.spec.resources?.localStorage)}
</span> </span>
</Flex> </Flex>
</Flex> </Flex>
@@ -49,21 +49,6 @@ const Basic: React.FC<BasicProps> = ({
[] []
); );
const renderMaxLabel = (
label: React.ReactNode,
max?: number | null
): React.ReactNode => {
if (max == null) return label;
return (
<Flex gap={4} align="center">
{label}
<span>
({intl.formatMessage({ id: 'common.max' }, { count: max })})
</span>
</Flex>
);
};
const renderStorageLabel = (): React.ReactNode => { const renderStorageLabel = (): React.ReactNode => {
if (page === 'instance' && onceMaxRequest?.localStorage != null) { if (page === 'instance' && onceMaxRequest?.localStorage != null) {
return intl.formatMessage( return intl.formatMessage(
@@ -74,16 +59,6 @@ const Basic: React.FC<BasicProps> = ({
return intl.formatMessage({ id: 'gpuservice.template.containerDisk' }); return intl.formatMessage({ id: 'gpuservice.template.containerDisk' });
}; };
const renderMemoryLabel = (): React.ReactNode => {
if (page === 'instance' && onceMaxRequest?.memory != null) {
return intl.formatMessage(
{ id: 'gpuservice.instance.memory.remaining' },
{ count: onceMaxRequest.memory }
);
}
return intl.formatMessage({ id: 'gpuservice.template.memory' });
};
return ( return (
<> <>
<div data-field="template"></div> <div data-field="template"></div>
@@ -227,37 +202,6 @@ const Basic: React.FC<BasicProps> = ({
</Form.Item> </Form.Item>
</div> </div>
</Flex> </Flex>
<Flex gap={12}>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'resources', 'cpu']}
normalize={(value) => (value ? `${value}` : '')}
getValueProps={(value) => ({ value: value ? String(value) : '' })}
>
<InputNumber
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
max={onceMaxRequest?.cpu ?? undefined}
disabled={disabled}
/>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'resources', 'ram']}
normalize={(value) => (value ? `${value}Gi` : undefined)}
getValueProps={(value) => ({
value: value ? String(value).replace(/Gi$/, '') : ''
})}
>
<InputNumber
label={renderMemoryLabel()}
max={onceMaxRequest?.memory ?? undefined}
disabled={disabled}
/>
</Form.Item>
</div>
</Flex>
<Ports disabled={disabled} /> <Ports disabled={disabled} />
<Env disabled={disabled} /> <Env disabled={disabled} />
</> </>
+40
View File
@@ -11,3 +11,43 @@ export const omitPathParams = <T extends Record<string, any>>(
'cluster_id' 'cluster_id'
]) as Omit<T, 'namespace' | 'clusterID'>; ]) as Omit<T, 'namespace' | 'clusterID'>;
}; };
export const parseQuantityToNumber = (value?: string | null): number | null => {
if (!value) return null;
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
if (!match) return null;
const num = Number(match[1]);
return Number.isFinite(num) && num > 0 ? num : null;
};
const GI_FROM_UNIT: Record<string, number> = {
Ki: 1 / (1024 * 1024),
Mi: 1 / 1024,
Gi: 1,
Ti: 1024
};
// Converts a K8s-style quantity ("24314504Ki", "2Ti", "10Gi", or a bare
// number assumed to be bytes) to an integer GiB. Unit suffix is
// case-insensitive.
export const parseQuantityToGi = (value?: string | null): number | null => {
if (!value) return null;
const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)?$/i.exec(String(value));
if (!match) return null;
const num = Number(match[1]);
if (!Number.isFinite(num) || num <= 0) return null;
const unit = match[2]
? match[2][0].toUpperCase() + match[2].slice(1).toLowerCase()
: null;
const factor = unit ? GI_FROM_UNIT[unit] : 1 / (1024 * 1024 * 1024);
return Math.floor(num * factor);
};
export const ceilMilliToCore = (value?: string | null): string | null => {
if (!value) return null;
const match = /^(-?\d+(?:\.\d+)?)m?$/.exec(value);
if (!match) return null;
const num = Number(match[1]);
if (!Number.isFinite(num)) return null;
return String(Math.ceil(num / 1000));
};