diff --git a/src/hooks/use-watch-list.ts b/src/hooks/use-watch-list.ts index dc62e8c3..2ff3b2e6 100644 --- a/src/hooks/use-watch-list.ts +++ b/src/hooks/use-watch-list.ts @@ -51,6 +51,13 @@ export default function useWatchList>(API: string) { }); }; + const handleDeleteItemFromCache = (id: number) => { + cacheWatchDataListRef.current = cacheWatchDataListRef.current.filter( + (item) => item.id !== id + ); + setWatchDataList(cacheWatchDataListRef.current); + }; + const getAllDataList = useMemoizedFn(async () => { try { listRequestTokenRef.current?.cancel?.(); @@ -78,6 +85,7 @@ export default function useWatchList>(API: string) { }, []); return { - watchDataList + watchDataList, + deleteItemFromCache: handleDeleteItemFromCache }; } diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 721321fc..e14ebc2a 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -259,6 +259,7 @@ export default { 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.', 'common.button.edit.item': 'Edit {name}', + 'common.button.copy.item': 'Duplicate {name}', 'common.button.terminal': 'Terminal', 'common.button.addItem': 'Add Item', 'common.help.default': 'Default: {content}', @@ -273,5 +274,6 @@ export default { 'common.button.enable': 'Enable', 'common.button.disable': 'Disable', 'common.status.enabled': 'Enabled', - 'common.status.disabled': 'Disabled' + 'common.status.disabled': 'Disabled', + 'common.button.duplicate': 'Duplicate' }; diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index bedef316..c0f40281 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -259,6 +259,7 @@ export default { 'common.sso.noConfig': 'Single sign-on is not enabled on this system. Please contact your administrator.', 'common.button.edit.item': 'Edit {name}', + 'common.button.copy.item': 'Duplicate {name}', 'common.button.terminal': 'Terminal', 'common.button.addItem': 'Add Item', 'common.help.default': 'Default: {content}', @@ -273,7 +274,8 @@ export default { 'common.button.enable': 'Enable', 'common.button.disable': 'Disable', 'common.status.enabled': 'Enabled', - 'common.status.disabled': 'Disabled' + 'common.status.disabled': 'Disabled', + 'common.button.duplicate': 'Duplicate' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -313,4 +315,5 @@ export default { // 34. 'common.button.disable': 'Disable', // 35. 'common.status.enabled': 'Enabled', // 36. 'common.status.disabled': 'Disabled' +// 33. 'common.button.duplicate': 'Duplicate' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index 08c81c0c..4e575eba 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -258,6 +258,7 @@ export default { 'common.sso.noConfig': 'Единый вход не настроен в этой системе. Пожалуйста, обратитесь к администратору.', 'common.button.edit.item': 'Редактировать {name}', + 'common.button.copy.item': 'Duplicate {name}', 'common.button.terminal': 'Терминал', 'common.button.addItem': 'Добавить элемент', 'common.help.default': 'По умолчанию: {content}', @@ -272,12 +273,11 @@ export default { 'common.button.enable': 'Enable', 'common.button.disable': 'Disable', 'common.status.enabled': 'Enabled', - 'common.status.disabled': 'Disabled' + 'common.status.disabled': 'Disabled', + 'common.button.duplicate': 'Duplicate' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== -// 1. 'common.button.enable': 'Enable', -// 2. 'common.button.disable': 'Disable', -// 3. 'common.status.enabled': 'Enabled', -// 4. 'common.status.disabled': 'Disabled' +// 1. 'common.button.copy.item': 'Duplicate {name}', +// 2. 'common.button.duplicate': 'Duplicate' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index bea519a7..902cb84e 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -251,6 +251,7 @@ export default { 'common.external.login': '使用 {type} 登录', 'common.sso.noConfig': '该系统未启用单点登录,请联系管理员。', 'common.button.edit.item': '编辑 {name}', + 'common.button.copy.item': '复制 {name}', 'common.button.terminal': '终端', 'common.button.addItem': '添加项', 'common.help.default': '默认: {content}', @@ -265,5 +266,6 @@ export default { 'common.button.enable': '启用', 'common.button.disable': '禁用', 'common.status.enabled': '已启用', - 'common.status.disabled': '未启用' + 'common.status.disabled': '未启用', + 'common.button.duplicate': '复制' }; diff --git a/src/pages/llmodels/config/index.ts b/src/pages/llmodels/config/index.ts index 72262e47..23198bd9 100644 --- a/src/pages/llmodels/config/index.ts +++ b/src/pages/llmodels/config/index.ts @@ -1,5 +1,12 @@ +import IconFont from '@/components/icon-font'; 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'; export const backendTipsList = [ @@ -290,12 +297,45 @@ export const modelCategoriesMap = { }; export const categoryOptions = [ - { label: 'LLM', value: modelCategoriesMap.llm }, - { label: 'Embedding', value: modelCategoriesMap.embedding }, - { label: 'Reranker', value: modelCategoriesMap.reranker }, - { label: 'Image', value: modelCategoriesMap.image }, - { label: 'Text-to-Speech', value: modelCategoriesMap.text_to_speech }, - { label: 'Speech-to-Text', value: modelCategoriesMap.speech_to_text } + { + label: 'LLM', + value: modelCategoriesMap.llm, + icon: React.createElement(WechatWorkOutlined, { style: { color: 'green' } }) + }, + { + 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 = [ diff --git a/src/pages/maas-provider/config/index.ts b/src/pages/maas-provider/config/index.ts index 357a7484..593a9cdc 100644 --- a/src/pages/maas-provider/config/index.ts +++ b/src/pages/maas-provider/config/index.ts @@ -32,7 +32,7 @@ export const rowActionList = [ }, { key: 'copy', - label: 'common.button.copy', + label: 'common.button.duplicate', icon: icons.CopyOutlined }, { diff --git a/src/pages/model-access/apis/index.ts b/src/pages/model-access/apis/index.ts index d0b8c4ee..09080aa7 100644 --- a/src/pages/model-access/apis/index.ts +++ b/src/pages/model-access/apis/index.ts @@ -1,5 +1,5 @@ import { request } from '@umijs/max'; -import { AccessItem, FormData } from '../config/types'; +import { AccessItem, AccessPointItem, FormData } from '../config/types'; 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) { - return request[]>( - `${ACCESS_API}/${params.id}/access`, +export async function queryAccessPoints(params: { id: number }, options?: any) { + return request>( + `${ACCESS_POINTS_API}?access_id=${params.id}`, { method: 'GET', + params: { + page: -1 + }, 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; +}) { + return request(`${ACCESS_POINTS_API}/${params.id}`, { + method: 'PUT', + data: params.data + }); +} + +export async function setAccessPointAsFallback(params: { + id: number; + data: Partial; +}) { + return request(`${ACCESS_POINTS_API}/${params.id}/set-fallback`, { + method: 'POST', + data: params.data + }); +} diff --git a/src/pages/model-access/components/access-points.tsx b/src/pages/model-access/components/access-points.tsx index 80b50819..b1b889ba 100644 --- a/src/pages/model-access/components/access-points.tsx +++ b/src/pages/model-access/components/access-points.tsx @@ -9,7 +9,8 @@ import { Col, Row } from 'antd'; import dayjs from 'dayjs'; import React from 'react'; import styled from 'styled-components'; -import { mockAccessPointList } from '../config/mock'; +import { EndpointStatus, EndpointStatusLabelMap } from '../config'; +import { AccessPointItem } from '../config/types'; const CellContent = styled.div` display: flex; align-items: center; @@ -17,9 +18,7 @@ const CellContent = styled.div` `; interface ProviderModelProps { - dataList: any[]; - provider: string; - providerId: number; + dataList: AccessPointItem[]; onSelect: (val: any, record: any) => void; } @@ -61,27 +60,32 @@ const AccessItem: React.FC = ({ onSelect, data }) => { paddingInline: 'var(--ant-table-cell-padding-inline)' }} > - qwen3-0.6b-zduxy + {data.provider_model_name} - OpenAI + - {data.weight && ( - {intl.formatMessage({ id: 'accesses.form.endpoint.weight' })}: - 20 / + {intl.formatMessage({ id: 'accesses.form.endpoint.weight' })}:{' '} + {data.weight} )} - {data.is_fallback && ( - - {intl.formatMessage({ - id: 'accesses.table.label.fallback' - })} - - )} + + {data.fallback_status_codes && + data.fallback_status_codes?.length > 0 && ( + <> + / + + {intl.formatMessage({ + id: 'accesses.table.label.fallback' + })} + + + )} @@ -89,8 +93,8 @@ const AccessItem: React.FC = ({ onSelect, data }) => { @@ -100,7 +104,7 @@ const AccessItem: React.FC = ({ onSelect, data }) => { - {dayjs().format('YYYY-MM-DD HH:mm:ss')} + {dayjs(data.created_at).format('YYYY-MM-DD HH:mm:ss')} @@ -112,7 +116,7 @@ const AccessItem: React.FC = ({ onSelect, data }) => { > onSelect(val, data)} > @@ -126,7 +130,7 @@ const AccessPoints: React.FC = ({ dataList, onSelect }) => { console.log('AccessPoints dataList:', dataList); return (
- {mockAccessPointList.map((item, index) => ( + {dataList.map((item, index) => ( ))}
diff --git a/src/pages/model-access/config/index.ts b/src/pages/model-access/config/index.ts index 24340c8a..6c202314 100644 --- a/src/pages/model-access/config/index.ts +++ b/src/pages/model-access/config/index.ts @@ -2,19 +2,19 @@ import icons from '@/components/icon-font/icons'; import { StatusMaps } from '@/config'; import { StatusType } from '@/config/types'; -export const ProviderStatusValueMap: Record = { - Ready: 'Ready', - InActive: 'Inactive' +export const EndpointsStatusValueMap: Record = { + Active: 'active', + Inactive: 'inactive' }; -export const ProviderStatusLabelMap = { - [ProviderStatusValueMap.Ready]: 'Ready', - [ProviderStatusValueMap.InActive]: 'Inactive' +export const EndpointStatusLabelMap = { + [EndpointsStatusValueMap.Active]: 'Active', + [EndpointsStatusValueMap.Inactive]: 'Inactive' }; -export const ProviderStatus: Record = { - [ProviderStatusValueMap.Ready]: StatusMaps.success, - [ProviderStatusValueMap.InActive]: StatusMaps.error +export const EndpointStatus: Record = { + [EndpointsStatusValueMap.Active]: StatusMaps.success, + [EndpointsStatusValueMap.Inactive]: StatusMaps.error }; // actions for each row diff --git a/src/pages/model-access/config/mock.ts b/src/pages/model-access/config/mock.ts deleted file mode 100644 index 2f761a85..00000000 --- a/src/pages/model-access/config/mock.ts +++ /dev/null @@ -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' - } -]; diff --git a/src/pages/model-access/config/types.ts b/src/pages/model-access/config/types.ts index 53f529e3..c33f7145 100644 --- a/src/pages/model-access/config/types.ts +++ b/src/pages/model-access/config/types.ts @@ -1,3 +1,23 @@ +export interface FormData { + name: string; + description: string; + categories: any[]; + meta: Record; + 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 { id: number; created_at: string; @@ -8,20 +28,9 @@ export interface AccessItem { categories: string[]; meta: Record; created_by_model: boolean; - endpoint_count: number; -} - -export interface FormData { - name: string; - description: string; - categories: any[]; - meta: Record; - endpoints: { - provider_model_name: string; - weight: number; - model_id: number; - provider_id: number; - }[]; + endpoints: number; + ready_endpoints: number; + access_policy: string; } export interface AccessPointItem { @@ -30,7 +39,7 @@ export interface AccessPointItem { updated_at: string; deleted_at: string; provider_model_name: string; - weight: number; + weight: number | null; model_id: number; provider_id: number; name: string; diff --git a/src/pages/model-access/forms/basic.tsx b/src/pages/model-access/forms/basic.tsx index 3ff69c7f..98ba32cf 100644 --- a/src/pages/model-access/forms/basic.tsx +++ b/src/pages/model-access/forms/basic.tsx @@ -1,6 +1,7 @@ import SealInput from '@/components/seal-form/seal-input'; 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 { Form } from 'antd'; import { FormData } from '../config/types'; @@ -8,6 +9,7 @@ import { FormData } from '../config/types'; const Basic = () => { const intl = useIntl(); const form = Form.useFormInstance(); + const { getRuleMessage } = useAppUtils(); return ( <> @@ -22,11 +24,17 @@ const Basic = () => { getValueProps={(value) => ({ value: Array.isArray(value) ? value[0] || null : value })} + rules={[ + { + required: true, + message: getRuleMessage('select', 'models.form.categories') + } + ]} > diff --git a/src/pages/model-access/forms/endpoints.tsx b/src/pages/model-access/forms/endpoints.tsx index 4d6afffc..3d5207d6 100644 --- a/src/pages/model-access/forms/endpoints.tsx +++ b/src/pages/model-access/forms/endpoints.tsx @@ -1,81 +1,72 @@ -import { LabelSelectorContext } from '@/components/label-selector/context'; import MetadataList from '@/components/metadata-list'; import SealCascader from '@/components/seal-form/seal-cascader'; import SealInput from '@/components/seal-form/seal-input'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import _ from 'lodash'; -import { useState } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { FormData } from '../config/types'; +import useEndpointSourceModels from '../hooks/use-endpoint-source-models'; -const providerModelList = [ - { - 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 EndpointsForm = forwardRef((props, ref) => { const intl = useIntl(); + const { sourceModels, loading, fetchSourceModels } = + useEndpointSourceModels(); const form = Form.useFormInstance(); - const endpoints = Form.useWatch('endpoints', form); + const endpoints = Form.useWatch('endpoints', form) || []; + const [fallbackValues, setFallbackValues] = useState<{ value: any[] }>({ + value: [] + }); const [dataList, setDataList] = useState< { - provider_model_name: string; - weight: number; - model_id: number; - provider_id: number; + weight: number | null; + value: any[]; }[] - >(endpoints || []); + >([]); - const handleEndpointsChange = (labels: Record) => { - form.setFieldValue('endpoints', labels); + useImperativeHandle(ref, () => ({ + 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 newDataList = [ ...dataList, { - provider_model_name: '', - weight: 1, - model_id: 0, - provider_id: 0 + weight: null, + value: [] } ]; setDataList(newDataList); @@ -84,115 +75,128 @@ const Endpoints = () => { const handleOnDelete = (index: number, item: any) => { const newDataList = dataList.filter((_, i) => i !== index); setDataList(newDataList); + + const endpointList = [...endpoints]; + endpointList.splice(index, 1); + form.setFieldValue('endpoints', [...endpointList]); }; - const handleFallbackChange = (e: any, index: number) => { - const checked = e.target.checked; - const newDataList = dataList.map((item, i) => { - if (i === index) { - return { - ...item, - is_fallback: checked - }; - } else if (checked) { - return { - ...item, - is_fallback: false - }; - } - return item; + const handleFallbackChange = (value: any[], options?: any[]) => { + const selectedOption = + options?.find?.((opt) => opt.value === value[1]) || {}; + + console.log('fallback selected option data:', value); + form.setFieldValue('fallback_endpoint', { + ...selectedOption?.data }); + 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); }; + useEffect(() => { + fetchSourceModels(); + }, []); + return ( <> - - ({ - 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' - }) - } - ) - ); - } + ({ + 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(); + } + }) + ]} + > + - - {(item, index) => ( - <> - triggerNode.parentNode} - > - : - - - )} - - - + {(item, index) => ( + <> + + handleEndpointsChange(value, index, options) + } + classNames={{ + popup: { + root: 'cascader-popup-wrapper gpu-selector' + } + }} + maxTagCount={1} + placeholder={intl.formatMessage({ + id: 'accesses.form.endpoint.model' + })} + value={item.value} + options={sourceModels} + showCheckedStrategy="SHOW_CHILD" + getPopupContainer={(triggerNode) => triggerNode.parentNode} + > + : + handleOnWeightChange(value, index)} + placeholder={intl.formatMessage({ + id: 'accesses.form.endpoint.weight' + })} + > + + )} + + + +
{ id: 'accesses.form.endpoint.fallback' })} maxTagCount={1} - options={providerModelList} + value={fallbackValues.value} + options={sourceModels} + onChange={(value, options) => handleFallbackChange(value, options)} showCheckedStrategy="SHOW_CHILD" getPopupContainer={(triggerNode) => triggerNode.parentNode} > - - +
+
); -}; +}); -export default Endpoints; +export default EndpointsForm; diff --git a/src/pages/model-access/forms/index.tsx b/src/pages/model-access/forms/index.tsx index 681ab337..57684584 100644 --- a/src/pages/model-access/forms/index.tsx +++ b/src/pages/model-access/forms/index.tsx @@ -1,12 +1,21 @@ import IconFont from '@/components/icon-font'; +import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import CollapsePanel from '@/pages/_components/collapse-panel'; import { useWrapperContext } from '@/pages/_components/column-wrapper/use-wrapper-context'; import ScrollSpyTabs from '@/pages/_components/scroll-spy-tabs'; import { useIntl } from '@umijs/max'; import { 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 useEditEndpoints from '../hooks/use-edit-endpoints'; import Basic from './basic'; import Endpoints from './endpoints'; import MetaData from './meta-data'; @@ -32,7 +41,8 @@ const AccessForm: React.FC = forwardRef((props, ref) => { const [activeKey, setActiveKey] = useState([TABKeysMap.BASIC]); const [form] = Form.useForm(); const scrollTabsRef = useRef(null); - + const endpointsRef = useRef(null); + const { generateEndpointData, fetchEndpoints } = useEditEndpoints(); const segmentOptions = [ { value: TABKeysMap.BASIC, @@ -58,10 +68,104 @@ const AccessForm: React.FC = forwardRef((props, ref) => { 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[]) => { 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, () => ({ submit: () => { form.submit(); @@ -86,14 +190,10 @@ const AccessForm: React.FC = forwardRef((props, ref) => { >
@@ -112,7 +212,7 @@ const AccessForm: React.FC = forwardRef((props, ref) => { key: TABKeysMap.ENDPOINTS, label: intl.formatMessage({ id: 'accesses.form.endpoint.title' }), forceRender: true, - children: + children: } ]} > diff --git a/src/pages/model-access/hooks/use-access-columns.tsx b/src/pages/model-access/hooks/use-access-columns.tsx index 12c38e8a..2a9c6a21 100644 --- a/src/pages/model-access/hooks/use-access-columns.tsx +++ b/src/pages/model-access/hooks/use-access-columns.tsx @@ -3,6 +3,7 @@ import AutoTooltip from '@/components/auto-tooltip'; import DropdownButtons from '@/components/drop-down-buttons'; import { SealColumnProps } from '@/components/seal-table/types'; import { tableSorter } from '@/config/settings'; +import ModelTag from '@/pages/_components/model-tag'; import { useIntl } from '@umijs/max'; import dayjs from 'dayjs'; import { useMemo } from 'react'; @@ -23,18 +24,23 @@ const useAccessColumns = ( sorter: tableSorter(1), span: 5, render: (text: string, record: AccessItem) => ( - <> + - {text} + {text} - + + ) }, { title: intl.formatMessage({ id: 'accesses.table.accessPoints' }), - dataIndex: 'accessPoints', + dataIndex: 'endpoints', span: 10, - render: (value: number) => {value} + render: (value: number, record: AccessItem) => ( + + {record.ready_endpoints} / {value} + + ) }, { title: intl.formatMessage({ id: 'common.table.createTime' }), diff --git a/src/pages/model-access/hooks/use-edit-endpoints.ts b/src/pages/model-access/hooks/use-edit-endpoints.ts new file mode 100644 index 00000000..0f8c142a --- /dev/null +++ b/src/pages/model-access/hooks/use-edit-endpoints.ts @@ -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; diff --git a/src/pages/model-access/hooks/use-endpoint-source-models.ts b/src/pages/model-access/hooks/use-endpoint-source-models.ts new file mode 100644 index 00000000..c995b958 --- /dev/null +++ b/src/pages/model-access/hooks/use-endpoint-source-models.ts @@ -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; +type CascaderOption = { + label: string; + value: string | number; + parent?: boolean; + disabled?: boolean; + index?: number; + children?: CascaderOption[]; +} & Partial; + +const useEndpointSourceModels = () => { + const [sourceModels, setSourceModels] = useState([]); + const [loading, setLoading] = useState(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; diff --git a/src/pages/model-access/index.tsx b/src/pages/model-access/index.tsx index 7a3af265..09a312e1 100644 --- a/src/pages/model-access/index.tsx +++ b/src/pages/model-access/index.tsx @@ -21,13 +21,16 @@ import AccessControlModal from '../llmodels/components/access-control-modal'; import { ACCESS_API, ACCESS_POINTS_API, + createAccess, deleteAccess, + deleteAccessPoint, + queryAccessPoints, queryModelAccesses, + setAccessPointAsFallback, updateAccess } from './apis'; import AccessPoints from './components/access-points'; import AddAccessModal from './components/add-access-modal'; -import { mockDataList } from './config/mock'; import { FormData, AccessItem as ListItem } from './config/types'; import useAccessColumns from './hooks/use-access-columns'; import useAccessControl from './hooks/use-access-control'; @@ -49,11 +52,12 @@ const Accesses: React.FC = () => { } = useTableFetch({ fetchAPI: queryModelAccesses, deleteAPI: deleteAccess, - watch: false, + watch: true, API: ACCESS_API, contentForDelete: 'menu.models.access' }); - const { watchDataList: allAccessPoints } = useWatchList(ACCESS_POINTS_API); + const { watchDataList: allAccessPoints, deleteItemFromCache } = + useWatchList(ACCESS_POINTS_API); const [expandAtom] = useAtom(expandKeysAtom); const { handleExpandChange, handleExpandAll, expandedRowKeys } = useExpandedRowKeys(expandAtom); @@ -84,6 +88,11 @@ const Accesses: React.FC = () => { id: openAccessModalStatus.currentData!.id }); } + if (openAccessModalStatus.action === PageAction.CREATE) { + await createAccess({ + data: params + }); + } fetchData(); closeAccessModal(); message.success(intl.formatMessage({ id: 'common.message.success' })); @@ -98,10 +107,7 @@ const Accesses: React.FC = () => { const handleEditProvider = (row: ListItem) => { openAccessModal( PageAction.EDIT, - intl.formatMessage( - { id: 'clusters.edit.cluster' }, - { cluster: row.name } - ), + intl.formatMessage({ id: 'common.button.edit.item' }, { name: row.name }), row ); }; @@ -135,13 +141,13 @@ const Accesses: React.FC = () => { const loadChildrenData = useMemoizedFn( async (row: ListItem, options?: any) => { const params = { - cluster_id: row.id, - page: -1 + id: row.id }; - // const data = await queryAccessPoints(params, { - // token: options?.token - // }); - return [1]; + const res = await queryAccessPoints(params, { + token: options?.token + }); + + return res.items || []; } ); @@ -149,24 +155,40 @@ const Accesses: React.FC = () => { handleTableChange({}, {}, order, { action: 'sort' }); }; - const onChildSelect = useMemoizedFn((val: any, record: any) => { - if (val === 'fallback') { - console.log('open fallback settings modal', record); - } + const handleDeleteEndpoint = (row: any) => { + modalRef.current?.show({ + 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 = ( list: any, options: { parent?: any; [key: string]: any } ) => { - return ( - - ); + return ; }; const columns = useAccessColumns(handleSelect); @@ -201,7 +223,7 @@ const Accesses: React.FC = () => { renderChildren={renderChildren} onTableSort={handleOnSortChange} showSorterTooltip={false} - dataSource={mockDataList} + dataSource={dataSource.dataList} loading={dataSource.loading} loadend={dataSource.loadend} rowSelection={rowSelection}