chore: access form test

This commit is contained in:
jialin
2026-02-03 18:21:08 +08:00
parent e188d788b9
commit 26e3a1944b
19 changed files with 622 additions and 323 deletions
+9 -1
View File
@@ -51,6 +51,13 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
}); });
}; };
const handleDeleteItemFromCache = (id: number) => {
cacheWatchDataListRef.current = cacheWatchDataListRef.current.filter(
(item) => item.id !== id
);
setWatchDataList(cacheWatchDataListRef.current);
};
const getAllDataList = useMemoizedFn(async () => { const getAllDataList = useMemoizedFn(async () => {
try { try {
listRequestTokenRef.current?.cancel?.(); listRequestTokenRef.current?.cancel?.();
@@ -78,6 +85,7 @@ export default function useWatchList<T = Record<string, any>>(API: string) {
}, []); }, []);
return { return {
watchDataList watchDataList,
deleteItemFromCache: handleDeleteItemFromCache
}; };
} }
+3 -1
View File
@@ -259,6 +259,7 @@ export default {
'common.sso.noConfig': 'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.', 'Single sign-on is not enabled on this system. Please contact your administrator.',
'common.button.edit.item': 'Edit {name}', 'common.button.edit.item': 'Edit {name}',
'common.button.copy.item': 'Duplicate {name}',
'common.button.terminal': 'Terminal', 'common.button.terminal': 'Terminal',
'common.button.addItem': 'Add Item', 'common.button.addItem': 'Add Item',
'common.help.default': 'Default: {content}', 'common.help.default': 'Default: {content}',
@@ -273,5 +274,6 @@ export default {
'common.button.enable': 'Enable', 'common.button.enable': 'Enable',
'common.button.disable': 'Disable', 'common.button.disable': 'Disable',
'common.status.enabled': 'Enabled', 'common.status.enabled': 'Enabled',
'common.status.disabled': 'Disabled' 'common.status.disabled': 'Disabled',
'common.button.duplicate': 'Duplicate'
}; };
+4 -1
View File
@@ -259,6 +259,7 @@ export default {
'common.sso.noConfig': 'common.sso.noConfig':
'Single sign-on is not enabled on this system. Please contact your administrator.', 'Single sign-on is not enabled on this system. Please contact your administrator.',
'common.button.edit.item': 'Edit {name}', 'common.button.edit.item': 'Edit {name}',
'common.button.copy.item': 'Duplicate {name}',
'common.button.terminal': 'Terminal', 'common.button.terminal': 'Terminal',
'common.button.addItem': 'Add Item', 'common.button.addItem': 'Add Item',
'common.help.default': 'Default: {content}', 'common.help.default': 'Default: {content}',
@@ -273,7 +274,8 @@ export default {
'common.button.enable': 'Enable', 'common.button.enable': 'Enable',
'common.button.disable': 'Disable', 'common.button.disable': 'Disable',
'common.status.enabled': 'Enabled', 'common.status.enabled': 'Enabled',
'common.status.disabled': 'Disabled' 'common.status.disabled': 'Disabled',
'common.button.duplicate': 'Duplicate'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -313,4 +315,5 @@ export default {
// 34. 'common.button.disable': 'Disable', // 34. 'common.button.disable': 'Disable',
// 35. 'common.status.enabled': 'Enabled', // 35. 'common.status.enabled': 'Enabled',
// 36. 'common.status.disabled': 'Disabled' // 36. 'common.status.disabled': 'Disabled'
// 33. 'common.button.duplicate': 'Duplicate'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+5 -5
View File
@@ -258,6 +258,7 @@ export default {
'common.sso.noConfig': 'common.sso.noConfig':
'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.', 'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.',
'common.button.edit.item': 'Редактировать {name}', 'common.button.edit.item': 'Редактировать {name}',
'common.button.copy.item': 'Duplicate {name}',
'common.button.terminal': 'Терминал', 'common.button.terminal': 'Терминал',
'common.button.addItem': 'Добавить элемент', 'common.button.addItem': 'Добавить элемент',
'common.help.default': 'По умолчанию: {content}', 'common.help.default': 'По умолчанию: {content}',
@@ -272,12 +273,11 @@ export default {
'common.button.enable': 'Enable', 'common.button.enable': 'Enable',
'common.button.disable': 'Disable', 'common.button.disable': 'Disable',
'common.status.enabled': 'Enabled', 'common.status.enabled': 'Enabled',
'common.status.disabled': 'Disabled' 'common.status.disabled': 'Disabled',
'common.button.duplicate': 'Duplicate'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'common.button.enable': 'Enable', // 1. 'common.button.copy.item': 'Duplicate {name}',
// 2. 'common.button.disable': 'Disable', // 2. 'common.button.duplicate': 'Duplicate'
// 3. 'common.status.enabled': 'Enabled',
// 4. 'common.status.disabled': 'Disabled'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+3 -1
View File
@@ -251,6 +251,7 @@ export default {
'common.external.login': '使用 {type} 登录', 'common.external.login': '使用 {type} 登录',
'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。', 'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。',
'common.button.edit.item': '编辑 {name}', 'common.button.edit.item': '编辑 {name}',
'common.button.copy.item': '复制 {name}',
'common.button.terminal': '终端', 'common.button.terminal': '终端',
'common.button.addItem': '添加项', 'common.button.addItem': '添加项',
'common.help.default': '默认: {content}', 'common.help.default': '默认: {content}',
@@ -265,5 +266,6 @@ export default {
'common.button.enable': '启用', 'common.button.enable': '启用',
'common.button.disable': '禁用', 'common.button.disable': '禁用',
'common.status.enabled': '已启用', 'common.status.enabled': '已启用',
'common.status.disabled': '未启用' 'common.status.disabled': '未启用',
'common.button.duplicate': '复制'
}; };
+47 -7
View File
@@ -1,5 +1,12 @@
import IconFont from '@/components/icon-font';
import { StatusMaps } from '@/config'; import { StatusMaps } from '@/config';
import { EditOutlined } from '@ant-design/icons'; import {
AudioOutlined,
EditOutlined,
PictureOutlined,
WechatWorkOutlined
} from '@ant-design/icons';
import React from 'react';
import { backendOptionsMap } from './backend-parameters'; import { backendOptionsMap } from './backend-parameters';
export const backendTipsList = [ export const backendTipsList = [
@@ -290,12 +297,45 @@ export const modelCategoriesMap = {
}; };
export const categoryOptions = [ export const categoryOptions = [
{ label: 'LLM', value: modelCategoriesMap.llm }, {
{ label: 'Embedding', value: modelCategoriesMap.embedding }, label: 'LLM',
{ label: 'Reranker', value: modelCategoriesMap.reranker }, value: modelCategoriesMap.llm,
{ label: 'Image', value: modelCategoriesMap.image }, icon: React.createElement(WechatWorkOutlined, { style: { color: 'green' } })
{ label: 'Text-to-Speech', value: modelCategoriesMap.text_to_speech }, },
{ label: 'Speech-to-Text', value: modelCategoriesMap.speech_to_text } {
label: 'Embedding',
value: modelCategoriesMap.embedding,
icon: React.createElement(IconFont, {
type: 'icon-cube',
style: { color: 'magenta' }
})
},
{
label: 'Reranker',
value: modelCategoriesMap.reranker,
icon: React.createElement(IconFont, {
type: 'icon-rank1',
style: { color: 'cyan' }
})
},
{
label: 'Image',
value: modelCategoriesMap.image,
icon: React.createElement(PictureOutlined, { style: { color: 'orange' } })
},
{
label: 'Text-to-Speech',
value: modelCategoriesMap.text_to_speech,
icon: React.createElement(IconFont, {
type: 'icon-sound-wave',
style: { color: 'geekblue' }
})
},
{
label: 'Speech-to-Text',
value: modelCategoriesMap.speech_to_text,
icon: React.createElement(AudioOutlined, { style: { color: 'processing' } })
}
]; ];
export const modelCategories = [ export const modelCategories = [
+1 -1
View File
@@ -32,7 +32,7 @@ export const rowActionList = [
}, },
{ {
key: 'copy', key: 'copy',
label: 'common.button.copy', label: 'common.button.duplicate',
icon: icons.CopyOutlined icon: icons.CopyOutlined
}, },
{ {
+33 -4
View File
@@ -1,5 +1,5 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { AccessItem, FormData } from '../config/types'; import { AccessItem, AccessPointItem, FormData } from '../config/types';
export const ACCESS_API = '/model-accesses'; export const ACCESS_API = '/model-accesses';
@@ -36,12 +36,41 @@ export async function deleteAccess(id: number) {
}); });
} }
export async function queryAccessPoints(params: { id: string }, options?: any) { export async function queryAccessPoints(params: { id: number }, options?: any) {
return request<Global.BaseOption<string>[]>( return request<Global.PageResponse<AccessPointItem>>(
`${ACCESS_API}/${params.id}/access`, `${ACCESS_POINTS_API}?access_id=${params.id}`,
{ {
method: 'GET', method: 'GET',
params: {
page: -1
},
cancelToken: options?.token cancelToken: options?.token
} }
); );
} }
export async function deleteAccessPoint(id: number) {
return request(`${ACCESS_POINTS_API}/${id}`, {
method: 'DELETE'
});
}
export async function updateAccessPoint(params: {
id: number;
data: Partial<AccessPointItem>;
}) {
return request(`${ACCESS_POINTS_API}/${params.id}`, {
method: 'PUT',
data: params.data
});
}
export async function setAccessPointAsFallback(params: {
id: number;
data: Partial<AccessPointItem>;
}) {
return request(`${ACCESS_POINTS_API}/${params.id}/set-fallback`, {
method: 'POST',
data: params.data
});
}
@@ -9,7 +9,8 @@ import { Col, Row } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { mockAccessPointList } from '../config/mock'; import { EndpointStatus, EndpointStatusLabelMap } from '../config';
import { AccessPointItem } from '../config/types';
const CellContent = styled.div` const CellContent = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@@ -17,9 +18,7 @@ const CellContent = styled.div`
`; `;
interface ProviderModelProps { interface ProviderModelProps {
dataList: any[]; dataList: AccessPointItem[];
provider: string;
providerId: number;
onSelect: (val: any, record: any) => void; onSelect: (val: any, record: any) => void;
} }
@@ -61,27 +60,32 @@ const AccessItem: React.FC<AccessItemProps> = ({ onSelect, data }) => {
paddingInline: 'var(--ant-table-cell-padding-inline)' paddingInline: 'var(--ant-table-cell-padding-inline)'
}} }}
> >
<AutoTooltip ghost>qwen3-0.6b-zduxy</AutoTooltip> <AutoTooltip ghost>{data.provider_model_name}</AutoTooltip>
</CellContent> </CellContent>
</Col> </Col>
<Col span={3}> <Col span={3}>
<CellContent>OpenAI</CellContent> <CellContent>-</CellContent>
</Col> </Col>
<Col span={4}> <Col span={4}>
<CellContent> <CellContent>
{data.weight && ( {data.weight && (
<AutoTooltip ghost> <AutoTooltip ghost>
{intl.formatMessage({ id: 'accesses.form.endpoint.weight' })}: {intl.formatMessage({ id: 'accesses.form.endpoint.weight' })}:{' '}
20 / {data.weight}
</AutoTooltip> </AutoTooltip>
)} )}
{data.is_fallback && (
<span> {data.fallback_status_codes &&
{intl.formatMessage({ data.fallback_status_codes?.length > 0 && (
id: 'accesses.table.label.fallback' <>
})} <span style={{ marginInline: 8 }}>/</span>
</span> <span>
)} {intl.formatMessage({
id: 'accesses.table.label.fallback'
})}
</span>
</>
)}
</CellContent> </CellContent>
</Col> </Col>
<Col span={3}> <Col span={3}>
@@ -89,8 +93,8 @@ const AccessItem: React.FC<AccessItemProps> = ({ onSelect, data }) => {
<AutoTooltip ghost> <AutoTooltip ghost>
<StatusTag <StatusTag
statusValue={{ statusValue={{
status: 'success', status: EndpointStatus[data.state],
text: 'Ready', text: EndpointStatusLabelMap[data.state],
message: '' message: ''
}} }}
/> />
@@ -100,7 +104,7 @@ const AccessItem: React.FC<AccessItemProps> = ({ onSelect, data }) => {
<Col span={5}> <Col span={5}>
<CellContent style={{ marginLeft: -6 }}> <CellContent style={{ marginLeft: -6 }}>
<AutoTooltip ghost minWidth={20}> <AutoTooltip ghost minWidth={20}>
{dayjs().format('YYYY-MM-DD HH:mm:ss')} {dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')}
</AutoTooltip> </AutoTooltip>
</CellContent> </CellContent>
</Col> </Col>
@@ -112,7 +116,7 @@ const AccessItem: React.FC<AccessItemProps> = ({ onSelect, data }) => {
> >
<DropdownButtons <DropdownButtons
items={childActionList} items={childActionList}
onSelect={onSelect} onSelect={(val) => onSelect(val, data)}
></DropdownButtons> ></DropdownButtons>
</CellContent> </CellContent>
</Col> </Col>
@@ -126,7 +130,7 @@ const AccessPoints: React.FC<ProviderModelProps> = ({ dataList, onSelect }) => {
console.log('AccessPoints dataList:', dataList); console.log('AccessPoints dataList:', dataList);
return ( return (
<div> <div>
{mockAccessPointList.map((item, index) => ( {dataList.map((item, index) => (
<AccessItem data={item} key={index} onSelect={onSelect}></AccessItem> <AccessItem data={item} key={index} onSelect={onSelect}></AccessItem>
))} ))}
</div> </div>
+9 -9
View File
@@ -2,19 +2,19 @@ import icons from '@/components/icon-font/icons';
import { StatusMaps } from '@/config'; import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types'; import { StatusType } from '@/config/types';
export const ProviderStatusValueMap: Record<string, string> = { export const EndpointsStatusValueMap: Record<string, string> = {
Ready: 'Ready', Active: 'active',
InActive: 'Inactive' Inactive: 'inactive'
}; };
export const ProviderStatusLabelMap = { export const EndpointStatusLabelMap = {
[ProviderStatusValueMap.Ready]: 'Ready', [EndpointsStatusValueMap.Active]: 'Active',
[ProviderStatusValueMap.InActive]: 'Inactive' [EndpointsStatusValueMap.Inactive]: 'Inactive'
}; };
export const ProviderStatus: Record<string, StatusType> = { export const EndpointStatus: Record<string, StatusType> = {
[ProviderStatusValueMap.Ready]: StatusMaps.success, [EndpointsStatusValueMap.Active]: StatusMaps.success,
[ProviderStatusValueMap.InActive]: StatusMaps.error [EndpointsStatusValueMap.Inactive]: StatusMaps.error
}; };
// actions for each row // actions for each row
-54
View File
@@ -1,54 +0,0 @@
import { AccessItem, AccessPointItem } from './types';
// mock data for AccessItem 5 items
export const mockDataList: AccessItem[] = [
{
id: 1,
name: 'model-A',
source: 'Deployments',
accessPoints: 5
},
{
id: 2,
name: 'model-B',
source: 'Qwen',
accessPoints: 3
},
{
id: 3,
name: 'model-C',
source: 'OpenAI',
accessPoints: 8
},
{
id: 4,
name: 'model-D',
source: 'Deepseek',
accessPoints: 2
},
{
id: 5,
name: 'model-E',
source: 'Anthropic',
accessPoints: 4
}
];
// mock data for AccessPointItem 2 items
export const mockAccessPointList: AccessPointItem[] = [
{
id: 1,
name: 'Access Point 1',
type: 'API',
is_fallback: true,
endpoint: 'https://api.example.com/endpoint1',
created_at: '2024-01-01T12:00:00Z'
},
{
id: 2,
name: 'Access Point 2',
type: 'SDK',
is_fallback: false,
created_at: '2024-02-01T12:00:00Z'
}
];
+24 -15
View File
@@ -1,3 +1,23 @@
export interface FormData {
name: string;
description: string;
categories: any[];
meta: Record<string, any>;
fallback_endpoint: {
provider_model_name?: string;
model_id?: number;
provider_id?: number;
fallback_status_codes?: string[];
};
endpoints: {
provider_model_name?: string;
weight?: number | null;
model_id?: number;
provider_id?: number;
fallback_status_codes?: string[];
}[];
}
export interface AccessItem { export interface AccessItem {
id: number; id: number;
created_at: string; created_at: string;
@@ -8,20 +28,9 @@ export interface AccessItem {
categories: string[]; categories: string[];
meta: Record<string, any>; meta: Record<string, any>;
created_by_model: boolean; created_by_model: boolean;
endpoint_count: number; endpoints: number;
} ready_endpoints: number;
access_policy: string;
export interface FormData {
name: string;
description: string;
categories: any[];
meta: Record<string, any>;
endpoints: {
provider_model_name: string;
weight: number;
model_id: number;
provider_id: number;
}[];
} }
export interface AccessPointItem { export interface AccessPointItem {
@@ -30,7 +39,7 @@ export interface AccessPointItem {
updated_at: string; updated_at: string;
deleted_at: string; deleted_at: string;
provider_model_name: string; provider_model_name: string;
weight: number; weight: number | null;
model_id: number; model_id: number;
provider_id: number; provider_id: number;
name: string; name: string;
+11 -3
View File
@@ -1,6 +1,7 @@
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { modelCategories } from '@/pages/llmodels/config'; import useAppUtils from '@/hooks/use-app-utils';
import { categoryOptions } from '@/pages/llmodels/config';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
@@ -8,6 +9,7 @@ import { FormData } from '../config/types';
const Basic = () => { const Basic = () => {
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const { getRuleMessage } = useAppUtils();
return ( return (
<> <>
<Form.Item name="name" data-field="name"> <Form.Item name="name" data-field="name">
@@ -22,11 +24,17 @@ const Basic = () => {
getValueProps={(value) => ({ getValueProps={(value) => ({
value: Array.isArray(value) ? value[0] || null : value value: Array.isArray(value) ? value[0] || null : value
})} })}
rules={[
{
required: true,
message: getRuleMessage('select', 'models.form.categories')
}
]}
> >
<SealSelect <SealSelect
options={modelCategories} required
options={categoryOptions}
label={intl.formatMessage({ id: 'models.form.categories' })} label={intl.formatMessage({ id: 'models.form.categories' })}
allowNull
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
<Form.Item name="description"> <Form.Item name="description">
+167 -161
View File
@@ -1,81 +1,72 @@
import { LabelSelectorContext } from '@/components/label-selector/context';
import MetadataList from '@/components/metadata-list'; import MetadataList from '@/components/metadata-list';
import SealCascader from '@/components/seal-form/seal-cascader'; import SealCascader from '@/components/seal-form/seal-cascader';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { useState } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import useEndpointSourceModels from '../hooks/use-endpoint-source-models';
const providerModelList = [ const EndpointsForm = forwardRef((props, ref) => {
{
label: 'Deployments',
value: 'deployments',
children: [
{
label: 'qwen3-0.6b',
value: 'qwen3-0.6b',
key: '1',
provider_id: 1,
model_id: 101
},
{
label: 'deepseek',
value: 'deepseek',
key: '2',
provider_id: 1,
model_id: 102
}
]
},
{
label: 'Doubao',
value: 'doubao',
children: [
{
label: 'qwen3-0.6b',
value: 'qwen3-0.6b',
key: '3',
provider_id: 2,
model_id: 201
},
{
label: 'deepseek',
value: 'deepseek',
key: '4',
provider_id: 2,
model_id: 202
}
]
}
];
const Endpoints = () => {
const intl = useIntl(); const intl = useIntl();
const { sourceModels, loading, fetchSourceModels } =
useEndpointSourceModels();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const endpoints = Form.useWatch('endpoints', form); const endpoints = Form.useWatch('endpoints', form) || [];
const [fallbackValues, setFallbackValues] = useState<{ value: any[] }>({
value: []
});
const [dataList, setDataList] = useState< const [dataList, setDataList] = useState<
{ {
provider_model_name: string; weight: number | null;
weight: number; value: any[];
model_id: number;
provider_id: number;
}[] }[]
>(endpoints || []); >([]);
const handleEndpointsChange = (labels: Record<string, any>) => { useImperativeHandle(ref, () => ({
form.setFieldValue('endpoints', labels); initFallbackValues: (values: { value: any[] }) => {
setFallbackValues(values);
},
initDataList: (
list: {
weight: number | null;
value: any[];
}[]
) => {
setDataList(list);
}
}));
const handleEndpointsChange = (
value: any[],
index: number,
options: any[]
) => {
const selectedOption =
options?.find?.((opt) => opt.value === value[1]) || {};
const endpointList = [...endpoints];
endpointList[index] = {
weight: endpointList[index]?.weight || null,
...selectedOption?.data
};
form.setFieldValue('endpoints', [...endpointList]);
const newDataList = [...dataList];
newDataList[index] = {
weight: newDataList[index]?.weight || null,
value: value
};
setDataList(newDataList);
}; };
const handleOnAdd = () => { const handleOnAdd = () => {
const newDataList = [ const newDataList = [
...dataList, ...dataList,
{ {
provider_model_name: '', weight: null,
weight: 1, value: []
model_id: 0,
provider_id: 0
} }
]; ];
setDataList(newDataList); setDataList(newDataList);
@@ -84,115 +75,128 @@ const Endpoints = () => {
const handleOnDelete = (index: number, item: any) => { const handleOnDelete = (index: number, item: any) => {
const newDataList = dataList.filter((_, i) => i !== index); const newDataList = dataList.filter((_, i) => i !== index);
setDataList(newDataList); setDataList(newDataList);
const endpointList = [...endpoints];
endpointList.splice(index, 1);
form.setFieldValue('endpoints', [...endpointList]);
}; };
const handleFallbackChange = (e: any, index: number) => { const handleFallbackChange = (value: any[], options?: any[]) => {
const checked = e.target.checked; const selectedOption =
const newDataList = dataList.map((item, i) => { options?.find?.((opt) => opt.value === value[1]) || {};
if (i === index) {
return { console.log('fallback selected option data:', value);
...item, form.setFieldValue('fallback_endpoint', {
is_fallback: checked ...selectedOption?.data
};
} else if (checked) {
return {
...item,
is_fallback: false
};
}
return item;
}); });
setFallbackValues({
value: value
});
};
const handleOnWeightChange = (value: any, index: number) => {
const endpointList = [...endpoints];
if (endpointList[index]) {
endpointList[index] = {
...endpointList[index],
weight: value
};
form.setFieldValue('endpoints', [...endpointList]);
}
const newDataList = [...dataList];
newDataList[index] = {
...newDataList[index],
weight: value
};
setDataList(newDataList); setDataList(newDataList);
}; };
useEffect(() => {
fetchSourceModels();
}, []);
return ( return (
<> <>
<LabelSelectorContext.Provider <Form.Item
value={{ name="endpoints"
placeholder: ['Model', 'Weight'], data-field="endpoints"
options: [ rules={[
{ ({ getFieldValue }) => ({
label: 'max_token_len', validator(rule, value) {
value: 'max_token_len' if (_.keys(value).length > 0) {
}, if (_.some(_.keys(value), (k: string) => !value[k])) {
{ return Promise.reject(
label: 'max_context_len', intl.formatMessage(
value: 'max_context_len' {
} id: 'common.validate.value'
] },
}} {
> name: intl.formatMessage({
<Form.Item id: 'models.form.selector'
name="endpoints" })
data-field="endpoints" }
rules={[ )
({ getFieldValue }) => ({ );
validator(rule, value) {
if (_.keys(value).length > 0) {
if (_.some(_.keys(value), (k: string) => !value[k])) {
return Promise.reject(
intl.formatMessage(
{
id: 'common.validate.value'
},
{
name: intl.formatMessage({
id: 'models.form.selector'
})
}
)
);
}
} }
return Promise.resolve();
} }
}) return Promise.resolve();
]} }
})
]}
>
<MetadataList
label={''}
styles={{
wrapper: {
paddingTop: 14
}
}}
dataList={dataList}
btnText={intl.formatMessage({ id: 'accesses.form.endpoint.add' })}
onAdd={handleOnAdd}
onDelete={handleOnDelete}
> >
<MetadataList {(item, index) => (
label={''} <>
styles={{ <SealCascader
wrapper: { required
paddingTop: 14 showSearch
} expandTrigger="hover"
}} multiple={false}
dataList={dataList} onChange={(value, options) =>
btnText={intl.formatMessage({ id: 'accesses.form.endpoint.add' })} handleEndpointsChange(value, index, options)
onAdd={handleOnAdd} }
onDelete={handleOnDelete} classNames={{
> popup: {
{(item, index) => ( root: 'cascader-popup-wrapper gpu-selector'
<> }
<SealCascader }}
required maxTagCount={1}
showSearch placeholder={intl.formatMessage({
expandTrigger="hover" id: 'accesses.form.endpoint.model'
multiple={false} })}
classNames={{ value={item.value}
popup: { options={sourceModels}
root: 'cascader-popup-wrapper gpu-selector' showCheckedStrategy="SHOW_CHILD"
} getPopupContainer={(triggerNode) => triggerNode.parentNode}
}} ></SealCascader>
maxTagCount={1} <span className="seprator">:</span>
placeholder={intl.formatMessage({ <SealInput.Number
id: 'accesses.form.endpoint.model' style={{ flex: 100 }}
})} min={0}
options={providerModelList} value={item.weight}
showCheckedStrategy="SHOW_CHILD" onChange={(value) => handleOnWeightChange(value, index)}
getPopupContainer={(triggerNode) => triggerNode.parentNode} placeholder={intl.formatMessage({
></SealCascader> id: 'accesses.form.endpoint.weight'
<span className="seprator">:</span> })}
<SealInput.Number ></SealInput.Number>
style={{ flex: 100 }} </>
placeholder={intl.formatMessage({ )}
id: 'accesses.form.endpoint.weight' </MetadataList>
})} </Form.Item>
></SealInput.Number> <Form.Item name="fallback_endpoint">
</> <div>
)}
</MetadataList>
</Form.Item>
<Form.Item name="fallback_endpoint">
<SealCascader <SealCascader
showSearch showSearch
expandTrigger="hover" expandTrigger="hover"
@@ -206,14 +210,16 @@ const Endpoints = () => {
id: 'accesses.form.endpoint.fallback' id: 'accesses.form.endpoint.fallback'
})} })}
maxTagCount={1} maxTagCount={1}
options={providerModelList} value={fallbackValues.value}
options={sourceModels}
onChange={(value, options) => handleFallbackChange(value, options)}
showCheckedStrategy="SHOW_CHILD" showCheckedStrategy="SHOW_CHILD"
getPopupContainer={(triggerNode) => triggerNode.parentNode} getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader> ></SealCascader>
</Form.Item> </div>
</LabelSelectorContext.Provider> </Form.Item>
</> </>
); );
}; });
export default Endpoints; export default EndpointsForm;
+109 -9
View File
@@ -1,12 +1,21 @@
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import CollapsePanel from '@/pages/_components/collapse-panel'; import CollapsePanel from '@/pages/_components/collapse-panel';
import { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context'; import { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context';
import ScrollSpyTabs from '@/pages/_components/scroll-spy-tabs'; import ScrollSpyTabs from '@/pages/_components/scroll-spy-tabs';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import _ from 'lodash';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react';
import { FormData, AccessItem as ListItem } from '../config/types'; import { FormData, AccessItem as ListItem } from '../config/types';
import useEditEndpoints from '../hooks/use-edit-endpoints';
import Basic from './basic'; import Basic from './basic';
import Endpoints from './endpoints'; import Endpoints from './endpoints';
import MetaData from './meta-data'; import MetaData from './meta-data';
@@ -32,7 +41,8 @@ const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const [activeKey, setActiveKey] = useState<string[]>([TABKeysMap.BASIC]); const [activeKey, setActiveKey] = useState<string[]>([TABKeysMap.BASIC]);
const [form] = Form.useForm(); const [form] = Form.useForm();
const scrollTabsRef = useRef<any>(null); const scrollTabsRef = useRef<any>(null);
const endpointsRef = useRef<any>(null);
const { generateEndpointData, fetchEndpoints } = useEditEndpoints();
const segmentOptions = [ const segmentOptions = [
{ {
value: TABKeysMap.BASIC, value: TABKeysMap.BASIC,
@@ -58,10 +68,104 @@ const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
setActiveKey(key); setActiveKey(key);
}; };
const formatEndpoints = (values: FormData) => {
console.log('formatEndpoints values:', values);
let endPoints = [...values.endpoints];
let fallbackEndpoint = values.fallback_endpoint;
if (fallbackEndpoint && endPoints.length > 0) {
endPoints = endPoints?.map((ep) => {
if (ep.model_id === fallbackEndpoint.model_id && ep.model_id) {
return {
...ep,
fallback_status_codes: ['4xx', '5xx']
};
}
if (
ep.provider_id === fallbackEndpoint.provider_id &&
ep.provider_model_name === fallbackEndpoint.provider_model_name &&
!fallbackEndpoint.model_id
) {
return {
...ep,
fallback_status_codes: ['4xx', '5xx']
};
}
return ep;
});
} else if (fallbackEndpoint) {
endPoints.push({
...fallbackEndpoint,
weight: null,
fallback_status_codes: ['4xx', '5xx']
});
}
return endPoints;
};
const handleOnFinish = (values: FormData) => {
const endpoints = formatEndpoints(values);
const data = {
..._.omit(values, ['endpoints', 'fallback_endpoint']),
endpoints: endpoints
};
console.log('data=========', data);
onFinish(data);
};
const handleOnCollapseChange = (keys: string | string[]) => { const handleOnCollapseChange = (keys: string | string[]) => {
setActiveKey(Array.isArray(keys) ? keys : [keys]); setActiveKey(Array.isArray(keys) ? keys : [keys]);
}; };
useEffect(() => {
const initEditionForm = async () => {
const endpointList = await fetchEndpoints(currentData!.id);
const { endpoints, fallbackEndpoint } =
generateEndpointData(endpointList);
console.log(
'endpoints:',
endpoints,
'fallbackEndpoint:',
fallbackEndpoint
);
// init form values
form.setFieldsValue({
...currentData,
endpoints: endpoints,
fallback_endpoint: fallbackEndpoint
});
// init endpoints form list
endpointsRef.current?.initDataList(
endpoints?.map((ep) => ({
weight: ep.weight,
value: ep.model_id
? ['deployments', ep.model_id]
: [ep.provider_id, ep.provider_model_name]
})) || []
);
// init fallback value
if (fallbackEndpoint) {
endpointsRef.current?.initFallbackValues({
value: fallbackEndpoint.model_id
? ['deployments', fallbackEndpoint.model_id]
: [
fallbackEndpoint.provider_id,
fallbackEndpoint.provider_model_name
]
});
}
};
if (action === PageAction.EDIT && currentData) {
initEditionForm();
} else {
form.resetFields();
}
}, [action, currentData, form]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submit: () => { submit: () => {
form.submit(); form.submit();
@@ -86,14 +190,10 @@ const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
> >
<Form <Form
form={form} form={form}
onFinish={onFinish} onFinish={handleOnFinish}
initialValues={{ initialValues={{
categories: [], categories: [],
meta: { meta: {}
size: null,
activated_size: null,
dimensions: null
}
}} }}
> >
<Basic /> <Basic />
@@ -112,7 +212,7 @@ const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
key: TABKeysMap.ENDPOINTS, key: TABKeysMap.ENDPOINTS,
label: intl.formatMessage({ id: 'accesses.form.endpoint.title' }), label: intl.formatMessage({ id: 'accesses.form.endpoint.title' }),
forceRender: true, forceRender: true,
children: <Endpoints></Endpoints> children: <Endpoints ref={endpointsRef}></Endpoints>
} }
]} ]}
></CollapsePanel> ></CollapsePanel>
@@ -3,6 +3,7 @@ import AutoTooltip from '@/components/auto-tooltip';
import DropdownButtons from '@/components/drop-down-buttons'; import DropdownButtons from '@/components/drop-down-buttons';
import { SealColumnProps } from '@/components/seal-table/types'; import { SealColumnProps } from '@/components/seal-table/types';
import { tableSorter } from '@/config/settings'; import { tableSorter } from '@/config/settings';
import ModelTag from '@/pages/_components/model-tag';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -23,18 +24,23 @@ const useAccessColumns = (
sorter: tableSorter(1), sorter: tableSorter(1),
span: 5, span: 5,
render: (text: string, record: AccessItem) => ( render: (text: string, record: AccessItem) => (
<> <span className="flex-center" style={{ maxWidth: '100%' }}>
<AutoTooltip ghost title={text}> <AutoTooltip ghost title={text}>
{text} <span className="m-r-5">{text}</span>
</AutoTooltip> </AutoTooltip>
</> <ModelTag categoryKey={record.categories?.[0]}></ModelTag>
</span>
) )
}, },
{ {
title: intl.formatMessage({ id: 'accesses.table.accessPoints' }), title: intl.formatMessage({ id: 'accesses.table.accessPoints' }),
dataIndex: 'accessPoints', dataIndex: 'endpoints',
span: 10, span: 10,
render: (value: number) => <span>{value}</span> render: (value: number, record: AccessItem) => (
<span>
{record.ready_endpoints} / {value}
</span>
)
}, },
{ {
title: intl.formatMessage({ id: 'common.table.createTime' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
@@ -0,0 +1,37 @@
import { queryAccessPoints } from '../apis';
import { AccessPointItem } from '../config/types';
const useEditEndpoints = () => {
const generateEndpointData = (endpoints: AccessPointItem[]) => {
const fallbackEndpoint =
endpoints?.filter(
(ep) => ep.fallback_status_codes && ep.fallback_status_codes?.length > 0
)?.[0] || null;
const endPoints = endpoints?.filter(
(ep) =>
!ep.fallback_status_codes || ep.fallback_status_codes?.length === 0
);
return {
endpoints: endPoints,
fallbackEndpoint: fallbackEndpoint
};
};
const fetchEndpoints = async (accessId: number) => {
try {
const res = await queryAccessPoints({ id: accessId });
return res.items || [];
} catch (error) {
return [];
}
};
return {
generateEndpointData,
fetchEndpoints
};
};
export default useEditEndpoints;
@@ -0,0 +1,77 @@
import { queryModelsList } from '@/pages/llmodels/apis';
import { ListItem as ModelListItem } from '@/pages/llmodels/config/types';
import { queryMaasProviders } from '@/pages/maas-provider/apis';
import { MaasProviderItem } from '@/pages/maas-provider/config/types';
import { useState } from 'react';
type EmptyObject = Record<never, never>;
type CascaderOption<T extends object = EmptyObject> = {
label: string;
value: string | number;
parent?: boolean;
disabled?: boolean;
index?: number;
children?: CascaderOption<T>[];
} & Partial<T>;
const useEndpointSourceModels = () => {
const [sourceModels, setSourceModels] = useState<CascaderOption[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const fetchSourceModels = async (params?: any) => {
setLoading(true);
try {
const [models, providers] = await Promise.all([
queryModelsList({ page: -1, ...params }),
queryMaasProviders({ page: -1, ...params })
]);
const modelsList = [
{
label: 'Deployments',
value: 'deployments',
parent: true,
children: models.items?.map?.((model: ModelListItem) => ({
label: model.name,
value: model.id,
data: {
model_id: model.id
},
source: 'deployment'
}))
}
].filter((group) => group.children && group.children.length > 0);
const providerOptions: CascaderOption[] = providers.items
?.map?.((provider: MaasProviderItem) => ({
label: provider.name,
value: provider.id,
parent: true,
children: provider.models?.map?.((model) => ({
label: model.name,
value: model.name,
data: {
provider_model_name: model.name,
provider_id: provider.id
},
source: 'providerModel'
}))
}))
.filter((group) => group.children && group.children.length > 0);
setSourceModels([...modelsList, ...providerOptions]);
} catch (error) {
setSourceModels([]);
} finally {
setLoading(false);
}
};
return {
sourceModels,
loading,
fetchSourceModels
};
};
export default useEndpointSourceModels;
+48 -26
View File
@@ -21,13 +21,16 @@ import AccessControlModal from '../llmodels/components/access-control-modal';
import { import {
ACCESS_API, ACCESS_API,
ACCESS_POINTS_API, ACCESS_POINTS_API,
createAccess,
deleteAccess, deleteAccess,
deleteAccessPoint,
queryAccessPoints,
queryModelAccesses, queryModelAccesses,
setAccessPointAsFallback,
updateAccess updateAccess
} from './apis'; } from './apis';
import AccessPoints from './components/access-points'; import AccessPoints from './components/access-points';
import AddAccessModal from './components/add-access-modal'; import AddAccessModal from './components/add-access-modal';
import { mockDataList } from './config/mock';
import { FormData, AccessItem as ListItem } from './config/types'; import { FormData, AccessItem as ListItem } from './config/types';
import useAccessColumns from './hooks/use-access-columns'; import useAccessColumns from './hooks/use-access-columns';
import useAccessControl from './hooks/use-access-control'; import useAccessControl from './hooks/use-access-control';
@@ -49,11 +52,12 @@ const Accesses: React.FC = () => {
} = useTableFetch<ListItem>({ } = useTableFetch<ListItem>({
fetchAPI: queryModelAccesses, fetchAPI: queryModelAccesses,
deleteAPI: deleteAccess, deleteAPI: deleteAccess,
watch: false, watch: true,
API: ACCESS_API, API: ACCESS_API,
contentForDelete: 'menu.models.access' contentForDelete: 'menu.models.access'
}); });
const { watchDataList: allAccessPoints } = useWatchList(ACCESS_POINTS_API); const { watchDataList: allAccessPoints, deleteItemFromCache } =
useWatchList(ACCESS_POINTS_API);
const [expandAtom] = useAtom(expandKeysAtom); const [expandAtom] = useAtom(expandKeysAtom);
const { handleExpandChange, handleExpandAll, expandedRowKeys } = const { handleExpandChange, handleExpandAll, expandedRowKeys } =
useExpandedRowKeys(expandAtom); useExpandedRowKeys(expandAtom);
@@ -84,6 +88,11 @@ const Accesses: React.FC = () => {
id: openAccessModalStatus.currentData!.id id: openAccessModalStatus.currentData!.id
}); });
} }
if (openAccessModalStatus.action === PageAction.CREATE) {
await createAccess({
data: params
});
}
fetchData(); fetchData();
closeAccessModal(); closeAccessModal();
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
@@ -98,10 +107,7 @@ const Accesses: React.FC = () => {
const handleEditProvider = (row: ListItem) => { const handleEditProvider = (row: ListItem) => {
openAccessModal( openAccessModal(
PageAction.EDIT, PageAction.EDIT,
intl.formatMessage( intl.formatMessage({ id: 'common.button.edit.item' }, { name: row.name }),
{ id: 'clusters.edit.cluster' },
{ cluster: row.name }
),
row row
); );
}; };
@@ -135,13 +141,13 @@ const Accesses: React.FC = () => {
const loadChildrenData = useMemoizedFn( const loadChildrenData = useMemoizedFn(
async (row: ListItem, options?: any) => { async (row: ListItem, options?: any) => {
const params = { const params = {
cluster_id: row.id, id: row.id
page: -1
}; };
// const data = await queryAccessPoints(params, { const res = await queryAccessPoints(params, {
// token: options?.token token: options?.token
// }); });
return [1];
return res.items || [];
} }
); );
@@ -149,24 +155,40 @@ const Accesses: React.FC = () => {
handleTableChange({}, {}, order, { action: 'sort' }); handleTableChange({}, {}, order, { action: 'sort' });
}; };
const onChildSelect = useMemoizedFn((val: any, record: any) => { const handleDeleteEndpoint = (row: any) => {
if (val === 'fallback') { modalRef.current?.show({
console.log('open fallback settings modal', record); content: 'accesses.table.accessPoints',
} okText: 'common.button.delete',
operation: 'common.delete.single.confirm',
name: row.name,
async onOk() {
await deleteAccessPoint(row.id);
deleteItemFromCache?.(row.id);
}
});
};
const onChildSelect = useMemoizedFn(async (val: any, record: any) => {
try {
if (val === 'fallback') {
await setAccessPointAsFallback({
id: record.id,
data: {
fallback_status_codes: ['4xx', '5xx']
}
});
message.success(intl.formatMessage({ id: 'common.message.success' }));
} else if (val === 'delete') {
handleDeleteEndpoint(record);
}
} catch (error) {}
}); });
const renderChildren = ( const renderChildren = (
list: any, list: any,
options: { parent?: any; [key: string]: any } options: { parent?: any; [key: string]: any }
) => { ) => {
return ( return <AccessPoints dataList={list} onSelect={onChildSelect} />;
<AccessPoints
dataList={list}
provider={options.parent?.provider}
providerId={options.parent?.id}
onSelect={onChildSelect}
/>
);
}; };
const columns = useAccessColumns(handleSelect); const columns = useAccessColumns(handleSelect);
@@ -201,7 +223,7 @@ const Accesses: React.FC = () => {
renderChildren={renderChildren} renderChildren={renderChildren}
onTableSort={handleOnSortChange} onTableSort={handleOnSortChange}
showSorterTooltip={false} showSorterTooltip={false}
dataSource={mockDataList} dataSource={dataSource.dataList}
loading={dataSource.loading} loading={dataSource.loading}
loadend={dataSource.loadend} loadend={dataSource.loadend}
rowSelection={rowSelection} rowSelection={rowSelection}