From 9eb1dba4a1b7b059aad238623afede897094de86 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 10 Nov 2025 20:03:06 +0800 Subject: [PATCH] fix: calc scrollheight dynamically for tabs change --- src/hooks/use-overlay-scroller.ts | 40 ++++++++--- src/locales/en-US/models.ts | 6 +- src/locales/ja-JP/models.ts | 10 ++- src/locales/ru-RU/models.ts | 9 ++- src/locales/zh-CN/models.ts | 6 +- .../_components/column-wrapper/index.tsx | 29 +++++--- .../column-wrapper/use-wrapper-context.ts | 6 ++ src/pages/llmodels/forms/backend.tsx | 5 +- src/pages/llmodels/forms/catalog.tsx | 19 ++--- src/pages/llmodels/forms/index.tsx | 44 +++++++----- src/pages/llmodels/forms/schedule-type.tsx | 47 +++++++++++-- src/pages/llmodels/hooks/use-field-scroll.ts | 65 ++++++++++++++--- .../llmodels/hooks/use-query-backends.ts | 4 +- .../resources/components/script-install.tsx | 70 +++++++++++++++++++ 14 files changed, 283 insertions(+), 77 deletions(-) create mode 100644 src/pages/resources/components/script-install.tsx diff --git a/src/hooks/use-overlay-scroller.ts b/src/hooks/use-overlay-scroller.ts index afc28b2a..736560ad 100644 --- a/src/hooks/use-overlay-scroller.ts +++ b/src/hooks/use-overlay-scroller.ts @@ -217,12 +217,6 @@ export default function useOverlayScroller(data?: { // const currentScroll = instanceRef.current?.scroll().position.y; const targetPos = targetRect.top - containerRect.top + currentScroll; - console.log( - 'target=======', - currentScroll, - targetPos, - instanceRef.current?.options() - ); scrollEventElement.current.scroll({ y: targetPos - offset, @@ -231,6 +225,35 @@ export default function useOverlayScroller(data?: { instanceRef.current?.update?.(); }; + const getScrollElementScrollableHeight = () => { + if (!instanceRef.current || !scrollEventElement.current) { + instanceRef.current = instance?.(); + scrollEventElement.current = + instanceRef.current?.elements()?.scrollEventElement; + } + + const scrollerState = instanceRef.current?.state(); + const overflowAmount = scrollerState?.overflowAmount; + const scrollOffsetElement = instanceRef.current?.elements().viewport; + console.log('osInstance==========', overflowAmount, scrollOffsetElement, { + top: + Math.round( + (overflowAmount?.y - scrollOffsetElement?.scrollTop || 0) / + overflowAmount?.y + ) * overflowAmount?.y, + scrollTop: scrollOffsetElement?.scrollTop, + scrollLeft: scrollOffsetElement?.scrollLeft, + scrollHeight: + scrollOffsetElement?.scrollHeight - scrollOffsetElement?.clientHeight + }); + + return { + scrollTop: scrollOffsetElement?.scrollTop, + scrollHeight: + scrollOffsetElement?.scrollHeight - scrollOffsetElement?.clientHeight + }; + }; + useEffect(() => { return () => { instanceRef.current?.destroy?.(); @@ -240,9 +263,10 @@ export default function useOverlayScroller(data?: { return { initialize: createInstance, - instance: instanceRef.current, - scrollEventElement: scrollEventElement.current, + instance: instanceRef, + scrollEventElement: scrollEventElement, initialized: initialized.current, + getScrollElementScrollableHeight, generateInstance, destroyInstance: destroyInstance, updateScrollerPosition: throttledUpdateScrollerPosition, diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 7ead49de..5f55bb24 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -45,7 +45,8 @@ export default { 'models.form.scheduletype': 'Schedule Type', 'models.form.categories': 'Model Category', 'models.form.scheduletype.auto': 'Auto', - 'models.form.scheduletype.manual': 'Specify GPU', + 'models.form.scheduletype.manual': 'Manual', + 'models.form.scheduletype.gpu': 'Specify GPU', 'models.form.scheduletype.gpuType': 'Specify GPU Type', 'models.form.scheduletype.auto.tips': 'Automatically deploys model instances to appropriate GPUs based on current resource conditions.', @@ -242,5 +243,6 @@ export default { 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', 'models.catalog.button.explore': 'Explore Other Models', - 'models.catalog.precision': 'Precision' + 'models.catalog.precision': 'Precision', + 'models.form.gpuPerReplica.tips': 'Enter a custom number' }; diff --git a/src/locales/ja-JP/models.ts b/src/locales/ja-JP/models.ts index 46a370fc..b570d6ee 100644 --- a/src/locales/ja-JP/models.ts +++ b/src/locales/ja-JP/models.ts @@ -47,7 +47,8 @@ export default { 'models.form.scheduletype': 'スケジュールタイプ', 'models.form.categories': 'モデルカテゴリ', 'models.form.scheduletype.auto': '自動', - 'models.form.scheduletype.manual': 'GPUを指定', + 'models.form.scheduletype.manual': '手動', + 'models.form.scheduletype.gpu': 'GPUを指定', 'models.form.scheduletype.gpuType': 'GPUタイプを指定', 'models.form.scheduletype.auto.tips': '現在のリソース状況に基づいて、モデルインスタンスを適切なGPUに自動的にデプロイします。', @@ -242,7 +243,8 @@ export default { 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', 'models.catalog.button.explore': 'Explore Other Models', - 'models.catalog.precision': 'Precision' + 'models.catalog.precision': 'Precision', + 'models.form.gpuPerReplica.tips': 'Enter a custom number' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -313,5 +315,7 @@ export default { // 48. 'models.form.backend.custom': 'User Defined', // 49. 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', // 50. 'models.catalog.button.explore': 'Explore Other Models', -// 51. 'models.catalog.precision': 'Precision' +// 51. 'models.catalog.precision': 'Precision', +// 52. 'models.form.gpuPerReplica.tips': 'Enter a custom number' + // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/models.ts b/src/locales/ru-RU/models.ts index 78173f24..bb920470 100644 --- a/src/locales/ru-RU/models.ts +++ b/src/locales/ru-RU/models.ts @@ -45,7 +45,8 @@ export default { 'models.form.scheduletype': 'Тип планирования', 'models.form.categories': 'Категория модели', 'models.form.scheduletype.auto': 'Авто', - 'models.form.scheduletype.manual': 'Указать GPU', + 'models.form.scheduletype.manual': 'Ручной', + 'models.form.scheduletype.gpu': 'Указать GPU', 'models.form.scheduletype.gpuType': 'Указать тип GPU', 'models.form.scheduletype.auto.tips': 'Автоматическое развертывание инстансов модели на подходящие GPU в зависимости от текущих ресурсов.', @@ -243,9 +244,11 @@ export default { 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', 'models.catalog.button.explore': 'Изучить другие модели', - 'models.catalog.precision': 'Точность' + 'models.catalog.precision': 'Точность', + 'models.form.gpuPerReplica.tips': 'Enter a custom number' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== -// 1. 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.' +// 1. 'models.form.rules.name': 'Up to 63 characters; letters, numbers, dots (.), underscores (_), and hyphens (-) only; must start and end with an alphanumeric character.', +// 2. 'models.form.gpuPerReplica.tips': 'Enter a custom number' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index 896572cb..8e5c04ed 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -46,7 +46,8 @@ export default { 'models.form.categories': '模型类别', 'models.form.scheduletype': '调度方式', 'models.form.scheduletype.auto': '自动', - 'models.form.scheduletype.manual': '指定 GPU', + 'models.form.scheduletype.manual': '手动', + 'models.form.scheduletype.gpu': '指定 GPU', 'models.form.scheduletype.gpuType': '指定 GPU 类型', 'models.form.scheduletype.auto.tips': '自动根据当前资源情况部署模型实例到合适的 GPU 上。', @@ -229,5 +230,6 @@ export default { 'models.form.rules.name': '长度不超过 63 个字符,只能包含字母、数字、点(.)、下划线(_)和连字符(-),且必须以字母或数字开头和结尾。', 'models.catalog.button.explore': '浏览其他模型', - 'models.catalog.precision': '精度' + 'models.catalog.precision': '精度', + 'models.form.gpuPerReplica.tips': '输入自定义数值' }; diff --git a/src/pages/_components/column-wrapper/index.tsx b/src/pages/_components/column-wrapper/index.tsx index adc8fe77..93642362 100644 --- a/src/pages/_components/column-wrapper/index.tsx +++ b/src/pages/_components/column-wrapper/index.tsx @@ -22,14 +22,20 @@ const ColumnWrapper: React.FC = ({ styles = {} }) => { const scroller = React.useRef(null); - const { initialize, instance, scrollToBottom, scrollToTarget } = - useOverlayScroller({ - options: { - scrollbars: { - autoHide: 'move' - } + const { + initialize, + instance, + scrollEventElement, + scrollToBottom, + scrollToTarget, + getScrollElementScrollableHeight + } = useOverlayScroller({ + options: { + scrollbars: { + autoHide: 'move' } - }); + } + }); React.useEffect(() => { if (scroller.current) { @@ -39,7 +45,14 @@ const ColumnWrapper: React.FC = ({ return (
void; scrollToTop?: () => void; + getScrollElementScrollableHeight?: () => { + scrollHeight: number; + scrollTop: number; + }; scrollToTarget?: (target: any, offset?: number) => void; } diff --git a/src/pages/llmodels/forms/backend.tsx b/src/pages/llmodels/forms/backend.tsx index d3c9d882..24c1cbbe 100644 --- a/src/pages/llmodels/forms/backend.tsx +++ b/src/pages/llmodels/forms/backend.tsx @@ -29,7 +29,7 @@ const BackendFields: React.FC = () => { const { onValuesChange, backendOptions, onBackendChange } = useFormContext(); const backend = Form.useWatch('backend', form); - const handleBackendVersionOnBlur = () => { + const handleBackendVersionOnChange = () => { onValuesChange?.({}, form.getFieldsValue()); }; @@ -137,13 +137,14 @@ const BackendFields: React.FC = () => { {backendOptionsMap.custom !== backend && ( { const intl = useIntl(); const formCtx = useFormContext(); - const catalogFormCtx = useCatalogFormContext(); const { getRuleMessage } = useAppUtils(); const { formKey } = formCtx; - const { - sizeOptions, - quantizationOptions, - onSizeChange, - onQuantizationChange - } = catalogFormCtx; if (formKey !== DeployFormKeyMap.CATALOG) { return null; } - const handleSizeChange = (val: any) => { - onSizeChange?.(val); - }; - - const handleOnQuantizationChange = (val: any) => { - onQuantizationChange?.(val); - }; return ( <> diff --git a/src/pages/llmodels/forms/index.tsx b/src/pages/llmodels/forms/index.tsx index 1d7bc181..7301e73a 100644 --- a/src/pages/llmodels/forms/index.tsx +++ b/src/pages/llmodels/forms/index.tsx @@ -1,6 +1,7 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import CollapsePanel from '@/pages/_components/collapse-panel'; +import { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context'; import { useIntl } from '@umijs/max'; import { Form, Segmented } from 'antd'; import _ from 'lodash'; @@ -72,6 +73,13 @@ interface DataFormProps { onBackendChange?: (value: string) => void; } +const TABKeysMap = { + BASIC: 'basic', + SCHEDULING: 'scheduling', + PERFORMANCE: 'performance', + ADVANCED: 'advanced' +}; + const DataForm: React.FC = forwardRef((props, ref) => { const { action, @@ -89,6 +97,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { onValuesChange, onOk } = props; + const { getScrollElementScrollableHeight } = useWrapperContext(); const { backendOptions, getBackendOptions } = useQueryBackends(); const { getGPUOptionList, gpuOptions, workerLabelOptions } = useGenerateGPUOptions(); @@ -96,14 +105,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { const intl = useIntl(); const [activeKey, setActiveKey] = React.useState([]); const scheduleType = Form.useWatch('scheduleType', form); - const [target, setTarget] = React.useState('basic'); - - const TABKeysMap = { - BASIC: 'basic', - SCHEDULING: 'scheduling', - PERFORMANCE: 'performance', - ADVANCED: 'advanced' - }; + const [target, setTarget] = React.useState(TABKeysMap.BASIC); const segmentOptions = [ { @@ -128,14 +130,12 @@ const DataForm: React.FC = forwardRef((props, ref) => { } ]; - const { scrollToSegment } = useFieldScroll({ - form, - activeKey, - setActiveKey, - segmentOptions - }); + console.log( + 'getScrollElementScrollableHeight', + getScrollElementScrollableHeight?.() + ); - const SegmentedTop = useMemo(() => { + const segmentedTop = useMemo(() => { if ( modelSourceMap.local_path_value === source || action === PageAction.EDIT || @@ -153,6 +153,15 @@ const DataForm: React.FC = forwardRef((props, ref) => { }; }, [source, formKey, action]); + const { scrollToSegment, holderHeight } = useFieldScroll({ + form, + activeKey, + setActiveKey, + segmentOptions, + segmentedTop: segmentedTop, + getScrollElementScrollableHeight: getScrollElementScrollableHeight + }); + const handleSumit = () => { form.submit(); }; @@ -288,7 +297,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { const handleTargetChange = async (val: any) => { setTarget(val); - await scrollToSegment(val, { offsetTop: SegmentedTop.offsetTop }); + await scrollToSegment(val, { offsetTop: segmentedTop.offsetTop }); }; const handleOnFinishFailed = (errorInfo: any) => { @@ -389,7 +398,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { onBackendChange: handleBackendChange }} > - + = forwardRef((props, ref) => { } ]} > +
); diff --git a/src/pages/llmodels/forms/schedule-type.tsx b/src/pages/llmodels/forms/schedule-type.tsx index abb71832..9892f879 100644 --- a/src/pages/llmodels/forms/schedule-type.tsx +++ b/src/pages/llmodels/forms/schedule-type.tsx @@ -5,9 +5,10 @@ import SealSelect from '@/components/seal-form/seal-select'; import TooltipList from '@/components/tooltip-list'; import useAppUtils from '@/hooks/use-app-utils'; import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; +import { Form, InputNumber } from 'antd'; import _ from 'lodash'; import React from 'react'; +import styled from 'styled-components'; import GPUCard from '../components/gpu-card'; import { placementStrategyOptions, @@ -18,6 +19,10 @@ import { backendOptionsMap } from '../config/backend-parameters'; import { useFormContext } from '../config/form-context'; import { FormData } from '../config/types'; +const InputWrapper = styled.div` + padding: 8px 4px; +`; + const placementStrategyTips = [ { title: 'Spread', @@ -70,6 +75,10 @@ const ScheduleTypeForm: React.FC = () => { const form = Form.useFormInstance(); const scheduleType = Form.useWatch('scheduleType', form); const workerSelector = Form.useWatch('worker_selector', form); + const GPUsPerReplicas = Form.useWatch( + ['gpu_selector', 'gpus_per_replica'], + form + ); const handleScheduleTypeChange = async (value: string) => { if (value === ScheduleValueMap.Auto) { @@ -81,6 +90,15 @@ const ScheduleTypeForm: React.FC = () => { form.setFieldValue(['gpu_selector', 'gpus_per_replica'], null); } }; + const handleGpusPerReplicasChange = (val: string | number | null) => { + if (val === null) { + form.setFieldValue(['gpu_selector', 'gpus_per_replica'], null); + } else { + form.setFieldValue(['gpu_selector', 'gpus_per_replica'], val); + } + + onValuesChange?.({}, form.getFieldsValue()); + }; const handleGpuSelectorChange = (value: any[]) => { if (value.length > 0) { @@ -191,15 +209,32 @@ const ScheduleTypeForm: React.FC = () => { { label: '2', value: 2 }, { label: '4', value: 4 }, { label: '8', value: 8 }, - { label: '16', value: 16 }, - { label: '32', value: 32 }, - { label: '64', value: 64 }, - { label: '128', value: 128 }, - { label: '256', value: 256 } + { label: '16', value: 16 } ]} description={ } + popupRender={(originNode) => ( +
+ {originNode} + + + +
+ )} />
diff --git a/src/pages/llmodels/hooks/use-field-scroll.ts b/src/pages/llmodels/hooks/use-field-scroll.ts index 85bcce42..f28ca18c 100644 --- a/src/pages/llmodels/hooks/use-field-scroll.ts +++ b/src/pages/llmodels/hooks/use-field-scroll.ts @@ -1,4 +1,5 @@ -import { useCallback } from 'react'; +import { useMemoizedFn } from 'ahooks'; +import { useCallback, useRef, useState } from 'react'; interface ScrollOptions { wait?: number; @@ -12,14 +13,27 @@ export default function useScrollAfterExpand({ activeKey, setActiveKey, segmentOptions, - defaultWait = 300 + defaultWait = 300, + segmentedTop = { top: 0, offsetTop: 96 }, + getScrollElementScrollableHeight }: { form: any; activeKey: string[]; setActiveKey: React.Dispatch>; segmentOptions: { value: string; field: string }[]; + getScrollElementScrollableHeight?: () => { + scrollHeight: number; + scrollTop: number; + }; defaultWait?: number; + segmentedTop: { + top: number; + offsetTop: number; + }; }) { + const [holderHeight, setHolderHeight] = useState(0); + const boxHeightRef = useRef(0); + const scrollToElement = useCallback( ( el: HTMLElement, @@ -46,7 +60,11 @@ export default function useScrollAfterExpand({ [] ); - const scrollToSegment = useCallback( + /** + * due to the scrollheight changes after expanding the segment and including the holder height. + * + */ + const scrollToSegment = useMemoizedFn( async (val: string, options?: ScrollOptions) => { if (!activeKey.includes(val)) { setActiveKey((prev) => [...prev, val]); @@ -55,19 +73,48 @@ export default function useScrollAfterExpand({ }); } - await new Promise(requestAnimationFrame); - const current = segmentOptions.find((item) => item.value === val); if (!current?.field) return; - const el = document.querySelector( + await new Promise(requestAnimationFrame); + + const el: HTMLElement | null = document.querySelector( `[data-field="${current.field}"]` ) as HTMLElement | null; + const targetRectTop = el?.getBoundingClientRect().top || 0; + + const scroller = getScrollElementScrollableHeight?.() || { + scrollHeight: 0, + scrollTop: 0 + }; + + // remaining scroll height + const remainingScrollHeight = scroller.scrollHeight - scroller.scrollTop; + + // total distance from the top of the scroller to the target element + const offsetDistance = + targetRectTop - segmentedTop.offsetTop - segmentedTop.top; + + let boxHeight = 0; + + // verify boxHeight is correct, if setting the boxHeight causes the element to be hidden, use the previous boxHeight + if (offsetDistance <= 0) { + boxHeight = boxHeightRef.current; + } else { + boxHeight = + offsetDistance - remainingScrollHeight + boxHeightRef.current; + } + + // update boxHeightRef + boxHeightRef.current = boxHeight; + + setHolderHeight(boxHeight); + await new Promise(requestAnimationFrame); + if (el) scrollToElement(el, options); - }, - [activeKey, setActiveKey, segmentOptions, defaultWait, scrollToElement] + } ); - return { scrollToSegment }; + return { scrollToSegment, holderHeight }; } diff --git a/src/pages/llmodels/hooks/use-query-backends.ts b/src/pages/llmodels/hooks/use-query-backends.ts index 52d5629e..3673dc00 100644 --- a/src/pages/llmodels/hooks/use-query-backends.ts +++ b/src/pages/llmodels/hooks/use-query-backends.ts @@ -17,11 +17,11 @@ export default function useQueryBackends() { value: item.backend_name, label: item.backend_name === backendOptionsMap.custom - ? intl.formatMessage({ id: 'backend.quickConfig' }) + ? intl.formatMessage({ id: 'backend.custom' }) : item.backend_name, title: item.backend_name === backendOptionsMap.custom - ? intl.formatMessage({ id: 'backend.quickConfig' }) + ? intl.formatMessage({ id: 'backend.custom' }) : item.backend_name.replace(/-custom$/, ''), default_backend_param: item.default_backend_param || [], default_version: item.default_version, diff --git a/src/pages/resources/components/script-install.tsx b/src/pages/resources/components/script-install.tsx new file mode 100644 index 00000000..28a4cdcb --- /dev/null +++ b/src/pages/resources/components/script-install.tsx @@ -0,0 +1,70 @@ +import HighlightCode from '@/components/highlight-code'; +import { useIntl } from '@umijs/max'; +import React, { useMemo } from 'react'; +import { addWorkerGuide } from '../config'; + +type ViewModalProps = { token: string }; + +const AddWorker: React.FC = (props) => { + const intl = useIntl(); + + const origin = window.location.origin; + + const labels = useMemo( + () => ({ + step1: intl.formatMessage({ id: 'resources.worker.add.step1' }), + step2: intl.formatMessage({ id: 'resources.worker.add.step2' }), + step2Tips: intl.formatMessage({ id: 'resources.worker.add.step2.tips' }), + step3: intl.formatMessage({ id: 'resources.worker.add.step3' }), + linuxOrMac: intl.formatMessage({ id: 'resources.worker.linuxormaxos' }) + }), + [intl] + ); + + return ( +
+

+ 1. +

+

{labels.linuxOrMac}

+ +

Windows

+ +

+ 2. {labels.step2}{' '} + +

+

{labels.linuxOrMac}

+ +

Windows

+ +

3. {labels.step3}

+
+ ); +}; + +export default AddWorker;