chore: adjust deployment modal form
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import LabelSelector from '@/components/label-selector';
|
||||
import ListInput from '@/components/list-input';
|
||||
import CheckboxField from '@/components/seal-form/checkbox-field';
|
||||
import SealCascader from '@/components/seal-form/seal-cascader';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
@@ -8,17 +9,8 @@ import TooltipList from '@/components/tooltip-list';
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import {
|
||||
Checkbox,
|
||||
Collapse,
|
||||
Form,
|
||||
FormInstance,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { Collapse, Form, FormInstance, Typography } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
@@ -32,7 +24,6 @@ import {
|
||||
placementStrategyOptions
|
||||
} from '../config';
|
||||
import { useFormContext } from '../config/form-context';
|
||||
import llamaConfig from '../config/llama-config';
|
||||
import mindieConfig from '../config/mindie-config';
|
||||
import { FormData } from '../config/types';
|
||||
import vllmConfig from '../config/vllm-config';
|
||||
@@ -62,42 +53,6 @@ const placementStrategyTips = [
|
||||
}
|
||||
];
|
||||
|
||||
const scheduleTypeTips = [
|
||||
{
|
||||
title: {
|
||||
text: 'models.form.scheduletype.auto',
|
||||
locale: true
|
||||
},
|
||||
tips: 'models.form.scheduletype.auto.tips'
|
||||
},
|
||||
{
|
||||
title: {
|
||||
text: 'models.form.scheduletype.manual',
|
||||
locale: true
|
||||
},
|
||||
tips: 'models.form.scheduletype.manual.tips'
|
||||
}
|
||||
];
|
||||
|
||||
const CheckboxField: React.FC<{
|
||||
title: string;
|
||||
label: string;
|
||||
checked?: boolean;
|
||||
onChange?: (e: CheckboxChangeEvent) => void;
|
||||
}> = ({ title, label, checked, onChange }) => {
|
||||
return (
|
||||
<Checkbox className="p-l-6" checked={checked} onChange={onChange}>
|
||||
<Tooltip title={title}>
|
||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>{label}</span>
|
||||
<QuestionCircleOutlined
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
const { form, isGGUF, gpuOptions, source, backendOptions, action } = props;
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
@@ -115,9 +70,6 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
const { onValuesChange } = useFormContext();
|
||||
|
||||
const paramsConfig = useMemo(() => {
|
||||
if (backend === backendOptionsMap.llamaBox) {
|
||||
return llamaConfig;
|
||||
}
|
||||
if (backend === backendOptionsMap.vllm) {
|
||||
return vllmConfig;
|
||||
}
|
||||
@@ -220,14 +172,6 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
description={<TooltipList list={backendTipsList}></TooltipList>}
|
||||
options={
|
||||
backendOptions ?? [
|
||||
{
|
||||
label: backendLabelMap[backendOptionsMap.llamaBox],
|
||||
value: backendOptionsMap.llamaBox,
|
||||
disabled:
|
||||
props.source === modelSourceMap.local_path_value
|
||||
? false
|
||||
: !isGGUF
|
||||
},
|
||||
{
|
||||
label: backendLabelMap[backendOptionsMap.vllm],
|
||||
value: backendOptionsMap.vllm,
|
||||
@@ -261,27 +205,6 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
{/* <Form.Item name="scheduleType">
|
||||
<SealSelect
|
||||
onChange={handleScheduleTypeChange}
|
||||
label={intl.formatMessage({ id: 'models.form.scheduletype' })}
|
||||
description={<TooltipList list={scheduleTypeTips}></TooltipList>}
|
||||
options={[
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'models.form.scheduletype.auto'
|
||||
}),
|
||||
value: 'auto'
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'models.form.scheduletype.manual'
|
||||
}),
|
||||
value: 'manual'
|
||||
}
|
||||
]}
|
||||
></SealSelect>
|
||||
</Form.Item> */}
|
||||
{scheduleType === 'auto' && (
|
||||
<>
|
||||
<Form.Item<FormData> name="placement_strategy">
|
||||
@@ -482,31 +405,10 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
></LabelSelector>
|
||||
</Form.Item>
|
||||
|
||||
{backend === backendOptionsMap.llamaBox && (
|
||||
<div style={{ paddingBottom: 22, paddingLeft: 10 }}>
|
||||
<Form.Item<FormData>
|
||||
name="cpu_offloading"
|
||||
valuePropName="checked"
|
||||
style={{ padding: '0 10px', marginBottom: 0 }}
|
||||
noStyle
|
||||
>
|
||||
<CheckboxField
|
||||
title={intl.formatMessage({
|
||||
id: 'models.form.partialoffload.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'resources.form.enablePartialOffload'
|
||||
})}
|
||||
></CheckboxField>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
{scheduleType === 'auto' &&
|
||||
[
|
||||
backendOptionsMap.llamaBox,
|
||||
backendOptionsMap.vllm,
|
||||
backendOptionsMap.ascendMindie
|
||||
].includes(backend) && (
|
||||
[backendOptionsMap.vllm, backendOptionsMap.ascendMindie].includes(
|
||||
backend
|
||||
) && (
|
||||
<div style={{ paddingBottom: 22, paddingLeft: 10 }}>
|
||||
<Form.Item<FormData>
|
||||
name="distributed_inference_across_workers"
|
||||
@@ -515,7 +417,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
noStyle
|
||||
>
|
||||
<CheckboxField
|
||||
title={intl.formatMessage({
|
||||
description={intl.formatMessage({
|
||||
id: 'models.form.distribution.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
@@ -533,7 +435,7 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
noStyle
|
||||
>
|
||||
<CheckboxField
|
||||
title={intl.formatMessage({
|
||||
description={intl.formatMessage({
|
||||
id: 'models.form.restart.onerror.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CatalogSkelton from '@/components/templates/card-skelton';
|
||||
import breakpoints from '@/config/breakpoints';
|
||||
import { Col, FloatButton, Row, Spin } from 'antd';
|
||||
import _ from 'lodash';
|
||||
@@ -6,7 +7,6 @@ import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { CatalogItem as CatalogItemType } from '../config/types';
|
||||
import CatalogItem from './catalog-item';
|
||||
import CatalogSkelton from './catalog-skelton';
|
||||
|
||||
const SpinWrapper = styled.div`
|
||||
width: 100%;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Col, Row, Skeleton } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface CatalogSkeltonProps {
|
||||
span: number;
|
||||
}
|
||||
|
||||
const CatalogSkelton: React.FC<CatalogSkeltonProps> = (props) => {
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
{Array(6)
|
||||
.fill(1)
|
||||
.map((_, index) => {
|
||||
return (
|
||||
<Col span={props.span} key={index}>
|
||||
<Skeleton
|
||||
avatar={{
|
||||
size: 32
|
||||
}}
|
||||
paragraph={{ rows: 2 }}
|
||||
style={{
|
||||
border: '1px solid var(--ant-color-border)',
|
||||
borderRadius: 'var(--border-radius-base)',
|
||||
padding: '16px 16px'
|
||||
}}
|
||||
></Skeleton>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CatalogSkelton);
|
||||
@@ -258,14 +258,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
description={<TooltipList list={backendTipsList}></TooltipList>}
|
||||
options={
|
||||
backendOptions ?? [
|
||||
{
|
||||
label: backendLabelMap[backendOptionsMap.llamaBox],
|
||||
value: backendOptionsMap.llamaBox,
|
||||
disabled:
|
||||
props.source === modelSourceMap.local_path_value
|
||||
? false
|
||||
: !isGGUF
|
||||
},
|
||||
{
|
||||
label: backendLabelMap[backendOptionsMap.vllm],
|
||||
value: backendOptionsMap.vllm,
|
||||
@@ -300,28 +292,6 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
></SealSelect>
|
||||
</Form.Item> */}
|
||||
<CatalogFrom></CatalogFrom>
|
||||
{/* <Form.Item<FormData>
|
||||
name="replicas"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'models.form.replicas')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Number
|
||||
style={{ width: '100%' }}
|
||||
label={intl.formatMessage({
|
||||
id: 'models.form.replicas'
|
||||
})}
|
||||
required
|
||||
description={intl.formatMessage(
|
||||
{ id: 'models.form.replicas.tips' },
|
||||
{ api: `${window.location.origin}/v1` }
|
||||
)}
|
||||
min={0}
|
||||
></SealInput.Number>
|
||||
</Form.Item> */}
|
||||
<Form.Item<FormData> name="description">
|
||||
<SealInput.TextArea
|
||||
scaleSize={true}
|
||||
@@ -343,4 +313,4 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default React.memo(DataForm);
|
||||
export default DataForm;
|
||||
|
||||
@@ -568,6 +568,7 @@ const AddModal: FC<AddModalProps> = (props) => {
|
||||
isGGUF: isGGUF,
|
||||
pageAction: action,
|
||||
modelFileOptions: props.modelFileOptions,
|
||||
gpuOptions: props.gpuOptions,
|
||||
onValuesChange: onValuesChange
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,18 +1,145 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import StatusTag from '@/components/status-tag';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import Card from '@/components/templates/card';
|
||||
import { Button } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
InstanceStatusMap,
|
||||
InstanceStatusMapValue,
|
||||
modelCategories,
|
||||
modelSourceMap,
|
||||
status
|
||||
} from '../config';
|
||||
|
||||
const ModelItem: React.FC<{ model: any; onDeploy: (model: any) => void }> = (
|
||||
props
|
||||
) => {
|
||||
const { model, onDeploy } = props;
|
||||
const ModelItemContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 8px;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
}
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-middle);
|
||||
font-weight: 500;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
.footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
.btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.btn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.time {
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 400;
|
||||
}
|
||||
.extra-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
.tag-item {
|
||||
margin-right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
height: 22px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const sourceIconMap = {
|
||||
[modelSourceMap.local_path_value]: 'icon-hard-disk',
|
||||
[modelSourceMap.huggingface_value]: 'icon-huggingface',
|
||||
[modelSourceMap.modelscope_value]: 'icon-modelscope'
|
||||
};
|
||||
|
||||
const ModelItem: React.FC<{
|
||||
model: Record<string, any>;
|
||||
onClick: (model: any) => void;
|
||||
}> = (props) => {
|
||||
const { model, onClick } = props;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="model-item" onClick={() => onDeploy(model)}>
|
||||
<h3>{model.name}</h3>
|
||||
<p>{model.description}</p>
|
||||
<span>Deploy</span>
|
||||
</div>
|
||||
<Card onClick={() => onClick(model)} clickable={false} ghost>
|
||||
<ModelItemContent>
|
||||
<div className="title">
|
||||
<span className="text">
|
||||
<IconFont
|
||||
type={sourceIconMap[model.source]}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<span>{model.name}</span>
|
||||
</span>
|
||||
<StatusTag
|
||||
maxTooltipWidth={400}
|
||||
statusValue={{
|
||||
status: status[InstanceStatusMap.Running] as any,
|
||||
text: InstanceStatusMapValue[InstanceStatusMap.Running],
|
||||
message: model.state_message
|
||||
}}
|
||||
></StatusTag>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="extra-info">
|
||||
<ThemeTag className="tag-item" color="blue">
|
||||
{_.find(modelCategories, { value: model.categories?.[0] })
|
||||
?.label || model.categories?.[0]}
|
||||
</ThemeTag>
|
||||
<ThemeTag className="tag-item" color="purple">
|
||||
128K context
|
||||
</ThemeTag>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<span className="time">
|
||||
{dayjs(model.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</span>
|
||||
<Button
|
||||
size="middle"
|
||||
className="btn"
|
||||
variant="filled"
|
||||
color="default"
|
||||
>
|
||||
Go to Playground
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModelItemContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import AlertBlockInfo from '@/components/alert-info/block';
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding-inline: 24px;
|
||||
`;
|
||||
|
||||
const Tips = styled.div`
|
||||
margin-top: 8px !important;
|
||||
ul {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin-top: 8px !important;
|
||||
li {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
line-height: 24px;
|
||||
margin: 0 !important;
|
||||
padding-inline: 16px 0 !important;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--ant-color-text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bold {
|
||||
font-weight: 700;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
.notice {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
color: var(--ant-color-text);
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const RenderMessage = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Tips>
|
||||
<div className="notice">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage({
|
||||
id: 'models.ollama.deprecated.notice'
|
||||
})
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</Tips>
|
||||
);
|
||||
};
|
||||
|
||||
const OllamaTips = () => {
|
||||
const intl = useIntl();
|
||||
const [maxHeight, setMaxHeight] = useState(1);
|
||||
const handleShowMore = () => {
|
||||
setMaxHeight(maxHeight === 1 ? 600 : 1);
|
||||
};
|
||||
return (
|
||||
<Wrapper>
|
||||
<AlertBlockInfo
|
||||
contentStyle={{ paddingInline: 0 }}
|
||||
maxHeight={maxHeight}
|
||||
ellipsis={false}
|
||||
message={<RenderMessage></RenderMessage>}
|
||||
title={
|
||||
<TitleWrapper>
|
||||
<div>
|
||||
{intl.formatMessage({ id: 'models.ollama.deprecated.title' })}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleShowMore}
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<IconFont type="icon-down"></IconFont>}
|
||||
></Button>
|
||||
</TitleWrapper>
|
||||
}
|
||||
type={'warning'}
|
||||
></AlertBlockInfo>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OllamaTips;
|
||||
@@ -1,12 +1,15 @@
|
||||
import CheckboxField from '@/components/seal-form/checkbox-field';
|
||||
import SealCascader from '@/components/seal-form/seal-cascader';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import TooltipList from '@/components/tooltip-list';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Checkbox, Form, Tooltip } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { Form } from 'antd';
|
||||
import React from 'react';
|
||||
import { backendOptionsMap } from '../config';
|
||||
import { useFormContext } from '../config/form-context';
|
||||
import { FormData } from '../config/types';
|
||||
import GPUCard from './gpu-card';
|
||||
|
||||
const scheduleTypeTips = [
|
||||
{
|
||||
@@ -25,29 +28,15 @@ const scheduleTypeTips = [
|
||||
}
|
||||
];
|
||||
|
||||
const CheckboxField: React.FC<{
|
||||
title: string;
|
||||
label: string;
|
||||
checked?: boolean;
|
||||
onChange?: (e: CheckboxChangeEvent) => void;
|
||||
}> = ({ title, label, checked, onChange }) => {
|
||||
return (
|
||||
<Checkbox className="p-l-6" checked={checked} onChange={onChange}>
|
||||
<Tooltip title={title}>
|
||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>{label}</span>
|
||||
<QuestionCircleOutlined
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
const Performance: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { onValuesChange, onQuantizationChange, source, quantizationOptions } =
|
||||
useFormContext();
|
||||
const {
|
||||
onValuesChange,
|
||||
onQuantizationChange,
|
||||
gpuOptions,
|
||||
source,
|
||||
quantizationOptions
|
||||
} = useFormContext();
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
@@ -61,6 +50,8 @@ const Performance: React.FC = () => {
|
||||
onQuantizationChange?.(val);
|
||||
};
|
||||
|
||||
const handleGpuSelectorChange = (value: any) => {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="scheduleType">
|
||||
@@ -80,30 +71,102 @@ const Performance: React.FC = () => {
|
||||
id: 'models.form.scheduletype.manual'
|
||||
}),
|
||||
value: 'manual'
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'models.form.scheduletype.gpuType'
|
||||
}),
|
||||
value: 'specific_gpu_type'
|
||||
}
|
||||
]}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="quantization"
|
||||
key="quantization"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('select', 'quantization', false)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealSelect
|
||||
filterOption
|
||||
defaultActiveFirstOption
|
||||
disabled={false}
|
||||
options={quantizationOptions}
|
||||
onChange={handleOnQuantizationChange}
|
||||
label="Quantization"
|
||||
required
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
{form.getFieldValue('scheduleType') === 'specific_gpu_type' && (
|
||||
<>
|
||||
<Form.Item name={['gpu_selector', 'gpu_type']}>
|
||||
<SealSelect
|
||||
label={intl.formatMessage({ id: 'models.form.gpuType' })}
|
||||
options={[
|
||||
{
|
||||
label: 'NVIDIA 4090',
|
||||
value: 'nvidia-4090'
|
||||
},
|
||||
{
|
||||
label: 'NVIDIA A100',
|
||||
value: 'nvidia-a100'
|
||||
},
|
||||
{
|
||||
label: 'NVIDIA H100',
|
||||
value: 'nvidia-h100'
|
||||
},
|
||||
{
|
||||
label: 'Huawei 910B',
|
||||
value: 'huawei-910b'
|
||||
},
|
||||
{
|
||||
label: 'Huawei 910C',
|
||||
value: 'huawei-910c'
|
||||
}
|
||||
]}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
<Form.Item name={['gpu_selector', 'gpu_count']}>
|
||||
<SealSelect
|
||||
label={intl.formatMessage({ id: 'models.form.gpuCount' })}
|
||||
options={[
|
||||
{
|
||||
label: 'Auto',
|
||||
value: 'auto'
|
||||
},
|
||||
{
|
||||
label: '1',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '2',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: '4',
|
||||
value: 4
|
||||
}
|
||||
]}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{form.getFieldValue('scheduleType') === 'manual' &&
|
||||
!form.getFieldValue('fix_gpu_type') && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={['gpu_selector', 'gpu_ids']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('select', 'models.form.gpuselector')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealCascader
|
||||
required
|
||||
showSearch
|
||||
expandTrigger="hover"
|
||||
multiple={
|
||||
form.getFieldValue('backend') !== backendOptionsMap.voxBox
|
||||
}
|
||||
popupClassName="cascader-popup-wrapper gpu-selector"
|
||||
maxTagCount={1}
|
||||
label={intl.formatMessage({ id: 'models.form.gpuselector' })}
|
||||
options={gpuOptions}
|
||||
showCheckedStrategy="SHOW_CHILD"
|
||||
value={form.getFieldValue(['gpu_selector', 'gpu_ids'])}
|
||||
optionNode={GPUCard}
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
||||
onChange={handleGpuSelectorChange}
|
||||
></SealCascader>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<div style={{ paddingBottom: 22, paddingLeft: 10 }}>
|
||||
<Form.Item<FormData>
|
||||
name="optimize_long_prompt"
|
||||
@@ -112,8 +175,7 @@ const Performance: React.FC = () => {
|
||||
noStyle
|
||||
>
|
||||
<CheckboxField
|
||||
title="Optimize long prompt tips"
|
||||
label="Optimize Long Prompt"
|
||||
label={intl.formatMessage({ id: 'models.form.optimizeLongPrompt' })}
|
||||
></CheckboxField>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -125,8 +187,9 @@ const Performance: React.FC = () => {
|
||||
noStyle
|
||||
>
|
||||
<CheckboxField
|
||||
title="Enable speculative decoding tips"
|
||||
label="Enable Speculative Decoding"
|
||||
label={intl.formatMessage({
|
||||
id: 'models.form.enableSpeculativeDecoding'
|
||||
})}
|
||||
></CheckboxField>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -111,6 +111,36 @@ interface ModelsProps {
|
||||
total: number;
|
||||
}
|
||||
|
||||
const clusterList = [
|
||||
{
|
||||
label: 'Custom',
|
||||
value: 'custom'
|
||||
},
|
||||
{
|
||||
label: 'Kubernetes',
|
||||
value: 'kubernetes'
|
||||
},
|
||||
{
|
||||
label: 'Digital Ocean',
|
||||
value: 'digital_ocean'
|
||||
}
|
||||
];
|
||||
|
||||
const statusList = [
|
||||
{
|
||||
label: 'Running',
|
||||
value: 'running'
|
||||
},
|
||||
{
|
||||
label: 'Stopped',
|
||||
value: 'stopped'
|
||||
},
|
||||
{
|
||||
label: 'Error',
|
||||
value: 'error'
|
||||
}
|
||||
];
|
||||
|
||||
const getFormattedData = (record: any, extraData = {}) => ({
|
||||
id: record.id,
|
||||
data: {
|
||||
@@ -605,7 +635,6 @@ const Models: React.FC<ModelsProps> = ({
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 400,
|
||||
span: 5,
|
||||
render: (text: string, record: ListItem) => (
|
||||
<span className="flex-center" style={{ maxWidth: '100%' }}>
|
||||
@@ -620,10 +649,10 @@ const Models: React.FC<ModelsProps> = ({
|
||||
title: 'Cluster',
|
||||
dataIndex: 'cluster',
|
||||
key: 'cluster',
|
||||
span: 4,
|
||||
span: 3,
|
||||
render: (text: string, record: ListItem) => (
|
||||
<span className="flex flex-column" style={{ width: '100%' }}>
|
||||
Custom
|
||||
{['Custom', 'Kubernetes', 'Digital Ocean'][record.id] || 'Custom'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
@@ -631,7 +660,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
title: intl.formatMessage({ id: 'models.form.source' }),
|
||||
dataIndex: 'source',
|
||||
key: 'source',
|
||||
span: 4,
|
||||
span: 5,
|
||||
render: (text: string, record: ListItem) => (
|
||||
<span className="flex flex-column" style={{ width: '100%' }}>
|
||||
<AutoTooltip ghost>{generateSource(record)}</AutoTooltip>
|
||||
@@ -778,7 +807,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
<Space>
|
||||
<Input
|
||||
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
|
||||
style={{ width: 200 }}
|
||||
style={{ width: 160 }}
|
||||
size="large"
|
||||
allowClear
|
||||
onChange={handleNameChange}
|
||||
@@ -789,7 +818,7 @@ const Models: React.FC<ModelsProps> = ({
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'models.filter.category'
|
||||
})}
|
||||
style={{ width: 180 }}
|
||||
style={{ width: 160 }}
|
||||
size="large"
|
||||
maxTagCount={1}
|
||||
onChange={handleCategoryChange}
|
||||
@@ -798,11 +827,20 @@ const Models: React.FC<ModelsProps> = ({
|
||||
<Select
|
||||
allowClear
|
||||
showSearch={false}
|
||||
placeholder="Filter by worker"
|
||||
style={{ width: 180 }}
|
||||
placeholder="Filter by cluster"
|
||||
style={{ width: 160 }}
|
||||
size="large"
|
||||
maxTagCount={1}
|
||||
options={[]}
|
||||
options={clusterList}
|
||||
></Select>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch={false}
|
||||
placeholder="Running Replicas"
|
||||
style={{ width: 140 }}
|
||||
size="large"
|
||||
maxTagCount={1}
|
||||
options={statusList}
|
||||
></Select>
|
||||
<Button
|
||||
type="text"
|
||||
|
||||
@@ -23,7 +23,6 @@ import { FormContext, FormInnerContext } from '../config/form-context';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
import HuggingFaceForm from '../forms/hugging-face';
|
||||
import LocalPathForm from '../forms/local-path';
|
||||
import OllamaForm from '../forms/ollama_library';
|
||||
import { useCheckCompatibility } from '../hooks';
|
||||
import AdvanceConfig from './advance-config';
|
||||
import ColumnWrapper from './column-wrapper';
|
||||
@@ -396,7 +395,6 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
|
||||
}}
|
||||
>
|
||||
<HuggingFaceForm></HuggingFaceForm>
|
||||
<OllamaForm></OllamaForm>
|
||||
<LocalPathForm></LocalPathForm>
|
||||
</FormInnerContext.Provider>
|
||||
<Form.Item name="backend" rules={[{ required: true }]}>
|
||||
@@ -447,30 +445,6 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
|
||||
}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
name="replicas"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'models.form.replicas')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Number
|
||||
style={{ width: '100%' }}
|
||||
label={intl.formatMessage({
|
||||
id: 'models.form.replicas'
|
||||
})}
|
||||
required
|
||||
description={intl.formatMessage(
|
||||
{
|
||||
id: 'models.form.replicas.tips'
|
||||
},
|
||||
{ api: `${window.location.origin}/v1` }
|
||||
)}
|
||||
min={0}
|
||||
></SealInput.Number>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData> name="description">
|
||||
<SealInput.TextArea
|
||||
scaleSize={true}
|
||||
|
||||
@@ -9,6 +9,7 @@ interface FormContextProps {
|
||||
sizeOptions?: Global.BaseOption<number>[];
|
||||
quantizationOptions?: Global.BaseOption<string>[];
|
||||
modelFileOptions?: any[];
|
||||
gpuOptions?: any[];
|
||||
onSizeChange?: (val: number) => void;
|
||||
onQuantizationChange?: (val: string) => void;
|
||||
onValuesChange?: (changedValues: any, allValues: any) => void;
|
||||
|
||||
@@ -83,10 +83,6 @@ export const ollamaModelOptions = [
|
||||
];
|
||||
|
||||
export const backendTipsList = [
|
||||
{
|
||||
title: 'llama-box',
|
||||
tips: 'models.form.backend.llamabox'
|
||||
},
|
||||
{
|
||||
title: 'vLLM',
|
||||
tips: 'models.form.backend.vllm'
|
||||
@@ -537,3 +533,20 @@ export const getBackendParamsTips = (backend: string) => {
|
||||
version: 'v0.0.13'
|
||||
};
|
||||
};
|
||||
|
||||
export const scheduleTypeTips = [
|
||||
{
|
||||
title: {
|
||||
text: 'models.form.scheduletype.auto',
|
||||
locale: true
|
||||
},
|
||||
tips: 'models.form.scheduletype.auto.tips'
|
||||
},
|
||||
{
|
||||
title: {
|
||||
text: 'models.form.scheduletype.manual',
|
||||
locale: true
|
||||
},
|
||||
tips: 'models.form.scheduletype.manual.tips'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -56,7 +56,9 @@ export interface FormData {
|
||||
model_scope_model_id?: string;
|
||||
model_scope_file_path?: string;
|
||||
gpu_selector?: {
|
||||
gpu_ids: string[];
|
||||
gpu_ids?: string[];
|
||||
gpu_type?: string;
|
||||
gpu_count?: number;
|
||||
};
|
||||
placement_strategy?: string;
|
||||
cpu_offloading?: boolean;
|
||||
@@ -65,6 +67,8 @@ export interface FormData {
|
||||
name: string;
|
||||
replicas: number;
|
||||
description: string;
|
||||
optimize_long_prompt: boolean;
|
||||
enable_speculative_decoding: boolean;
|
||||
}
|
||||
|
||||
interface ComputedResourceClaim {
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import SealAutoComplete from '@/components/seal-form/auto-complete';
|
||||
import SealInput from '@/components/seal-form/seal-input';
|
||||
import SealSelect from '@/components/seal-form/seal-select';
|
||||
import TooltipList from '@/components/tooltip-list';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { ModelFileFormData as FormData } from '@/pages/resources/config/types';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form, Typography } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import OllamaTips from '../components/ollama-tips';
|
||||
import {
|
||||
localPathTipsList,
|
||||
modelSourceMap,
|
||||
ollamaModelOptions,
|
||||
sourceOptions
|
||||
} from '../config';
|
||||
import { localPathTipsList, modelSourceMap, sourceOptions } from '../config';
|
||||
|
||||
interface TargetFormProps {
|
||||
ref?: any;
|
||||
@@ -73,56 +65,7 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderOllamaModelFields = () => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
name="ollama_library_model_name"
|
||||
key="ollama_library_model_name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'models.table.name')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealAutoComplete
|
||||
allowClear
|
||||
filterOption
|
||||
defaultActiveFirstOption
|
||||
disabled={false}
|
||||
options={ollamaModelOptions}
|
||||
description={
|
||||
<span>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'models.form.ollamalink' })}
|
||||
</span>
|
||||
<Typography.Link
|
||||
className="flex-center"
|
||||
href="https://www.ollama.com/library"
|
||||
target="_blank"
|
||||
>
|
||||
<IconFont
|
||||
type="icon-external-link"
|
||||
className="font-size-14"
|
||||
></IconFont>
|
||||
</Typography.Link>
|
||||
</span>
|
||||
}
|
||||
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
|
||||
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
|
||||
required
|
||||
></SealAutoComplete>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFieldsBySource = useMemo(() => {
|
||||
if (props.source === modelSourceMap.ollama_library_value) {
|
||||
return renderOllamaModelFields();
|
||||
}
|
||||
|
||||
if (props.source === modelSourceMap.local_path_value) {
|
||||
return renderLocalPathFields();
|
||||
}
|
||||
@@ -132,9 +75,6 @@ const TargetForm: React.FC<TargetFormProps> = forwardRef((props, ref) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{source === modelSourceMap.ollama_library_value && (
|
||||
<OllamaTips></OllamaTips>
|
||||
)}
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleOk}
|
||||
|
||||
@@ -51,25 +51,6 @@ const HuggingFaceForm: React.FC = () => {
|
||||
onBlur={handleOnBlur}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
{/* {isGGUF && (
|
||||
<Form.Item<FormData>
|
||||
name="file_name"
|
||||
key="file_name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'models.form.filename')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealInput.Input
|
||||
label={intl.formatMessage({ id: 'models.form.filename' })}
|
||||
required
|
||||
disabled={pageAction === PageAction.CREATE}
|
||||
onBlur={handleOnBlur}
|
||||
></SealInput.Input>
|
||||
</Form.Item>
|
||||
)} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import SealAutoComplete from '@/components/seal-form/auto-complete';
|
||||
import useAppUtils from '@/hooks/use-app-utils';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import React from 'react';
|
||||
import { modelSourceMap, ollamaModelOptions } from '../config';
|
||||
import { useFormContext, useFormInnerContext } from '../config/form-context';
|
||||
import { FormData } from '../config/types';
|
||||
|
||||
const OllamaForm: React.FC = () => {
|
||||
const formCtx = useFormContext();
|
||||
const formInnerCtx = useFormInnerContext();
|
||||
const { byBuiltIn, onValuesChange } = formCtx;
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
const intl = useIntl();
|
||||
const source = Form.useWatch('source');
|
||||
const formInstance = Form.useFormInstance();
|
||||
|
||||
if (![modelSourceMap.ollama_library_value].includes(source) || byBuiltIn) {
|
||||
return null;
|
||||
}
|
||||
const handleModelNameChange = (value: string) => {
|
||||
if (value) {
|
||||
onValuesChange?.({}, formInstance.getFieldsValue());
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = (e: any) => {
|
||||
onValuesChange?.({}, formInstance.getFieldsValue());
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
name="ollama_library_model_name"
|
||||
key="ollama_library_model_name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: getRuleMessage('input', 'models.table.name')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealAutoComplete
|
||||
allowClear
|
||||
filterOption
|
||||
defaultActiveFirstOption
|
||||
disabled={false}
|
||||
options={ollamaModelOptions}
|
||||
onSelect={handleModelNameChange}
|
||||
onBlur={handleOnBlur}
|
||||
description={
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage({ id: 'models.form.ollamalink' })
|
||||
}}
|
||||
></span>
|
||||
}
|
||||
label={intl.formatMessage({ id: 'model.form.ollama.model' })}
|
||||
placeholder={intl.formatMessage({ id: 'model.form.ollamaholder' })}
|
||||
required
|
||||
></SealAutoComplete>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OllamaForm;
|
||||
@@ -104,8 +104,10 @@ export const useGenerateFormEditInitialValues = () => {
|
||||
queryWorkersList({ page: 1, perPage: 100 })
|
||||
]);
|
||||
const gpuList = generateCascaderOptions(gpuData.items, workerData.items);
|
||||
|
||||
gpuDeviceList.current = gpuList;
|
||||
workerList.current = workerData.items;
|
||||
|
||||
return gpuList;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
import { FilterBar } from '@/components/page-tools';
|
||||
import MoreButton from '@/components/buttons/more';
|
||||
import PageTools from '@/components/page-tools';
|
||||
import CardList from '@/components/templates/card-list';
|
||||
import CardSkeleton from '@/components/templates/card-skelton';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Input, Select, Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { MODELS_API, queryModelsList } from './apis';
|
||||
import ModelItem from './components/model-item';
|
||||
|
||||
const UserModels: React.FC = () => {
|
||||
const { dataSource, queryParams, fetchData, handleSearch, handleNameChange } =
|
||||
useTableFetch<any>({
|
||||
fetchAPI: queryModelsList,
|
||||
API: MODELS_API,
|
||||
watch: false
|
||||
});
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSearch = () => {
|
||||
// Implement search functionality here
|
||||
const loadMore = () => {
|
||||
fetchData({
|
||||
...queryParams,
|
||||
page: queryParams.page + 1
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = () => {};
|
||||
|
||||
const handleOnDeploy = (model: any) => {
|
||||
// Implement deploy functionality here
|
||||
const handleOnClick = (model: any) => {
|
||||
console.log('Deploying model:', model);
|
||||
};
|
||||
|
||||
const renderCard = (data: any) => {
|
||||
return <ModelItem model={data} onDeploy={handleOnDeploy} />;
|
||||
return <ModelItem model={data} onClick={handleOnClick} />;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -34,27 +47,55 @@ const UserModels: React.FC = () => {
|
||||
}}
|
||||
extra={[]}
|
||||
>
|
||||
<FilterBar
|
||||
showSelect={true}
|
||||
showPrimaryButton={false}
|
||||
showDeleteButton={false}
|
||||
selectHolder="Filter by cluster"
|
||||
<PageTools
|
||||
marginBottom={22}
|
||||
marginTop={30}
|
||||
buttonText={intl.formatMessage({ id: 'resources.button.create' })}
|
||||
handleInputChange={() => {}}
|
||||
handleSearch={handleSearch}
|
||||
width={{ input: 200 }}
|
||||
></FilterBar>
|
||||
{/* <CardList
|
||||
left={
|
||||
<Space>
|
||||
<Input
|
||||
placeholder={intl.formatMessage({ id: 'common.filter.name' })}
|
||||
style={{ width: 230 }}
|
||||
size="large"
|
||||
allowClear
|
||||
onClear={() =>
|
||||
handleNameChange({
|
||||
target: {
|
||||
value: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch={false}
|
||||
placeholder={intl.formatMessage({ id: 'models.filter.category' })}
|
||||
style={{ width: 180 }}
|
||||
size="large"
|
||||
maxTagCount={1}
|
||||
options={[]}
|
||||
></Select>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
icon={<SyncOutlined></SyncOutlined>}
|
||||
onClick={handleSearch}
|
||||
></Button>
|
||||
</Space>
|
||||
}
|
||||
></PageTools>
|
||||
<CardList
|
||||
dataList={dataSource.dataList}
|
||||
loading={dataSource.loading}
|
||||
onDeploy={handleOnDeploy}
|
||||
activeId={-1}
|
||||
isFirst={isFirst}
|
||||
Skeleton={CatalogSkelton}
|
||||
isFirst={!dataSource.loadend}
|
||||
Skeleton={CardSkeleton}
|
||||
renderItem={renderCard}
|
||||
></CardList> */}
|
||||
></CardList>
|
||||
<MoreButton
|
||||
show={queryParams.page < dataSource.totalPage}
|
||||
loading={dataSource.loading}
|
||||
loadMore={loadMore}
|
||||
></MoreButton>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user