feat: provider form

This commit is contained in:
jialin
2026-02-03 18:21:08 +08:00
parent 679e10abd6
commit b2a8d27178
15 changed files with 424 additions and 32 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_qm5jah0zmp8.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_j0qgzymxj98.js'
});
export default IconFont;
+11 -1
View File
@@ -15,6 +15,13 @@ interface ListInputProps {
onChange: (data: string[]) => void;
onBlur?: (e: any, index: number) => void;
onDelete?: (index: number) => void;
renderItem?: (
data: any,
props: {
onChange: (value: string) => void;
onBlur?: (e: any) => void;
}
) => React.ReactNode;
}
const ListInput: React.FC<ListInputProps> = (props) => {
@@ -28,7 +35,8 @@ const ListInput: React.FC<ListInputProps> = (props) => {
btnText,
options,
labelExtra,
trim = true
trim = true,
renderItem
} = props;
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
const countRef = React.useRef(0);
@@ -92,12 +100,14 @@ const ListInput: React.FC<ListInputProps> = (props) => {
<ListItem
placeholder={props.placeholder}
options={options}
data={item}
key={item.uid}
value={item.value}
onBlur={(e) => onBlur?.(e, index)}
onRemove={() => handleOnRemove(index)}
onChange={(val) => handleOnChange(val, index)}
trim={trim}
renderItem={renderItem}
/>
);
})}
+27 -10
View File
@@ -8,11 +8,19 @@ interface LabelItemProps {
onRemove: () => void;
onChange: (value: string) => void;
onBlur?: (e: any) => void;
renderItem?: (
data: any,
props: {
onChange: (value: string) => void;
onBlur?: (e: any) => void;
}
) => React.ReactNode;
value: string;
label?: string;
placeholder?: string;
options?: Global.HintOptions[];
trim?: boolean;
data?: any;
}
const ListItem: React.FC<LabelItemProps> = (props) => {
@@ -23,7 +31,9 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
label,
value,
options,
trim = true
trim = true,
data,
renderItem
} = props;
const handleOnChange = (value: any) => {
@@ -32,15 +42,22 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
return (
<div className="list-item">
<HintInput
value={value}
onChange={handleOnChange}
onBlur={onBlur}
label={label}
sourceOptions={options}
trim={trim}
placeholder={props.placeholder}
/>
{renderItem ? (
renderItem(data, {
onChange: handleOnChange,
onBlur
})
) : (
<HintInput
value={value}
onChange={handleOnChange}
onBlur={onBlur}
label={label}
sourceOptions={options}
trim={trim}
placeholder={props.placeholder}
/>
)}
<Button
size="small"
className="btn"
+4 -2
View File
@@ -11,7 +11,8 @@ import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select';
const SealSelect: React.FC<
SelectProps & SealFormItemProps & { footer?: React.ReactNode }
SelectProps &
SealFormItemProps & { footer?: React.ReactNode; alwaysFocus?: boolean }
> = (props) => {
const {
label,
@@ -25,6 +26,7 @@ const SealSelect: React.FC<
notFoundContent = null,
loading,
footer,
alwaysFocus = false,
...rest
} = props;
const intl = useIntl();
@@ -99,7 +101,7 @@ const SealSelect: React.FC<
className="seal-select-wrapper"
status={status}
label={label}
isFocus={isFocus}
isFocus={alwaysFocus || isFocus}
required={required}
description={description}
disabled={props.disabled}
+5 -1
View File
@@ -2,6 +2,7 @@ import styled from 'styled-components';
import { BORDERRADIUS, INPUT_INNER_PADDING, INPUTHEIGHT } from '../config';
const SelectWrapper = styled.div`
flex: 1;
.seal-select-wrapper {
border: none;
box-shadow: none;
@@ -89,6 +90,9 @@ const SelectWrapper = styled.div`
left: 0;
right: 0;
}
.ant-select .ant-select-content {
padding-block: 0px 0;
}
}
}
.ant-select-selection-overflow-item > span {
@@ -118,7 +122,7 @@ const SelectWrapper = styled.div`
}
.ant-select-content {
padding-block: 20px 0 !important;
padding-block: 20px 0;
box-shadow: none !important;
}
&.ant-cascader {
+2 -2
View File
@@ -1,9 +1,9 @@
import { request } from '@umijs/max';
import { FormData, MaasProviderItem } from '../config/types';
export const MAAS_PROVIDERS_API = '/maas-providers';
export const MAAS_PROVIDERS_API = '/models';
export const PROVIDER_MODELS_API = '/maas-provider-models';
export const PROVIDER_MODELS_API = '/models';
export async function queryMaasProviders(
params: Global.SearchParams,
@@ -1,3 +1,10 @@
import { categoryOptions, modelCategoriesMap } from '@/pages/llmodels/config';
import {
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons';
import { Flex, Tag } from 'antd';
import React from 'react';
interface ProviderModelProps {
@@ -6,8 +13,61 @@ interface ProviderModelProps {
providerId: number;
}
const models: {
name: string;
status: 'accessible' | 'inaccessible' | 'none';
type: string;
}[] = [
{
name: 'model-1',
status: 'accessible',
type: modelCategoriesMap.llm
},
{
name: 'model-2',
status: 'inaccessible',
type: modelCategoriesMap.embedding
},
{
name: 'model-3',
status: 'none',
type: modelCategoriesMap.image
},
{
name: 'model-4',
status: 'accessible',
type: modelCategoriesMap.llm
}
];
const ProviderModels: React.FC<ProviderModelProps> = () => {
return <div>Provider Models Component</div>;
const iconsMap = {
accessible: <CheckCircleOutlined />,
inaccessible: <CloseCircleOutlined />,
none: <ExclamationCircleOutlined />
};
return (
<div style={{ paddingInline: 16 }}>
<Flex gap="8px" wrap="wrap">
{models.map((model) => (
<Tag
key={model.name}
icon={iconsMap[model.status]}
color={
model.status === 'accessible'
? 'success'
: model.status === 'inaccessible'
? 'red'
: 'gray'
}
>
{model.name} (
{categoryOptions.find((item) => item.value === model.type)?.label})
</Tag>
))}
</Flex>
</div>
);
};
export default ProviderModels;
+1 -1
View File
@@ -21,7 +21,7 @@ export const maasProviderLabelMap = {
[maasProviderValueMap.Doubao]: 'Doubao',
[maasProviderValueMap.Qwen]: 'Qwen',
[maasProviderValueMap.OpenAI]: 'OpenAI',
[maasProviderValueMap.Deepseek]: 'Deepseek',
[maasProviderValueMap.Deepseek]: 'DeepSeek',
[maasProviderValueMap.Anthropic]: 'Anthropic'
};
@@ -0,0 +1,26 @@
import ListInput from '@/components/list-input';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData } from '../config/types';
const AccessToken = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const handleOnChange = (values: string[]) => {};
return (
<>
<Form.Item name="tokens">
<ListInput
btnText="Add Token"
label="Access Tokens"
dataList={[]}
onChange={handleOnChange}
></ListInput>
</Form.Item>
</>
);
};
export default AccessToken;
@@ -0,0 +1,58 @@
import SealInput from '@/components/seal-form/seal-input';
import { PageActionType } from '@/config/types';
import YamlEditor from '@/pages/_components/yaml-editor';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react';
import { maasProviderType } from '../config';
const AdvanceConfig: React.FC<{
action: PageActionType;
provider?: maasProviderType;
ref?: any;
}> = forwardRef(({ action, provider }, ref) => {
const [form] = Form.useForm();
const intl = useIntl();
const editorRef = React.useRef<any>(null);
const [fileContent, setFileContent] = React.useState<string>('');
useImperativeHandle(ref, () => ({
getYamlValue: () => {
return editorRef.current?.getValue();
},
setYamlValue: (values: any) => {
console.log('setYamlValue:', values);
editorRef.current?.setValue(values || '');
}
}));
return (
<>
<div data-field="customConfig"></div>
<Form.Item
hidden
name="custom_config"
rules={[
{
required: false,
message: ''
}
]}
>
<SealInput.TextArea required={false} trim={false}></SealInput.TextArea>
</Form.Item>
<YamlEditor
ref={editorRef}
title={'Yaml'}
value={fileContent}
height={300}
onUpload={(content) => {
setFileContent(content);
}}
></YamlEditor>
<div className="scroller-to-holder" style={{ height: 1 }}></div>
</>
);
});
export default AdvanceConfig;
+38
View File
@@ -0,0 +1,38 @@
import SealInput from '@/components/seal-form/seal-input';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData } from '../config/types';
const Basic = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
return (
<>
<Form.Item name="name" data-field="name">
<SealInput.Input
required
label={intl.formatMessage({
id: 'common.table.name'
})}
/>
</Form.Item>
<Form.Item name="provider" hidden>
<SealInput.Input
label={intl.formatMessage({
id: 'providers.table.providerName'
})}
/>
</Form.Item>
<Form.Item name="description">
<SealInput.TextArea
scaleSize={true}
label={intl.formatMessage({
id: 'common.table.description'
})}
></SealInput.TextArea>
</Form.Item>
</>
);
};
export default Basic;
+100 -4
View File
@@ -1,17 +1,75 @@
import IconFont from '@/components/icon-font';
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 { Form } from 'antd';
import { forwardRef, useImperativeHandle } from 'react';
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { maasProviderType } from '../config';
import { MaasProviderItem as ListItem } from '../config/types';
import AccessToken from './access-token';
import AdvanceConfig from './advance-config';
import Basic from './basic';
import SupportedModels from './supported-models';
interface ProviderFormProps {
ref?: any;
action: PageActionType;
provider?: maasProviderType;
currentData?: ListItem; // Used when action is EDIT
onFinish: (values: FormData) => void;
}
const TABKeysMap = {
BASIC: 'basic',
SUPPORTEDMODELS: 'supportedModels',
CUSTOMCONFIG: 'customConfig',
ADVANCED: 'advanced'
};
const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const { action, provider, currentData, onFinish } = props;
const intl = useIntl();
const { getScrollElementScrollableHeight } = useWrapperContext();
const [activeKey, setActiveKey] = useState<string[]>([TABKeysMap.BASIC]);
const [form] = Form.useForm();
const scrollTabsRef = useRef<any>(null);
const segmentOptions = [
{
value: TABKeysMap.BASIC,
label: 'Basic',
icon: <IconFont type="icon-basic" />,
field: 'name'
},
{
value: TABKeysMap.SUPPORTEDMODELS,
label: 'Supported Models',
icon: <IconFont type="icon-speed" />,
field: 'supportedModels'
},
{
value: TABKeysMap.CUSTOMCONFIG,
label: 'Custom Config',
icon: <IconFont type="icon-settings" />,
field: 'customConfig'
}
// {
// value: TABKeysMap.ADVANCED,
// label: intl.formatMessage({ id: 'resources.form.advanced' }),
// icon: <IconFont type="icon-settings" />,
// field: 'categories'
// }
];
const handleActiveChange = (key: string[]) => {
setActiveKey(key);
};
const handleOnCollapseChange = (keys: string | string[]) => {
setActiveKey(Array.isArray(keys) ? keys : [keys]);
};
useImperativeHandle(ref, () => ({
submit: () => {
@@ -23,9 +81,47 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
}));
return (
<Form form={form} layout="vertical">
{/* Form fields go here */}
</Form>
<ScrollSpyTabs
ref={scrollTabsRef}
defaultTarget="basic"
segmentOptions={segmentOptions}
activeKey={activeKey}
setActiveKey={handleActiveChange}
segmentedTop={{
top: 0,
offsetTop: 96
}}
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
>
<Form form={form}>
<Basic />
<AccessToken />
<CollapsePanel
activeKey={activeKey}
accordion={false}
onChange={handleOnCollapseChange}
items={[
{
key: TABKeysMap.SUPPORTEDMODELS,
label: 'Supported Models',
forceRender: true,
children: <SupportedModels></SupportedModels>
},
{
key: TABKeysMap.CUSTOMCONFIG,
label: 'Custom Config',
forceRender: true,
children: (
<AdvanceConfig
action={action}
provider={provider}
></AdvanceConfig>
)
}
]}
></CollapsePanel>
</Form>
</ScrollSpyTabs>
);
});
@@ -0,0 +1,75 @@
import ListInput from '@/components/list-input';
import SealSelect from '@/components/seal-form/seal-select';
import { CheckCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd';
import styled from 'styled-components';
import { FormData } from '../config/types';
const SelectWrapper = styled.div`
flex: 1;
display: flex;
align-items: center;
gap: 8px;
.icon-wrapper {
display: flex;
align-items: center;
height: 100%;
}
`;
const SupportedModels = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const renderModelItem = (
data: any,
{
onChange,
onBlur
}: { onChange: (value: string) => void; onBlur?: (e: any) => void }
) => {
return (
<SelectWrapper>
<SealSelect
alwaysFocus={true}
value={data.value}
onChange={onChange}
onBlur={onBlur}
options={[
{ label: 'Model A', value: 'model_a' },
{ label: 'Model B', value: 'model_b' },
{ label: 'Model C', value: 'model_c' }
]}
/>
<span className="icon-wrapper">
<CheckCircleFilled
style={{ color: 'var(--ant-color-success)', fontSize: 16 }}
/>
</span>
<Button type="link" size="small">
Test
</Button>
</SelectWrapper>
);
};
const handleOnChange = (values: string[]) => {};
return (
<>
<Form.Item name="models">
<div data-field="supportedModels"></div>
<ListInput
btnText="Add Model"
label="Supported Models"
dataList={[]}
renderItem={renderModelItem}
onChange={handleOnChange}
></ListInput>
</Form.Item>
</>
);
};
export default SupportedModels;
@@ -19,8 +19,15 @@ const useCreateProvider = (options: { refresh: () => void }) => {
provider: null
});
const openModal = () => {
setOpenModalStatus({ ...openModalStatus, open: true });
const openModal = (action: PageActionType, row?: ListItem) => {
setOpenModalStatus({
...openModalStatus,
open: true,
title: action === PageAction.CREATE ? 'Create Provider' : 'Edit Provider',
action,
currentData: row,
provider: row ? row.provider : null
});
};
const closeModal = () => {
+6 -7
View File
@@ -22,7 +22,6 @@ import {
PROVIDER_MODELS_API,
deleteProvider,
queryMaasProviders,
queryProviderModels,
updateProvider
} from './apis';
import AddMaasProvider from './components/add-provider-modal';
@@ -73,7 +72,7 @@ const MaasProvider: React.FC = () => {
});
const handleClickDropdown = () => {
openProviderModal();
openProviderModal('create');
};
const handleModalOk = async (data: FormData) => {
@@ -149,10 +148,10 @@ const MaasProvider: React.FC = () => {
cluster_id: row.id,
page: -1
};
const data = await queryProviderModels(params, {
token: options?.token
});
return data?.items || [];
// const data = await queryProviderModels(params, {
// token: options?.token
// });
return [1, 2, 3];
}
);
@@ -217,7 +216,7 @@ const MaasProvider: React.FC = () => {
loadend={dataSource.loadend}
rowSelection={rowSelection}
columns={columns}
childParentKey="cluster_id"
childParentKey="provider_id"
expandable={true}
empty={
<NoResult