fix: instance type api update
This commit is contained in:
@@ -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" />
|
<span className="meta-label">{label}</span>
|
||||||
<Flex align="center" gap={4}>
|
<span className="meta-value">{value || '-'}</span>
|
||||||
<span>{label}</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 }}>
|
<span className="meta-row">
|
||||||
{acceleratable && (
|
{acceleratable && (
|
||||||
<span className="meta-row">
|
<>
|
||||||
<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 {
|
||||||
|
|||||||
@@ -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,49 +533,48 @@ 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']}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 12
|
marginBottom: 12
|
||||||
}}
|
}}
|
||||||
hidden={!sshEnabled}
|
hidden={!sshEnabled}
|
||||||
normalize={(value) =>
|
normalize={(value) =>
|
||||||
Array.isArray(value)
|
Array.isArray(value)
|
||||||
? value?.map((item) => ({ name: item }))
|
? value?.map((item) => ({ name: item }))
|
||||||
: []
|
: []
|
||||||
|
}
|
||||||
|
getValueProps={(value) => ({
|
||||||
|
value: Array.isArray(value)
|
||||||
|
? value.map((item) => item?.name ?? item)
|
||||||
|
: []
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: sshEnabled,
|
||||||
|
message: getRuleMessage('select', 'gpuservice.publicKey')
|
||||||
}
|
}
|
||||||
getValueProps={(value) => ({
|
]}
|
||||||
value: Array.isArray(value)
|
>
|
||||||
? value.map((item) => item?.name ?? item)
|
<MultipleSelect
|
||||||
: []
|
required
|
||||||
|
disabled={disabled}
|
||||||
|
mode="multiple"
|
||||||
|
maxTagCount={1}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'gpuservice.instance.ssh.assignKey'
|
||||||
})}
|
})}
|
||||||
rules={[
|
styles={{
|
||||||
{
|
wrapper: {
|
||||||
required: sshEnabled,
|
width: '100%'
|
||||||
message: getRuleMessage('select', 'gpuservice.publicKey')
|
|
||||||
}
|
}
|
||||||
]}
|
}}
|
||||||
>
|
options={sshkeyOptions}
|
||||||
<MultipleSelect
|
></MultipleSelect>
|
||||||
required
|
</Form.Item>
|
||||||
disabled={disabled}
|
</div>
|
||||||
mode="multiple"
|
|
||||||
maxTagCount={1}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: 'gpuservice.instance.ssh.assignKey'
|
|
||||||
})}
|
|
||||||
styles={{
|
|
||||||
wrapper: {
|
|
||||||
width: '100%'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={sshkeyOptions}
|
|
||||||
></MultipleSelect>
|
|
||||||
</Form.Item>
|
|
||||||
</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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user