fix: parse quality data
This commit is contained in:
@@ -268,6 +268,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
await onOk({
|
||||
...values
|
||||
});
|
||||
console.log('submit form values', values);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ceilMilliToCore } from '@/pages/gpu-service/utils';
|
||||
import { AutoTooltip, IconFont, ThemeTag } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Flex } from 'antd';
|
||||
@@ -62,7 +61,7 @@ interface InstanceTypeItemProps {
|
||||
const MetaItem: React.FC<{
|
||||
icon: string;
|
||||
label?: string;
|
||||
value?: string | null;
|
||||
value?: string | null | number;
|
||||
showDot?: boolean;
|
||||
show?: boolean;
|
||||
}> = ({ icon, label, value, showDot = true, show = true }) => {
|
||||
@@ -91,15 +90,13 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
const onceMaxRequestData = item.status?.onceMaxRequest || {};
|
||||
|
||||
// RAM resource
|
||||
const ramRaw = acceleratable
|
||||
? specData.unitResources?.ram
|
||||
const ramUnit = acceleratable
|
||||
? specData.unitResourcesParsed?.ram?.value
|
||||
: onceMaxRequestData.ram;
|
||||
|
||||
console.log('InstanceTypeItem', item.name, 'ramRaw', ramRaw);
|
||||
|
||||
// CPU resource
|
||||
const cpuRaw = acceleratable
|
||||
? specData.unitResources?.cpu
|
||||
const cpuUnitCores = acceleratable
|
||||
? specData.unitResourcesParsed?.cpu?.cores
|
||||
: onceMaxRequestData.cpu;
|
||||
|
||||
const renderName = () => {
|
||||
@@ -159,7 +156,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
},
|
||||
{ count: '' }
|
||||
)}
|
||||
value={`${item.maxAccelerator || '-'}`}
|
||||
value={`${item.maxAccelerator || 0}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -169,14 +166,14 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
|
||||
showDot={false}
|
||||
icon="icon-ram-02"
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
|
||||
value={toDisplayUnit(convertKiToGi(ramRaw)) ?? '-'}
|
||||
value={ramUnit ? `${ramUnit}GB` : '-'}
|
||||
/>
|
||||
<MetaItem
|
||||
show={!!cpuRaw}
|
||||
show={!!cpuUnitCores}
|
||||
showDot={true}
|
||||
icon="icon-cpu"
|
||||
label="CPU"
|
||||
value={ceilMilliToCore(cpuRaw) ?? '-'}
|
||||
value={cpuUnitCores || '-'}
|
||||
/>
|
||||
</span>
|
||||
</Meta>
|
||||
|
||||
@@ -41,10 +41,10 @@ export interface FormData {
|
||||
}[];
|
||||
volumeMount: string;
|
||||
resources: {
|
||||
cpu?: string;
|
||||
ram?: string;
|
||||
localStorage?: string;
|
||||
accelerator?: number | string;
|
||||
cpu: string | null | number;
|
||||
ram: string | null | number;
|
||||
localStorage: string | null | number;
|
||||
accelerator: number | string | null;
|
||||
};
|
||||
volume: {
|
||||
ephemeral?: {
|
||||
@@ -162,6 +162,18 @@ export interface InstanceTypeSpec {
|
||||
cpu: QuanityCPU;
|
||||
ram: QuanityMemory;
|
||||
};
|
||||
unitResourcesParsed?: {
|
||||
cpu: {
|
||||
cores?: number;
|
||||
unit: string;
|
||||
num: number;
|
||||
} | null;
|
||||
ram: {
|
||||
value: number;
|
||||
unit: string;
|
||||
num: number;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstanceTypeStatus {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import {
|
||||
ceilMilliToCore,
|
||||
parseQuantityToGi,
|
||||
parseQuantityToNumber
|
||||
} from '@/pages/gpu-service/utils';
|
||||
import { ceilMilliToCore, parseQuantityToGi } from '@/pages/gpu-service/utils';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
CheckboxField,
|
||||
@@ -198,10 +194,52 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
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 = (
|
||||
instanceType: InstanceTypeItem | undefined,
|
||||
count: number,
|
||||
options: { writeAccelerator?: boolean } = {}
|
||||
count: number
|
||||
) => {
|
||||
if (!instanceType) {
|
||||
setSelectedInstanceType(undefined);
|
||||
@@ -214,40 +252,22 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
);
|
||||
|
||||
setSelectedInstanceType(instanceType);
|
||||
|
||||
setOnceMaxRequest({
|
||||
cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest),
|
||||
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest),
|
||||
cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores,
|
||||
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest)?.value,
|
||||
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({
|
||||
clusterId: candidate?.cluster ? _.toNumber(candidate.cluster) : null,
|
||||
spec: {
|
||||
type: candidate?.name || '',
|
||||
...(options.writeAccelerator
|
||||
? { resources: { accelerator: _.toString(count) } }
|
||||
: {}),
|
||||
...(acceleratable
|
||||
? {
|
||||
resources: {
|
||||
...(options.writeAccelerator
|
||||
? { accelerator: _.toString(count) }
|
||||
: {}),
|
||||
...(cpuCores != null ? { cpu: cpuCores } : {}),
|
||||
...(ramGi != null ? { ram: `${ramGi}Gi` } : {})
|
||||
}
|
||||
}
|
||||
: {})
|
||||
resources: {
|
||||
...buildResourcesData(instanceType, {
|
||||
count
|
||||
})
|
||||
}
|
||||
} as any
|
||||
});
|
||||
};
|
||||
@@ -293,8 +313,22 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
action === PageAction.VIEW ||
|
||||
realAction === PageAction.CREATE
|
||||
) {
|
||||
console.log('currentData', currentData);
|
||||
form.setFieldsValue({
|
||||
...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,
|
||||
storageMode: detectMode(currentData?.spec?.volume)
|
||||
});
|
||||
@@ -302,13 +336,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}, [action, currentData, form, open, realAction, instanceTypeList]);
|
||||
|
||||
const getUnitResources = () => {
|
||||
if (selectedInstanceType?.spec?.unitResources) {
|
||||
return selectedInstanceType.spec.unitResources;
|
||||
if (selectedInstanceType?.spec?.unitResourcesParsed) {
|
||||
return selectedInstanceType.spec.unitResourcesParsed;
|
||||
}
|
||||
try {
|
||||
return (
|
||||
JSON.parse(currentData?.description || '{}')?.spec?.unitResources ??
|
||||
undefined
|
||||
JSON.parse(currentData?.description || '{}')?.spec
|
||||
?.unitResourcesParsed ?? undefined
|
||||
);
|
||||
} catch {
|
||||
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({
|
||||
..._.omit(values, ['enable_ssh']),
|
||||
spec: {
|
||||
...values.spec,
|
||||
ports: submittedPorts,
|
||||
resources: submittedResources
|
||||
resources: {
|
||||
...values.spec?.resources,
|
||||
...buildResourcesDataForSubmit(values)
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -377,7 +400,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
getFieldsValue: () => form.getFieldsValue(),
|
||||
applyInstanceType: (instanceType: InstanceTypeItem) => {
|
||||
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 styled from 'styled-components';
|
||||
import { BasicResourceMax } from '../../templates/forms/basic';
|
||||
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
|
||||
import InstanceTypeItem from '../components/instance-type-item';
|
||||
import { getAcceleratorMax } from '../config';
|
||||
import {
|
||||
@@ -102,7 +101,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
};
|
||||
|
||||
const renderMemoryLabel = (): React.ReactNode => {
|
||||
if (isGPU) {
|
||||
if (isGPU || action === PageAction.EDIT || !onceMaxRequest?.memory) {
|
||||
return intl.formatMessage({ id: 'gpuservice.template.memory' });
|
||||
}
|
||||
|
||||
@@ -235,40 +234,24 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||
<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 };
|
||||
}}
|
||||
normalize={(value) => (value ? `${value}Gi` : null)}
|
||||
getValueProps={(value) => ({
|
||||
value: _.toString(value).replace(/Gi$/, '')
|
||||
})}
|
||||
>
|
||||
<InputNumber
|
||||
disabled={isGPU || disabled}
|
||||
disabled={isGPU || disabled || action === PageAction.EDIT}
|
||||
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
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
|
||||
<InputNumber
|
||||
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
|
||||
max={onceMaxRequest?.cpu ?? undefined}
|
||||
disabled={disabled || isGPU}
|
||||
disabled={disabled || isGPU || action === PageAction.EDIT}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ const GPUService: React.FC = () => {
|
||||
} = useViewEvents();
|
||||
const {
|
||||
fetchClusterList,
|
||||
cancelRequest: cancelClusterRequest,
|
||||
cancelRequest,
|
||||
clusterList,
|
||||
loading: clusterLoading
|
||||
} = useQueryClusterList();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import React from 'react';
|
||||
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
|
||||
import { queryGPUServiceInstanceTypes } from '../apis';
|
||||
import { getAcceleratorMax } from '../config';
|
||||
import { InstanceTypeItem } from '../config/types';
|
||||
@@ -44,13 +45,33 @@ export default function useQueryInstanceTypes() {
|
||||
|
||||
const list = (res?.items || []).map((item) => {
|
||||
const remainingData = isAvailable(item);
|
||||
const rawMax = item.status?.onceMaxRequest;
|
||||
|
||||
return {
|
||||
...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,
|
||||
disabled: !remainingData.available
|
||||
};
|
||||
});
|
||||
console.log('queryInstanceTypes', list);
|
||||
|
||||
setDataList(list);
|
||||
return list;
|
||||
};
|
||||
|
||||
@@ -20,34 +20,89 @@ export const parseQuantityToNumber = (value?: string | null): number | null => {
|
||||
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
|
||||
const UNIT_FACTOR: Record<string, number> = {
|
||||
'': 1,
|
||||
n: 1e-9,
|
||||
u: 1e-6,
|
||||
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
|
||||
// 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 parseQuantity = (
|
||||
value?: string | null
|
||||
): { num: number; unit: string; base: number } | null => {
|
||||
if (value == null) return null;
|
||||
const str = String(value).trim();
|
||||
const len = str.length;
|
||||
if (len === 0) return null;
|
||||
|
||||
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]);
|
||||
let unit = '';
|
||||
let numEnd = len;
|
||||
if (len >= 2 && str[len - 1] === 'i') {
|
||||
unit = str.slice(len - 2);
|
||||
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;
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user