From 4f02bcd40c3a14f66ecda9d44f16024a9c2c46d6 Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 4 Mar 2026 10:52:31 +0800 Subject: [PATCH] feat: paste multiple lines in parameters input --- src/components/list-input/hint-input.tsx | 12 ++++++- src/components/list-input/index.tsx | 36 +++++++++++++++++++ src/components/list-input/list-item.tsx | 7 +++- src/components/seal-form/auto-complete.tsx | 2 ++ .../cluster-management/cluster-detail.tsx | 14 ++------ .../hooks/use-cluster-columns.tsx | 8 ++--- .../llmodels/components/hf-model-file.tsx | 2 +- src/pages/llmodels/index.tsx | 6 ++-- src/pages/resources/components/gpus.tsx | 15 +++----- src/pages/resources/components/workers.tsx | 27 +++----------- .../resources/hooks/use-worker-columns.tsx | 5 +-- 11 files changed, 76 insertions(+), 58 deletions(-) diff --git a/src/components/list-input/hint-input.tsx b/src/components/list-input/hint-input.tsx index 737d4927..5884ad27 100644 --- a/src/components/list-input/hint-input.tsx +++ b/src/components/list-input/hint-input.tsx @@ -7,6 +7,7 @@ interface HintInputProps { label?: string; onChange: (value: string) => void; onBlur?: (e: any) => void; + onPaste?: (e: any) => void; placeholder?: string; trim?: boolean; sourceOptions?: Global.HintOptions[]; @@ -15,7 +16,15 @@ interface HintInputProps { const matchReg = /[^=]+=[^=]*$/; const HintInput: React.FC = (props) => { - const { value, label, onChange, onBlur, sourceOptions, trim = true } = props; + const { + value, + label, + onChange, + onBlur, + onPaste, + sourceOptions, + trim = true + } = props; const cursorPosRef = React.useRef(0); const contextBeforeCursorRef = React.useRef(''); const [options, setOptions] = React.useState< @@ -92,6 +101,7 @@ const HintInput: React.FC = (props) => { options={options} trim={trim} style={{ flex: 1 }} + onPaste={onPaste} /> ); }; diff --git a/src/components/list-input/index.tsx b/src/components/list-input/index.tsx index 56d68b2f..862c3fde 100644 --- a/src/components/list-input/index.tsx +++ b/src/components/list-input/index.tsx @@ -79,6 +79,41 @@ const ListInput: React.FC = (props) => { setList(values); }; + const handleOnPaste = (e: any, index: number) => { + const pastedText = e.clipboardData?.getData('text'); + if (!pastedText) return; + + const lines = pastedText.split(/\r?\n/).filter((line: string) => { + const trimmedLine = trim ? line.trim() : line; + return trimmedLine.length > 0; + }); + + if (lines.length <= 1) { + // if there's only one line, let the default paste behavior handle it + return; + } + + e.preventDefault(); + + const values = _.cloneDeep(list); + + // replace the current item with the first line of the pasted text + values[index].value = trim ? lines[0].trim() : lines[0]; + + // create new list items for the remaining lines + for (let i = 1; i < lines.length; i++) { + updateCountRef(); + values.splice(index + i, 0, { + value: trim ? lines[i].trim() : lines[i], + uid: countRef.current + }); + } + + const valueList = _.map(values, 'value').filter((val: string) => !!val); + setList(values); + onChange(valueList); + }; + React.useEffect(() => { const valueList = _.map(list, 'value').filter((val: string) => !!val); if (!_.isEqual(valueList, dataList)) { @@ -122,6 +157,7 @@ const ListInput: React.FC = (props) => { onBlur={(e) => onBlur?.(e, index)} onRemove={() => handleOnRemove(index)} onChange={(val) => handleOnChange(val, index)} + onPaste={(e) => handleOnPaste(e, index)} trim={trim} renderItem={renderItem} /> diff --git a/src/components/list-input/list-item.tsx b/src/components/list-input/list-item.tsx index 52ff656c..8e768347 100644 --- a/src/components/list-input/list-item.tsx +++ b/src/components/list-input/list-item.tsx @@ -8,11 +8,13 @@ interface LabelItemProps { onRemove: () => void; onChange: (value: string) => void; onBlur?: (e: any) => void; + onPaste?: (e: any) => void; renderItem?: ( data: any, props: { onChange: (value: string) => void; onBlur?: (e: any) => void; + onPaste?: (e: any) => void; } ) => React.ReactNode; value: string; @@ -29,6 +31,7 @@ const ListItem: React.FC = (props) => { onRemove, onChange, onBlur, + onPaste, label, value, options, @@ -47,13 +50,15 @@ const ListItem: React.FC = (props) => { {renderItem ? ( renderItem(data, { onChange: handleOnChange, - onBlur + onBlur, + onPaste }) ) : ( void; onInput?: (e: Event) => void; clearSpaceOnBlur?: boolean; } @@ -148,6 +149,7 @@ const SealAutoComplete: React.FC< onChange={handleChange} popupRender={popupRender} onInput={handleOnInput} + onPaste={props.onPaste} > diff --git a/src/pages/cluster-management/cluster-detail.tsx b/src/pages/cluster-management/cluster-detail.tsx index a27c9b83..3389ba42 100644 --- a/src/pages/cluster-management/cluster-detail.tsx +++ b/src/pages/cluster-management/cluster-detail.tsx @@ -46,27 +46,19 @@ const ClusterDetailModal = () => { key: 'workers', label: `Workers`, icon: , - children: ( - - ) + children: }, { key: 'deployments', label: `Deployments`, icon: , - children: + children: }, { key: 'gpus', label: `GPUs`, icon: , - children: + children: } ]} /> diff --git a/src/pages/cluster-management/hooks/use-cluster-columns.tsx b/src/pages/cluster-management/hooks/use-cluster-columns.tsx index 893505d2..0fe78f34 100644 --- a/src/pages/cluster-management/hooks/use-cluster-columns.tsx +++ b/src/pages/cluster-management/hooks/use-cluster-columns.tsx @@ -9,7 +9,7 @@ import { tableSorter } from '@/config/settings'; import GrafanaIcon from '@/pages/_components/grafana-icon'; import { StarFilled } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Tooltip } from 'antd'; +import { Tooltip, Typography } from 'antd'; import dayjs from 'dayjs'; import { useAtomValue } from 'jotai'; import { useMemo } from 'react'; @@ -100,9 +100,9 @@ const useClusterColumns = ( span: 3, render: (text: string, record: ClusterListItem) => ( <> - - {text} - + onCellClick?.(record, 'name')}> + {record.name} + {record.is_default && ( = forwardRef((props, ref) => { }} options={modelFilesSortOptions.current} size="middle" - style={{ width: '120px' }} + style={{ width: '120px', fontWeight: 400 }} > {dataSource.loading && ( diff --git a/src/pages/llmodels/index.tsx b/src/pages/llmodels/index.tsx index fc26dc88..2e6c2afe 100644 --- a/src/pages/llmodels/index.tsx +++ b/src/pages/llmodels/index.tsx @@ -8,7 +8,7 @@ import useUpdateChunkedList from '@/hooks/use-update-chunk-list'; import { useMemoizedFn } from 'ahooks'; import _ from 'lodash'; import qs from 'query-string'; -import React, { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { MODELS_API, MODEL_INSTANCE_API, @@ -18,7 +18,7 @@ import { import TableList from './components/table-list'; import { ListItem } from './config/types'; -const Models: React.FC<{ clusterId?: number }> = ({ clusterId }) => { +const Models = () => { const { pagination, setPagination } = usePaginationStatus( PaginationKey.Deployments ); @@ -49,7 +49,7 @@ const Models: React.FC<{ clusterId?: number }> = ({ clusterId }) => { page: 1, perPage: 10, search: '', - cluster_id: clusterId || 0, + cluster_id: 0, categories: [], state: '', sort_by: '', diff --git a/src/pages/resources/components/gpus.tsx b/src/pages/resources/components/gpus.tsx index e41e9734..75880ba2 100644 --- a/src/pages/resources/components/gpus.tsx +++ b/src/pages/resources/components/gpus.tsx @@ -8,15 +8,12 @@ import { useQueryClusterList } from '@/pages/cluster-management/services/use-que import { useIntl, useSearchParams } from '@umijs/max'; import { ConfigProvider, Table } from 'antd'; import _ from 'lodash'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { GPU_DEVICES_API, queryGpuDevicesList } from '../apis'; import { GPUDeviceItem } from '../config/types'; import useGPUColumns from '../hooks/use-gpu-columns'; -const GPUList: React.FC<{ clusterId?: number; widths?: { input: number } }> = ({ - clusterId, - widths -}) => { +const GPUList = () => { const { dataSource, queryParams, @@ -31,10 +28,7 @@ const GPUList: React.FC<{ clusterId?: number; widths?: { input: number } }> = ({ key: PaginationKey.GPUs, fetchAPI: queryGpuDevicesList, polling: true, - API: GPU_DEVICES_API, - defaultQueryParams: { - cluster_id: clusterId - } + API: GPU_DEVICES_API }); const [searchParams] = useSearchParams(); const page = searchParams.get('page'); @@ -108,8 +102,7 @@ const GPUList: React.FC<{ clusterId?: number; widths?: { input: number } }> = ({ handleInputChange={handleNameChange} handleSelectChange={handleClusterChange} selectOptions={clusterList} - showSelect={page !== 'clusters'} - widths={{ input: widths?.input || 200 }} + showSelect={true} > = ({ - clusterId, - showSelect = true, - showAddButton = true, - widths = { input: 200 }, - sourceType = 'resources' -}) => { +const Workers = () => { const { dataSource, rowSelection, @@ -63,10 +51,7 @@ const Workers: React.FC<{ contentForDelete: 'resources.worker', watch: true, API: WORKERS_API, - updateManually: true, - defaultQueryParams: { - cluster_id: clusterId - } + updateManually: true }); const { goToGrafana, ActionButton } = useGranfanaLink({ type: 'worker' @@ -255,8 +240,7 @@ const Workers: React.FC<{ loadend: dataSource.loadend, firstLoad: extraStatus.firstLoad, sortOrder, - handleSelect, - sourceType + handleSelect }); useEffect(() => { @@ -268,7 +252,7 @@ const Workers: React.FC<{ <> void; }): ColumnsType => { const intl = useIntl(); @@ -316,7 +314,6 @@ const useWorkerColumns = ({ { title: intl.formatMessage({ id: 'clusters.title' }), dataIndex: 'cluster_id', - hidden: sourceType === 'cluster', render: (id: number) => ( {_.get(clusterData.data, id, '')} @@ -440,7 +437,7 @@ const useWorkerColumns = ({ ) } ], - [intl, sourceType, sortOrder, clusterData, loadend, firstLoad, handleSelect] + [intl, sortOrder, clusterData, loadend, firstLoad, handleSelect] ); };