fix: parse quality data

This commit is contained in:
jialin
2026-05-31 22:23:32 +08:00
committed by jialin
parent 16a97772d3
commit 55028cf3a3
8 changed files with 214 additions and 122 deletions
@@ -268,6 +268,7 @@ const AddModal: React.FC<AddModalProps> = ({
await onOk({ await onOk({
...values ...values
}); });
console.log('submit form values', values);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -1,4 +1,3 @@
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';
@@ -62,7 +61,7 @@ interface InstanceTypeItemProps {
const MetaItem: React.FC<{ const MetaItem: React.FC<{
icon: string; icon: string;
label?: string; label?: string;
value?: string | null; value?: string | null | number;
showDot?: boolean; showDot?: boolean;
show?: boolean; show?: boolean;
}> = ({ icon, label, value, showDot = true, show = true }) => { }> = ({ icon, label, value, showDot = true, show = true }) => {
@@ -91,15 +90,13 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const onceMaxRequestData = item.status?.onceMaxRequest || {}; const onceMaxRequestData = item.status?.onceMaxRequest || {};
// RAM resource // RAM resource
const ramRaw = acceleratable const ramUnit = acceleratable
? specData.unitResources?.ram ? specData.unitResourcesParsed?.ram?.value
: onceMaxRequestData.ram; : onceMaxRequestData.ram;
console.log('InstanceTypeItem', item.name, 'ramRaw', ramRaw);
// CPU resource // CPU resource
const cpuRaw = acceleratable const cpuUnitCores = acceleratable
? specData.unitResources?.cpu ? specData.unitResourcesParsed?.cpu?.cores
: onceMaxRequestData.cpu; : onceMaxRequestData.cpu;
const renderName = () => { const renderName = () => {
@@ -159,7 +156,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
}, },
{ count: '' } { count: '' }
)} )}
value={`${item.maxAccelerator || '-'}`} value={`${item.maxAccelerator || 0}`}
/> />
</> </>
)} )}
@@ -169,14 +166,14 @@ 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(ramRaw)) ?? '-'} value={ramUnit ? `${ramUnit}GB` : '-'}
/> />
<MetaItem <MetaItem
show={!!cpuRaw} show={!!cpuUnitCores}
showDot={true} showDot={true}
icon="icon-cpu" icon="icon-cpu"
label="CPU" label="CPU"
value={ceilMilliToCore(cpuRaw) ?? '-'} value={cpuUnitCores || '-'}
/> />
</span> </span>
</Meta> </Meta>
@@ -41,10 +41,10 @@ export interface FormData {
}[]; }[];
volumeMount: string; volumeMount: string;
resources: { resources: {
cpu?: string; cpu: string | null | number;
ram?: string; ram: string | null | number;
localStorage?: string; localStorage: string | null | number;
accelerator?: number | string; accelerator: number | string | null;
}; };
volume: { volume: {
ephemeral?: { ephemeral?: {
@@ -162,6 +162,18 @@ export interface InstanceTypeSpec {
cpu: QuanityCPU; cpu: QuanityCPU;
ram: QuanityMemory; ram: QuanityMemory;
}; };
unitResourcesParsed?: {
cpu: {
cores?: number;
unit: string;
num: number;
} | null;
ram: {
value: number;
unit: string;
num: number;
} | null;
};
} }
export interface InstanceTypeStatus { export interface InstanceTypeStatus {
+76 -53
View File
@@ -1,10 +1,6 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { import { ceilMilliToCore, parseQuantityToGi } from '@/pages/gpu-service/utils';
ceilMilliToCore,
parseQuantityToGi,
parseQuantityToNumber
} from '@/pages/gpu-service/utils';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import {
CheckboxField, CheckboxField,
@@ -198,10 +194,52 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
localStorage: null localStorage: null
}); });
const buildResourcesData = (
instanceType: InstanceTypeItem | undefined,
options: {
count: number;
}
) => {
const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed;
const acceleratable = instanceType?.spec?.acceleratable;
const { count = 0 } = options;
if (acceleratable) {
return {
accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores
: undefined,
ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value
: undefined
};
}
return {};
};
const buildResourcesDataForSubmit = (values: FormData) => {
const unitResourcesParsed = getUnitResources();
const accelerator = _.toNumber(values.spec?.resources?.accelerator) || 0;
const cpuNum = unitResourcesParsed?.cpu?.num;
const ramNum = unitResourcesParsed?.ram?.num;
return {
cpu:
accelerator > 0 && cpuNum
? `${accelerator * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}`
: `${values.spec?.resources?.cpu}`,
ram:
accelerator > 0 && ramNum
? `${accelerator * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
: values.spec?.resources?.ram
};
};
const resolveAndApply = ( const resolveAndApply = (
instanceType: InstanceTypeItem | undefined, instanceType: InstanceTypeItem | undefined,
count: number, count: number
options: { writeAccelerator?: boolean } = {}
) => { ) => {
if (!instanceType) { if (!instanceType) {
setSelectedInstanceType(undefined); setSelectedInstanceType(undefined);
@@ -214,40 +252,22 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
); );
setSelectedInstanceType(instanceType); setSelectedInstanceType(instanceType);
setOnceMaxRequest({ setOnceMaxRequest({
cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest), cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores,
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest), memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest)?.value,
localStorage: parseQuantityToGi(candidate?.localStorage?.onceMaxRequest) localStorage: parseQuantityToGi(candidate?.localStorage?.onceMaxRequest)
?.value
}); });
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 resources: {
? { resources: { accelerator: _.toString(count) } } ...buildResourcesData(instanceType, {
: {}), count
...(acceleratable })
? { }
resources: {
...(options.writeAccelerator
? { accelerator: _.toString(count) }
: {}),
...(cpuCores != null ? { cpu: cpuCores } : {}),
...(ramGi != null ? { ram: `${ramGi}Gi` } : {})
}
}
: {})
} as any } as any
}); });
}; };
@@ -293,8 +313,22 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
action === PageAction.VIEW || action === PageAction.VIEW ||
realAction === PageAction.CREATE realAction === PageAction.CREATE
) { ) {
console.log('currentData', currentData);
form.setFieldsValue({ form.setFieldsValue({
...currentData, ...currentData,
spec: {
...currentData?.spec,
resources: {
...currentData?.spec?.resources,
...buildResourcesData(
JSON.parse(currentData?.description || '{}'),
{
count:
_.toNumber(currentData?.spec?.resources?.accelerator) || 0
}
)
}
},
enable_ssh: !!currentData?.spec?.sshPublicKeys?.length, enable_ssh: !!currentData?.spec?.sshPublicKeys?.length,
storageMode: detectMode(currentData?.spec?.volume) storageMode: detectMode(currentData?.spec?.volume)
}); });
@@ -302,13 +336,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}, [action, currentData, form, open, realAction, instanceTypeList]); }, [action, currentData, form, open, realAction, instanceTypeList]);
const getUnitResources = () => { const getUnitResources = () => {
if (selectedInstanceType?.spec?.unitResources) { if (selectedInstanceType?.spec?.unitResourcesParsed) {
return selectedInstanceType.spec.unitResources; return selectedInstanceType.spec.unitResourcesParsed;
} }
try { try {
return ( return (
JSON.parse(currentData?.description || '{}')?.spec?.unitResources ?? JSON.parse(currentData?.description || '{}')?.spec
undefined ?.unitResourcesParsed ?? undefined
); );
} catch { } catch {
return undefined; return undefined;
@@ -329,26 +363,15 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}); });
} }
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 resources: {
...values.spec?.resources,
...buildResourcesDataForSubmit(values)
}
} }
}); });
}; };
@@ -377,7 +400,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
getFieldsValue: () => form.getFieldsValue(), getFieldsValue: () => form.getFieldsValue(),
applyInstanceType: (instanceType: InstanceTypeItem) => { applyInstanceType: (instanceType: InstanceTypeItem) => {
const count = instanceType.spec?.acceleratable ? 1 : 0; const count = instanceType.spec?.acceleratable ? 1 : 0;
resolveAndApply(instanceType, count, { writeAccelerator: true }); resolveAndApply(instanceType, count);
} }
})); }));
@@ -8,7 +8,6 @@ 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 { 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 {
@@ -102,7 +101,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
}; };
const renderMemoryLabel = (): React.ReactNode => { const renderMemoryLabel = (): React.ReactNode => {
if (isGPU) { if (isGPU || action === PageAction.EDIT || !onceMaxRequest?.memory) {
return intl.formatMessage({ id: 'gpuservice.template.memory' }); return intl.formatMessage({ id: 'gpuservice.template.memory' });
} }
@@ -235,40 +234,24 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Form.Item<FormData> <Form.Item<FormData>
name={['spec', 'resources', 'ram']} name={['spec', 'resources', 'ram']}
normalize={(value) => (value ? `${value}Gi` : undefined)} normalize={(value) => (value ? `${value}Gi` : null)}
getValueProps={(value) => { getValueProps={(value) => ({
if (!value) return { value: '' }; value: _.toString(value).replace(/Gi$/, '')
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 <InputNumber
disabled={isGPU || disabled} disabled={isGPU || disabled || action === PageAction.EDIT}
label={renderMemoryLabel()} label={renderMemoryLabel()}
max={onceMaxRequest?.memory ?? undefined} max={onceMaxRequest?.memory ?? undefined}
/> />
</Form.Item> </Form.Item>
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Form.Item<FormData> <Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
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 <InputNumber
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)} label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
max={onceMaxRequest?.cpu ?? undefined} max={onceMaxRequest?.cpu ?? undefined}
disabled={disabled || isGPU} disabled={disabled || isGPU || action === PageAction.EDIT}
/> />
</Form.Item> </Form.Item>
</div> </div>
+1 -1
View File
@@ -88,7 +88,7 @@ const GPUService: React.FC = () => {
} = useViewEvents(); } = useViewEvents();
const { const {
fetchClusterList, fetchClusterList,
cancelRequest: cancelClusterRequest, cancelRequest,
clusterList, clusterList,
loading: clusterLoading loading: clusterLoading
} = useQueryClusterList(); } = useQueryClusterList();
@@ -1,5 +1,6 @@
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import React from 'react'; import React from 'react';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
import { queryGPUServiceInstanceTypes } from '../apis'; import { queryGPUServiceInstanceTypes } from '../apis';
import { getAcceleratorMax } from '../config'; import { getAcceleratorMax } from '../config';
import { InstanceTypeItem } from '../config/types'; import { InstanceTypeItem } from '../config/types';
@@ -44,13 +45,33 @@ export default function useQueryInstanceTypes() {
const list = (res?.items || []).map((item) => { const list = (res?.items || []).map((item) => {
const remainingData = isAvailable(item); const remainingData = isAvailable(item);
const rawMax = item.status?.onceMaxRequest;
return { return {
...item, ...item,
spec: {
...item.spec,
unitResourcesParsed: {
cpu: ceilMilliToCore(item.spec?.unitResources?.cpu ?? null),
ram: parseQuantityToGi(item.spec?.unitResources?.ram ?? null)
}
},
status: {
...item.status,
onceMaxRequest: {
...rawMax,
cpu: rawMax?.cpu ? `${ceilMilliToCore(rawMax.cpu)?.cores}` : '',
ram: rawMax?.ram ? `${parseQuantityToGi(rawMax.ram)?.value}` : '',
localStorage: rawMax?.localStorage
? `${parseQuantityToGi(rawMax.localStorage)?.value}`
: ''
}
},
maxAccelerator: remainingData.maxAccelerator, maxAccelerator: remainingData.maxAccelerator,
disabled: !remainingData.available disabled: !remainingData.available
}; };
}); });
console.log('queryInstanceTypes', list);
setDataList(list); setDataList(list);
return list; return list;
}; };
+81 -26
View File
@@ -20,34 +20,89 @@ export const parseQuantityToNumber = (value?: string | null): number | null => {
return Number.isFinite(num) && num > 0 ? num : null; return Number.isFinite(num) && num > 0 ? num : null;
}; };
const GI_FROM_UNIT: Record<string, number> = { const UNIT_FACTOR: Record<string, number> = {
Ki: 1 / (1024 * 1024), '': 1,
Mi: 1 / 1024, n: 1e-9,
Gi: 1, u: 1e-6,
Ti: 1024 m: 1e-3,
k: 1e3,
M: 1e6,
G: 1e9,
T: 1e12,
P: 1e15,
E: 1e18,
Ki: 1024,
Mi: 1024 ** 2,
Gi: 1024 ** 3,
Ti: 1024 ** 4,
Pi: 1024 ** 5,
Ei: 1024 ** 6
}; };
// Converts a K8s-style quantity ("24314504Ki", "2Ti", "10Gi", or a bare export const parseQuantity = (
// number assumed to be bytes) to an integer GiB. Unit suffix is value?: string | null
// case-insensitive. ): { num: number; unit: string; base: number } | null => {
export const parseQuantityToGi = (value?: string | null): number | null => { if (value == null) return null;
if (!value) return null; const str = String(value).trim();
const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)?$/i.exec(String(value)); const len = str.length;
if (!match) return null; if (len === 0) 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 => { let unit = '';
if (!value) return null; let numEnd = len;
const match = /^(-?\d+(?:\.\d+)?)m?$/.exec(value); if (len >= 2 && str[len - 1] === 'i') {
if (!match) return null; unit = str.slice(len - 2);
const num = Number(match[1]); if (UNIT_FACTOR[unit] === undefined) return null;
numEnd = len - 2;
} else {
const last = str[len - 1];
const code = last.charCodeAt(0);
const isDigit = code >= 48 && code <= 57;
if (!isDigit && UNIT_FACTOR[last] !== undefined) {
unit = last;
numEnd = len - 1;
}
}
if (numEnd === 0) return null;
const num = Number(str.slice(0, numEnd));
if (!Number.isFinite(num)) return null; if (!Number.isFinite(num)) return null;
return String(Math.ceil(num / 1000)); const base = num * UNIT_FACTOR[unit];
if (!Number.isFinite(base)) return null;
return { num, unit, base };
};
export const parseQuantityToGi = (
value?: string | null
): { value: number; unit: string; num: number } | null => {
if (!value) return null;
const normalized = String(value).replace(
/(ki|mi|gi|ti|pi|ei)$/i,
(m) => m[0].toUpperCase() + m[1].toLowerCase()
);
const parsed = parseQuantity(normalized);
if (!parsed || parsed.base <= 0) return null;
return {
value: _.floor(parsed.base / UNIT_FACTOR.Gi, 0),
unit: parsed.unit,
num: parsed.num
};
};
export const ceilMilliToCore = (
value?: string | null,
unit?: string
): { cores: number; unit: string; num: number } | null => {
const parsed = parseQuantity(value);
if (!parsed) return null;
const outUnit = unit ?? parsed.unit;
const outFactor = UNIT_FACTOR[outUnit];
if (outFactor === undefined) return null;
return {
cores: Math.ceil(parsed.base),
unit: outUnit,
num: parsed.base / outFactor
};
}; };