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]);
|
||||
|
||||
const handleFinish = async (values: InstanceFormValues) => {
|
||||
await onFinish({
|
||||
...values,
|
||||
spec: {
|
||||
...values.spec,
|
||||
sshPublicKey: { name: sshKeyData?.name }
|
||||
}
|
||||
});
|
||||
console.log('values=========', values);
|
||||
// await onFinish({
|
||||
// ...values,
|
||||
// spec: {
|
||||
// ...values.spec,
|
||||
// sshPublicKey: { name: sshKeyData?.name }
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -305,7 +306,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
id: 'gpuservice.instance.section.type'
|
||||
}),
|
||||
forceRender: true,
|
||||
children: <InstanceTypeFormItem />
|
||||
children: <InstanceTypeFormItem action={action} />
|
||||
},
|
||||
{
|
||||
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 { Flex, Form } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
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 form = Form.useFormInstance<FormData>();
|
||||
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
|
||||
@@ -65,8 +73,8 @@ const InstanceTypeFormItem = () => {
|
||||
const maxGpuCount = useMemo(() => {
|
||||
const onceMaxRequest =
|
||||
selectedInstanceType?.status?.accelerator?.onceMaxRequest;
|
||||
const num = onceMaxRequest ? Number(onceMaxRequest) : NaN;
|
||||
return Number.isFinite(num) && num > 0 ? num : 1;
|
||||
const num = onceMaxRequest ? Number(onceMaxRequest) : 0;
|
||||
return num;
|
||||
}, [selectedInstanceType]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -135,24 +143,12 @@ const InstanceTypeFormItem = () => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CInputNumber
|
||||
<NumberSelection
|
||||
min={1}
|
||||
max={maxGpuCount}
|
||||
precision={0}
|
||||
label={
|
||||
<Flex gap={4} align="center">
|
||||
{intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
|
||||
<span>
|
||||
(
|
||||
{intl.formatMessage(
|
||||
{ id: 'common.max' },
|
||||
{ count: maxGpuCount }
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
</Flex>
|
||||
}
|
||||
required
|
||||
step={1}
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
@@ -106,16 +106,17 @@ const StorageVolume = ({
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{action === PageAction.CREATE && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ marginBottom: 6 }}
|
||||
onClick={() => setOverlayOpen(true)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||
</Button>
|
||||
)}
|
||||
{action === PageAction.CREATE &&
|
||||
storageMode === StorageModeValueMap.Existing && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ marginBottom: 6 }}
|
||||
onClick={() => setOverlayOpen(true)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{storageMode === StorageModeValueMap.Existing && (
|
||||
|
||||
Reference in New Issue
Block a user