chore: scrollable tabs
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import SegmentLine from '@/components/segment-line';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import _ from 'lodash';
|
||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import useFieldScroll from './use-field-scroll';
|
||||
|
||||
const SegmentedHeader = styled.div<{ $top?: number }>`
|
||||
position: sticky;
|
||||
top: ${(props) => props.$top || 0}px;
|
||||
z-index: 10;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
background-color: var(--ant-color-bg-elevated);
|
||||
`;
|
||||
|
||||
/**
|
||||
* ScrollSpyTabs component
|
||||
* defaultTarget: The default active target tab
|
||||
* segmentedTop: { // Mostlty, It's always a constants.
|
||||
* top: number; // The top offset for the sticky header
|
||||
* offsetTop: number; // The offset top for the target
|
||||
* }
|
||||
* getScrollElementScrollableHeight: function to get the scrollable height of the scroll element
|
||||
* segmentOptions.field: The target data-field={segmentOptions.field} to scroll to
|
||||
* activeKey: The current active keys for collapsible sections
|
||||
* setActiveKey: The function to set active keys for collapsible sections
|
||||
*/
|
||||
interface ScrollSpyTabsProps {
|
||||
ref?: any;
|
||||
children?: React.ReactNode;
|
||||
defaultTarget?: string;
|
||||
segmentedTop: {
|
||||
top: number;
|
||||
offsetTop: number;
|
||||
};
|
||||
activeKey: string[];
|
||||
setActiveKey: (keys: string[]) => void;
|
||||
getScrollElementScrollableHeight?: () => {
|
||||
scrollHeight: number;
|
||||
scrollTop: number;
|
||||
};
|
||||
segmentOptions: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: React.ReactNode;
|
||||
field: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const ScrollSpyTabs: React.FC<ScrollSpyTabsProps> = forwardRef(
|
||||
(
|
||||
{
|
||||
getScrollElementScrollableHeight,
|
||||
segmentedTop,
|
||||
segmentOptions,
|
||||
defaultTarget,
|
||||
activeKey,
|
||||
setActiveKey,
|
||||
children
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [target, setTarget] = React.useState<string>(
|
||||
defaultTarget || segmentOptions[0]?.value || ''
|
||||
);
|
||||
|
||||
const { scrollToSegment, holderHeight } = useFieldScroll({
|
||||
activeKey,
|
||||
setActiveKey,
|
||||
segmentOptions,
|
||||
segmentedTop: segmentedTop,
|
||||
getScrollElementScrollableHeight: getScrollElementScrollableHeight
|
||||
});
|
||||
|
||||
const throttleScrollToSegment = useMemoizedFn(
|
||||
_.throttle(
|
||||
async (val: string) => {
|
||||
setTarget(val);
|
||||
scrollToSegment(val, { offsetTop: segmentedTop.offsetTop });
|
||||
},
|
||||
500,
|
||||
{ trailing: true }
|
||||
)
|
||||
);
|
||||
|
||||
const handleTargetChange = async (val: any) => {
|
||||
throttleScrollToSegment(val);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleTargetChange
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SegmentedHeader $top={segmentedTop.top}>
|
||||
<SegmentLine
|
||||
theme={'light'}
|
||||
defaultValue={target}
|
||||
value={target}
|
||||
onChange={handleTargetChange}
|
||||
options={segmentOptions}
|
||||
/>
|
||||
</SegmentedHeader>
|
||||
{children}
|
||||
<div className="holder" style={{ height: holderHeight }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ScrollSpyTabs;
|
||||
+4
-6
@@ -9,7 +9,6 @@ interface ScrollOptions {
|
||||
}
|
||||
|
||||
export default function useScrollAfterExpand({
|
||||
form,
|
||||
activeKey,
|
||||
setActiveKey,
|
||||
segmentOptions,
|
||||
@@ -17,9 +16,8 @@ export default function useScrollAfterExpand({
|
||||
segmentedTop = { top: 0, offsetTop: 96 },
|
||||
getScrollElementScrollableHeight
|
||||
}: {
|
||||
form: any;
|
||||
activeKey: string[];
|
||||
setActiveKey: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setActiveKey: (keys: string[]) => void;
|
||||
segmentOptions: { value: string; field: string }[];
|
||||
getScrollElementScrollableHeight?: () => {
|
||||
scrollHeight: number;
|
||||
@@ -27,8 +25,8 @@ export default function useScrollAfterExpand({
|
||||
};
|
||||
defaultWait?: number;
|
||||
segmentedTop: {
|
||||
top: number;
|
||||
offsetTop: number;
|
||||
top: number; // The top offset for the sticky header
|
||||
offsetTop: number; // The offset top for the target
|
||||
};
|
||||
}) {
|
||||
const [holderHeight, setHolderHeight] = useState<number>(0);
|
||||
@@ -67,7 +65,7 @@ export default function useScrollAfterExpand({
|
||||
const scrollToSegment = useMemoizedFn(
|
||||
async (val: string, options?: ScrollOptions) => {
|
||||
if (!activeKey.includes(val)) {
|
||||
setActiveKey((prev) => [...prev, val]);
|
||||
setActiveKey([...activeKey, val]);
|
||||
await new Promise((r) => {
|
||||
setTimeout(r, options?.wait ?? defaultWait);
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import SegmentLine from '@/components/segment-line';
|
||||
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 ScrollSpyTabs from '@/pages/_components/scroll-spy-tabs';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
|
||||
import { Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, {
|
||||
@@ -14,7 +13,6 @@ import React, {
|
||||
useImperativeHandle,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
DeployFormKeyMap,
|
||||
DO_NOT_NOTIFY_RECREATE,
|
||||
@@ -31,7 +29,6 @@ import {
|
||||
SourceType
|
||||
} from '../config/types';
|
||||
import { generateGPUIds } from '../config/utils';
|
||||
import useFieldScroll from '../hooks/use-field-scroll';
|
||||
import { useGenerateGPUOptions } from '../hooks/use-form-initial-values';
|
||||
import useQueryBackends from '../hooks/use-query-backends';
|
||||
import { useQueryContextLength } from '../services/use-query-context-length';
|
||||
@@ -48,15 +45,6 @@ const scheduleRequiredFields = ['gpu_selector'];
|
||||
|
||||
const performanceRequiredFields = ['speculative_config'];
|
||||
|
||||
const SegmentedHeader = styled.div<{ $top?: number }>`
|
||||
position: sticky;
|
||||
top: ${(props) => props.$top || 0}px;
|
||||
z-index: 10;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
background-color: var(--ant-color-bg-elevated);
|
||||
`;
|
||||
|
||||
interface DataFormProps {
|
||||
initialValues?: FormData;
|
||||
ref?: any;
|
||||
@@ -108,11 +96,11 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
const [form] = Form.useForm();
|
||||
const intl = useIntl();
|
||||
const [activeKey, setActiveKey] = React.useState<string[]>([]);
|
||||
const [target, setTarget] = React.useState<string>(TABKeysMap.BASIC);
|
||||
const { modelContextData, fetchContextLength } = useQueryContextLength();
|
||||
const localPath = Form.useWatch('local_path', form);
|
||||
const modelScopeModelId = Form.useWatch('model_scope_model_id', form);
|
||||
const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form);
|
||||
const scrollTabsRef = React.useRef<any>(null);
|
||||
|
||||
const segmentOptions = [
|
||||
{
|
||||
@@ -159,15 +147,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
};
|
||||
}, [source, formKey, action]);
|
||||
|
||||
const { scrollToSegment, holderHeight } = useFieldScroll({
|
||||
form,
|
||||
activeKey,
|
||||
setActiveKey,
|
||||
segmentOptions,
|
||||
segmentedTop: segmentedTop,
|
||||
getScrollElementScrollableHeight: getScrollElementScrollableHeight
|
||||
});
|
||||
|
||||
const handleSumit = () => {
|
||||
form.submit();
|
||||
};
|
||||
@@ -303,21 +282,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
setActiveKey(Array.isArray(keys) ? keys : [keys]);
|
||||
};
|
||||
|
||||
const throttleScrollToSegment = useMemoizedFn(
|
||||
_.throttle(
|
||||
async (val: string) => {
|
||||
setTarget(val);
|
||||
scrollToSegment(val, { offsetTop: segmentedTop.offsetTop });
|
||||
},
|
||||
500,
|
||||
{ trailing: true }
|
||||
)
|
||||
);
|
||||
|
||||
const handleTargetChange = async (val: any) => {
|
||||
throttleScrollToSegment(val);
|
||||
};
|
||||
|
||||
const handleOnFinishFailed = (errorInfo: any) => {
|
||||
const { errorFields } = errorInfo;
|
||||
if (errorFields && errorFields.length > 0) {
|
||||
@@ -352,18 +316,18 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
}
|
||||
|
||||
if (isBaseRequired) {
|
||||
handleTargetChange(TABKeysMap.BASIC);
|
||||
scrollTabsRef.current?.handleTargetChange(TABKeysMap.BASIC);
|
||||
} else if (isScheduleRequired) {
|
||||
handleTargetChange(TABKeysMap.SCHEDULING);
|
||||
scrollTabsRef.current?.handleTargetChange(TABKeysMap.SCHEDULING);
|
||||
} else if (isPerformanceRequired) {
|
||||
handleTargetChange(TABKeysMap.PERFORMANCE);
|
||||
scrollTabsRef.current?.handleTargetChange(TABKeysMap.PERFORMANCE);
|
||||
} else if (isAdvancedRequired && formKey === DeployFormKeyMap.CATALOG) {
|
||||
handleTargetChange(TABKeysMap.ADVANCED);
|
||||
scrollTabsRef.current?.handleTargetChange(TABKeysMap.ADVANCED);
|
||||
} else if (
|
||||
isAdvancedRequired &&
|
||||
formKey === DeployFormKeyMap.DEPLOYMENT
|
||||
) {
|
||||
handleTargetChange(TABKeysMap.BASIC);
|
||||
scrollTabsRef.current?.handleTargetChange(TABKeysMap.BASIC);
|
||||
}
|
||||
|
||||
setActiveKey((prev: string[]) => [
|
||||
@@ -417,6 +381,10 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
// fetchContextLength({ ...params, source });
|
||||
}, [isGGUF, source, localPath, modelScopeModelId, huggingfaceRepoId]);
|
||||
|
||||
const handleActiveChange = (key: string[]) => {
|
||||
setActiveKey(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormContext.Provider
|
||||
value={{
|
||||
@@ -434,91 +402,91 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
onBackendChange: handleBackendChange
|
||||
}}
|
||||
>
|
||||
<SegmentedHeader $top={segmentedTop.top}>
|
||||
<SegmentLine
|
||||
theme={'light'}
|
||||
defaultValue="basic"
|
||||
value={target}
|
||||
onChange={handleTargetChange}
|
||||
options={segmentOptions}
|
||||
/>
|
||||
</SegmentedHeader>
|
||||
<Form
|
||||
name="deployModel"
|
||||
form={form}
|
||||
onFinish={handleOk}
|
||||
preserve={false}
|
||||
clearOnDestroy={true}
|
||||
onValuesChange={handleOnValuesChange}
|
||||
onFinishFailed={handleOnFinishFailed}
|
||||
scrollToFirstError={true}
|
||||
initialValues={{
|
||||
replicas: 1,
|
||||
source: props.source,
|
||||
placement_strategy: 'spread',
|
||||
scheduleType: ScheduleValueMap.Auto,
|
||||
categories: null,
|
||||
restart_on_error: true,
|
||||
distributed_inference_across_workers: true,
|
||||
mode: 'throughput',
|
||||
generic_proxy: false,
|
||||
extended_kv_cache: {
|
||||
enabled: false,
|
||||
chunk_size: null,
|
||||
ram_ratio: 1.2,
|
||||
ram_size: null
|
||||
},
|
||||
speculative_config: {
|
||||
enabled: false,
|
||||
algorithm: '',
|
||||
draft_model: null,
|
||||
num_draft_tokens:
|
||||
initialValues?.speculative_config?.num_draft_tokens || 4,
|
||||
ngram_min_match_length:
|
||||
initialValues?.speculative_config?.ngram_min_match_length || 1,
|
||||
ngram_max_match_length:
|
||||
initialValues?.speculative_config?.ngram_max_match_length || 10
|
||||
},
|
||||
...initialValues,
|
||||
backend_version: initialValues?.backend_version || null,
|
||||
max_context_len: initialValues?.max_context_len || 2048
|
||||
}}
|
||||
<ScrollSpyTabs
|
||||
ref={scrollTabsRef}
|
||||
defaultTarget="basic"
|
||||
segmentOptions={segmentOptions}
|
||||
activeKey={activeKey}
|
||||
setActiveKey={handleActiveChange}
|
||||
segmentedTop={segmentedTop}
|
||||
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
|
||||
>
|
||||
<BasicForm
|
||||
fields={fields}
|
||||
sourceList={sourceList}
|
||||
clusterList={clusterList}
|
||||
sourceDisable={sourceDisable}
|
||||
handleClusterChange={handleClusterChange}
|
||||
onSourceChange={onSourceChange}
|
||||
></BasicForm>
|
||||
<CollapsePanel
|
||||
activeKey={activeKey}
|
||||
accordion={false}
|
||||
onChange={handleOnCollapseChange}
|
||||
items={[
|
||||
{
|
||||
key: TABKeysMap.PERFORMANCE,
|
||||
label: intl.formatMessage({ id: 'models.form.performance' }),
|
||||
forceRender: true,
|
||||
children: <Performance></Performance>
|
||||
<Form
|
||||
name="deployModel"
|
||||
form={form}
|
||||
onFinish={handleOk}
|
||||
preserve={false}
|
||||
clearOnDestroy={true}
|
||||
onValuesChange={handleOnValuesChange}
|
||||
onFinishFailed={handleOnFinishFailed}
|
||||
scrollToFirstError={true}
|
||||
initialValues={{
|
||||
replicas: 1,
|
||||
source: props.source,
|
||||
placement_strategy: 'spread',
|
||||
scheduleType: ScheduleValueMap.Auto,
|
||||
categories: null,
|
||||
restart_on_error: true,
|
||||
distributed_inference_across_workers: true,
|
||||
mode: 'throughput',
|
||||
generic_proxy: false,
|
||||
extended_kv_cache: {
|
||||
enabled: false,
|
||||
chunk_size: null,
|
||||
ram_ratio: 1.2,
|
||||
ram_size: null
|
||||
},
|
||||
{
|
||||
key: TABKeysMap.SCHEDULING,
|
||||
label: intl.formatMessage({ id: 'models.form.scheduling' }),
|
||||
forceRender: true,
|
||||
children: <ScheduleTypeForm></ScheduleTypeForm>
|
||||
speculative_config: {
|
||||
enabled: false,
|
||||
algorithm: '',
|
||||
draft_model: null,
|
||||
num_draft_tokens:
|
||||
initialValues?.speculative_config?.num_draft_tokens || 4,
|
||||
ngram_min_match_length:
|
||||
initialValues?.speculative_config?.ngram_min_match_length || 1,
|
||||
ngram_max_match_length:
|
||||
initialValues?.speculative_config?.ngram_max_match_length || 10
|
||||
},
|
||||
{
|
||||
key: TABKeysMap.ADVANCED,
|
||||
label: intl.formatMessage({ id: 'resources.form.advanced' }),
|
||||
forceRender: true,
|
||||
children: <AdvanceConfig></AdvanceConfig>
|
||||
}
|
||||
]}
|
||||
></CollapsePanel>
|
||||
<div className="holder" style={{ height: holderHeight }}></div>
|
||||
</Form>
|
||||
...initialValues,
|
||||
backend_version: initialValues?.backend_version || null,
|
||||
max_context_len: initialValues?.max_context_len || 2048
|
||||
}}
|
||||
>
|
||||
<BasicForm
|
||||
fields={fields}
|
||||
sourceList={sourceList}
|
||||
clusterList={clusterList}
|
||||
sourceDisable={sourceDisable}
|
||||
handleClusterChange={handleClusterChange}
|
||||
onSourceChange={onSourceChange}
|
||||
></BasicForm>
|
||||
<CollapsePanel
|
||||
activeKey={activeKey}
|
||||
accordion={false}
|
||||
onChange={handleOnCollapseChange}
|
||||
items={[
|
||||
{
|
||||
key: TABKeysMap.PERFORMANCE,
|
||||
label: intl.formatMessage({ id: 'models.form.performance' }),
|
||||
forceRender: true,
|
||||
children: <Performance></Performance>
|
||||
},
|
||||
{
|
||||
key: TABKeysMap.SCHEDULING,
|
||||
label: intl.formatMessage({ id: 'models.form.scheduling' }),
|
||||
forceRender: true,
|
||||
children: <ScheduleTypeForm></ScheduleTypeForm>
|
||||
},
|
||||
{
|
||||
key: TABKeysMap.ADVANCED,
|
||||
label: intl.formatMessage({ id: 'resources.form.advanced' }),
|
||||
forceRender: true,
|
||||
children: <AdvanceConfig></AdvanceConfig>
|
||||
}
|
||||
]}
|
||||
></CollapsePanel>
|
||||
</Form>
|
||||
</ScrollSpyTabs>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user