diff --git a/src/pages/_components/number-selection/index.tsx b/src/pages/_components/number-selection/index.tsx new file mode 100644 index 00000000..f4eca299 --- /dev/null +++ b/src/pages/_components/number-selection/index.tsx @@ -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 = ({ + 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(() => + 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 ( +
+ {label !== undefined && label !== null && ( +
+ +
+ )} +
+ {items.map((num) => { + return ( +
handleSelect(num)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSelect(num); + } + }} + > + {num} +
+ ); + })} +
+ {showCustomInput && ( +
+
+
+ +
+
+ )} +
+ ); +}; + +export default NumberSelection; diff --git a/src/pages/_components/number-selection/styles.less b/src/pages/_components/number-selection/styles.less new file mode 100644 index 00000000..4694f712 --- /dev/null +++ b/src/pages/_components/number-selection/styles.less @@ -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; + } + } + } +} diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 042ad5d9..a40f68fb 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -224,13 +224,14 @@ const GPUServiceInstanceForm: React.FC = 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 = forwardRef( id: 'gpuservice.instance.section.type' }), forceRender: true, - children: + children: }, { key: TABKeysMap.TEMPLATE, diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index 8403d64b..17070707 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -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 = ({ ); }; -const InstanceTypeFormItem = () => { +interface InstanceTypeFormItemProps { + action: PageActionType; +} + +const InstanceTypeFormItem: React.FC = ({ + action +}) => { const intl = useIntl(); const form = Form.useFormInstance(); 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 = () => { } ]} > - - {intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} - - ( - {intl.formatMessage( - { id: 'common.max' }, - { count: maxGpuCount } - )} - ) - - - } - required + step={1} + disabled={action === PageAction.EDIT} + label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} /> )} diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index 219ec090..2f315b19 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -106,16 +106,17 @@ const StorageVolume = ({ } ]} /> - {action === PageAction.CREATE && ( - - )} + {action === PageAction.CREATE && + storageMode === StorageModeValueMap.Existing && ( + + )} {storageMode === StorageModeValueMap.Existing && (