feat: add number selection
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
import { LabelInfo } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { InputNumber } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from './styles.less';
|
||||||
|
|
||||||
|
interface NumberSelectionProps {
|
||||||
|
id?: string;
|
||||||
|
step?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
value?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
required?: boolean;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
styles?: {
|
||||||
|
input?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
onChange?: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRESET_ITEMS = [1, 2, 4, 8];
|
||||||
|
|
||||||
|
const NumberSelection: React.FC<NumberSelectionProps> = ({
|
||||||
|
id,
|
||||||
|
step = 1,
|
||||||
|
min = 1,
|
||||||
|
max = 16,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const showCustomInput = max > 8;
|
||||||
|
const computedItems = showCustomInput
|
||||||
|
? PRESET_ITEMS.filter((n) => n >= min && n <= max)
|
||||||
|
: Array.from(
|
||||||
|
{ length: Math.max(0, Math.floor((max - min) / step) + 1) },
|
||||||
|
(_, i) => min + i * step
|
||||||
|
);
|
||||||
|
const items = computedItems.length === 0 ? [0] : computedItems;
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState<number | null>(() =>
|
||||||
|
value !== undefined && value !== null && !PRESET_ITEMS.includes(value)
|
||||||
|
? value
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
setInputValue(null);
|
||||||
|
} else if (PRESET_ITEMS.includes(value)) {
|
||||||
|
setInputValue(null);
|
||||||
|
} else {
|
||||||
|
setInputValue(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleSelect = (num: number) => {
|
||||||
|
if (disabled || num === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputValue(null);
|
||||||
|
onChange?.(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (num: number | null) => {
|
||||||
|
setInputValue(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitInput = () => {
|
||||||
|
if (disabled || inputValue === null || inputValue === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange?.(inputValue);
|
||||||
|
};
|
||||||
|
console.log('value+++', value, 'inputValue', items, inputValue);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
className={classNames(styles.wrapper, className, {
|
||||||
|
[styles.disabled]: disabled
|
||||||
|
})}
|
||||||
|
style={style}
|
||||||
|
role="radiogroup"
|
||||||
|
>
|
||||||
|
{label !== undefined && label !== null && (
|
||||||
|
<div className={styles.label}>
|
||||||
|
<LabelInfo label={label} required={required}></LabelInfo>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.content}>
|
||||||
|
{items.map((num) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={num}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={num === value}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
className={classNames(styles.numberItem, {
|
||||||
|
[styles.active]: num === value
|
||||||
|
})}
|
||||||
|
onClick={() => handleSelect(num)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(num);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{showCustomInput && (
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<div className={styles.line}></div>
|
||||||
|
<div
|
||||||
|
className={classNames(styles.inputContainer, {
|
||||||
|
[styles.hasValue]: !!inputValue
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
controls
|
||||||
|
variant="borderless"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
disabled={disabled}
|
||||||
|
value={inputValue || undefined}
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
width: 140,
|
||||||
|
backgroundColor: 'var(--ant-color-bg-container)',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
fontWeight: inputValue ? 500 : 400,
|
||||||
|
...(styles?.input as React.CSSProperties)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({ id: 'common.option.other' })}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={commitInput}
|
||||||
|
onPressEnter={commitInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NumberSelection;
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--ant-border-radius-lg);
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--ant-color-fill-quaternary);
|
||||||
|
.inputContainer {
|
||||||
|
&.hasValue {
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
:global {
|
||||||
|
.ant-input-number-action {
|
||||||
|
border-color: var(--ant-color-split) !important;
|
||||||
|
}
|
||||||
|
.ant-input-number-actions {
|
||||||
|
width: 32px !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
margin-inline: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.inputWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
height: 22px;
|
||||||
|
border-left: 1px solid var(--ant-color-split);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-right: 1px solid var(--ant-color-split);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.numberItem {
|
||||||
|
position: relative;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
background-color 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--ant-color-fill-secondary);
|
||||||
|
}
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: var(--ant-color-primary);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--ant-color-bg-container);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
border-color: var(--ant-color-split);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--ant-color-bg-container-disabled);
|
||||||
|
.label {
|
||||||
|
color: var(--ant-color-text-disabled);
|
||||||
|
}
|
||||||
|
.numberItem {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--ant-color-text-disabled);
|
||||||
|
&:hover:not(.active) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -224,13 +224,14 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
}, [action, currentData, form, open, namespace]);
|
}, [action, currentData, form, open, namespace]);
|
||||||
|
|
||||||
const handleFinish = async (values: InstanceFormValues) => {
|
const handleFinish = async (values: InstanceFormValues) => {
|
||||||
await onFinish({
|
console.log('values=========', values);
|
||||||
...values,
|
// await onFinish({
|
||||||
spec: {
|
// ...values,
|
||||||
...values.spec,
|
// spec: {
|
||||||
sshPublicKey: { name: sshKeyData?.name }
|
// ...values.spec,
|
||||||
}
|
// sshPublicKey: { name: sshKeyData?.name }
|
||||||
});
|
// }
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
@@ -305,7 +306,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
id: 'gpuservice.instance.section.type'
|
id: 'gpuservice.instance.section.type'
|
||||||
}),
|
}),
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
children: <InstanceTypeFormItem />
|
children: <InstanceTypeFormItem action={action} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: TABKeysMap.TEMPLATE,
|
key: TABKeysMap.TEMPLATE,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { InputNumber as CInputNumber } from '@gpustack/core-ui';
|
import { PageAction } from '@/config';
|
||||||
|
import { PageActionType } from '@/config/types';
|
||||||
|
import NumberSelection from '@/pages/_components/number-selection';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Flex, Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import InstanceTypeItem from '../components/instance-type-item';
|
import InstanceTypeItem from '../components/instance-type-item';
|
||||||
@@ -45,7 +47,13 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InstanceTypeFormItem = () => {
|
interface InstanceTypeFormItemProps {
|
||||||
|
action: PageActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
||||||
|
action
|
||||||
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
|
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
|
||||||
@@ -65,8 +73,8 @@ const InstanceTypeFormItem = () => {
|
|||||||
const maxGpuCount = useMemo(() => {
|
const maxGpuCount = useMemo(() => {
|
||||||
const onceMaxRequest =
|
const onceMaxRequest =
|
||||||
selectedInstanceType?.status?.accelerator?.onceMaxRequest;
|
selectedInstanceType?.status?.accelerator?.onceMaxRequest;
|
||||||
const num = onceMaxRequest ? Number(onceMaxRequest) : NaN;
|
const num = onceMaxRequest ? Number(onceMaxRequest) : 0;
|
||||||
return Number.isFinite(num) && num > 0 ? num : 1;
|
return num;
|
||||||
}, [selectedInstanceType]);
|
}, [selectedInstanceType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,24 +143,12 @@ const InstanceTypeFormItem = () => {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CInputNumber
|
<NumberSelection
|
||||||
min={1}
|
min={1}
|
||||||
max={maxGpuCount}
|
max={maxGpuCount}
|
||||||
precision={0}
|
step={1}
|
||||||
label={
|
disabled={action === PageAction.EDIT}
|
||||||
<Flex gap={4} align="center">
|
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
|
|
||||||
<span>
|
|
||||||
(
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: 'common.max' },
|
|
||||||
{ count: maxGpuCount }
|
|
||||||
)}
|
|
||||||
)
|
|
||||||
</span>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -106,16 +106,17 @@ const StorageVolume = ({
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{action === PageAction.CREATE && (
|
{action === PageAction.CREATE &&
|
||||||
<Button
|
storageMode === StorageModeValueMap.Existing && (
|
||||||
type="link"
|
<Button
|
||||||
size="small"
|
type="link"
|
||||||
style={{ marginBottom: 6 }}
|
size="small"
|
||||||
onClick={() => setOverlayOpen(true)}
|
style={{ marginBottom: 6 }}
|
||||||
>
|
onClick={() => setOverlayOpen(true)}
|
||||||
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
>
|
||||||
</Button>
|
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{storageMode === StorageModeValueMap.Existing && (
|
{storageMode === StorageModeValueMap.Existing && (
|
||||||
|
|||||||
Reference in New Issue
Block a user