diff --git a/config/routes.ts b/config/routes.ts index b0e6de18..d1b89eb1 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -105,6 +105,16 @@ export default [ defaultIcon: 'icon-models', access: 'canSeeAdmin', component: './llmodels/user-models' + }, + { + name: 'backendsList', + path: '/models/backends/list', + key: 'backendsList', + icon: 'icon-backend', + selectedIcon: 'icon-backend-filled', + defaultIcon: 'icon-backend', + access: 'canSeeAdmin', + component: './backends/list' } ] }, diff --git a/src/assets/logo/sglang.png b/src/assets/logo/sglang.png new file mode 100644 index 00000000..2a8bc258 Binary files /dev/null and b/src/assets/logo/sglang.png differ diff --git a/src/assets/logo/vllm.png b/src/assets/logo/vllm.png new file mode 100644 index 00000000..0db97e1b Binary files /dev/null and b/src/assets/logo/vllm.png differ diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 593a24fa..2e9bc504 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -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_1ahssp5gmym.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_wt33o3w25g.js' }); export default IconFont; diff --git a/src/components/page-tools/index.tsx b/src/components/page-tools/index.tsx index 7bcff109..1c0839c6 100644 --- a/src/components/page-tools/index.tsx +++ b/src/components/page-tools/index.tsx @@ -63,7 +63,7 @@ interface FilterBarProps { actionItems?: ActionItem[]; selectOptions?: Global.BaseOption[]; showSelect?: boolean; - buttonText: string; + buttonText?: string; buttonIcon?: React.ReactNode; marginBottom?: number; marginTop?: number; @@ -72,6 +72,8 @@ interface FilterBarProps { actionType?: 'dropdown' | 'button'; showPrimaryButton?: boolean; showDeleteButton?: boolean; + right?: React.ReactNode; + left?: React.ReactNode; width?: { input?: number; select?: number; @@ -98,6 +100,8 @@ export const FilterBar: React.FC = (props) => { selectHolder, showPrimaryButton = true, showDeleteButton = true, + right, + left, width } = props; const intl = useIntl(); @@ -195,7 +199,7 @@ export const FilterBar: React.FC = (props) => { > } - right={renderRight} + right={right || renderRight} > ); }; diff --git a/src/components/templates/card.tsx b/src/components/templates/card.tsx index 14480f1a..dba395eb 100644 --- a/src/components/templates/card.tsx +++ b/src/components/templates/card.tsx @@ -66,7 +66,7 @@ const Inner = styled.div.attrs({ display: flex; width: 100%; flex-direction: column; - justify-content: center; + justify-content: flex-start; gap: 8px; height: 100%; `; diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 1e97abff..9f5e8c54 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -31,5 +31,6 @@ export default { 'menu.clusterManagement.clusters': 'Clusters', 'menu.clusterManagement.credentials': 'Cloud Credentials', 'menu.clusterManagement.clusterDetail': 'Cluster Detail', - 'menu.clusterManagement.clusterCreate': 'Create Cluster' + 'menu.clusterManagement.clusterCreate': 'Create Cluster', + 'menu.models.backendsList': 'Backends' }; diff --git a/src/locales/ja-JP/menu.ts b/src/locales/ja-JP/menu.ts index 7f0c362e..cde623a8 100644 --- a/src/locales/ja-JP/menu.ts +++ b/src/locales/ja-JP/menu.ts @@ -31,7 +31,8 @@ export default { 'menu.clusterManagement.credentials': 'Cloud Credentials', 'menu.models.userModels': 'My Models', 'menu.clusterManagement.clusterDetail': 'Cluster Detail', - 'menu.clusterManagement.clusterCreate': 'Create Cluster' + 'menu.clusterManagement.clusterCreate': 'Create Cluster', + 'menu.models.backendsList': 'Backends' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== @@ -47,5 +48,6 @@ export default { // 10. 'menu.clusterManagement.credentials': 'Cloud Credentials', // 11. 'menu.models.userModels': 'My Models' // 12. 'menu.clusterManagement.clusterDetail': 'Cluster Detail', -// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster' +// 13. 'menu.clusterManagement.clusterCreate': 'Create Cluster', +// 14. 'menu.models.backendsList': 'Backends' // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/menu.ts b/src/locales/ru-RU/menu.ts index 5c87ba12..37977bc7 100644 --- a/src/locales/ru-RU/menu.ts +++ b/src/locales/ru-RU/menu.ts @@ -31,9 +31,10 @@ export default { 'menu.clusterManagement.credentials': 'Облачные аккаунты', 'menu.models.userModels': 'Мои модели', 'menu.clusterManagement.clusterDetail': 'Детали кластера', - 'menu.clusterManagement.clusterCreate': 'Создать кластер' + 'menu.clusterManagement.clusterCreate': 'Создать кластер', + 'menu.models.backendsList': 'Backends' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== - +// 14. 'menu.models.backendsList': 'Backends' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index e08d2728..01bd9e6f 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -31,5 +31,6 @@ export default { 'menu.clusterManagement.clusters': '集群', 'menu.clusterManagement.credentials': '云凭证', 'menu.clusterManagement.clusterDetail': '集群详情', - 'menu.clusterManagement.clusterCreate': '创建集群' + 'menu.clusterManagement.clusterCreate': '创建集群', + 'menu.models.backendsList': '后端' }; diff --git a/src/pages/backends/apis/index.ts b/src/pages/backends/apis/index.ts new file mode 100644 index 00000000..6b27cbd8 --- /dev/null +++ b/src/pages/backends/apis/index.ts @@ -0,0 +1,52 @@ +import { request } from '@umijs/max'; +import { FormData, ListItem } from '../config/types'; + +export const INFERENCE_BACKEND_API = '/inference-backend'; + +export const FROM_YAML_API = '/from-yaml'; + +export async function queryBackendsList(params: Global.SearchParams) { + return request>(`${INFERENCE_BACKEND_API}`, { + method: 'GET', + params + }); +} + +export async function createBackend(params: { data: FormData }) { + return request(`${INFERENCE_BACKEND_API}`, { + method: 'POST', + data: params.data + }); +} + +export async function updateBackend(id: number, params: { data: FormData }) { + return request(`${INFERENCE_BACKEND_API}/${id}`, { + method: 'PUT', + data: params.data + }); +} + +export async function deleteBackend(id: number) { + return request(`${INFERENCE_BACKEND_API}/${id}`, { + method: 'DELETE' + }); +} + +export async function createBackendFromYAML(params: { + data: { content: string }; +}) { + return request(`${INFERENCE_BACKEND_API}${FROM_YAML_API}`, { + method: 'POST', + data: params.data + }); +} + +export async function updateBackendFromYAML( + id: number, + params: { data: { content: string } } +) { + return request(`${INFERENCE_BACKEND_API}/${id}${FROM_YAML_API}`, { + method: 'PUT', + data: params.data + }); +} diff --git a/src/pages/backends/components/backend-card.tsx b/src/pages/backends/components/backend-card.tsx new file mode 100644 index 00000000..eaf90753 --- /dev/null +++ b/src/pages/backends/components/backend-card.tsx @@ -0,0 +1,86 @@ +import DropDownActions from '@/components/drop-down-actions'; +import IconFont from '@/components/icon-font'; +import Card from '@/components/templates/card'; +import { Button } from 'antd'; +import styled from 'styled-components'; +import { backendActions } from '../config'; +import { ListItem } from '../config/types'; + +const Header = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + .title { + display: flex; + align-items: center; + gap: 16px; + } +`; + +const Content = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-top: 12x; + gap: 16px; + .title { + color: var(--ant-color-text); + font-weight: 500; + line-height: 1.5; + } +`; + +interface BackendCardProps { + onClick: (data: any) => void; + onSelect?: (item: any) => void; + data: ListItem; +} + +const BackendCard: React.FC = ({ + onClick, + data, + onSelect +}) => { + return ( + onClick(data)} + clickable={true} + disabled={false} + height={205} + ghost + header={ +
+
+ {data.backend_name} +
+ + + + + +
+ } + > + +
{data.backend_name}
+
This is a description
+
默认版本:{data.default_version}
+
+ 镜像:{data.version_configs?.[data.default_version]?.image_name} +
+
+
+ ); +}; + +export default BackendCard; diff --git a/src/pages/backends/components/backend-list.tsx b/src/pages/backends/components/backend-list.tsx new file mode 100644 index 00000000..fe9239ed --- /dev/null +++ b/src/pages/backends/components/backend-list.tsx @@ -0,0 +1,108 @@ +import CardSkeleton from '@/components/templates/card-skelton'; +import breakpoints from '@/config/breakpoints'; +import { Col, FloatButton, Row, Spin } from 'antd'; +import _ from 'lodash'; +import ResizeObserver from 'rc-resize-observer'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import BackendCard from './backend-card'; + +const SpinWrapper = styled.div` + width: 100%; + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 0; + left: 0; + max-height: 400px; + right: 0; + .skelton-wrapper { + width: 100%; + } +`; + +interface BackendListProps { + defaultSpan?: number; + resizable?: boolean; + dataList: any[]; + loading: boolean; + activeId: Global.WithFalse; + isFirst: boolean; +} + +const InnerSkeleton: React.FC<{ + span: number; +}> = (props) => { + return ; +}; + +const ListSkeleton: React.FC<{ + span: number; + loading: boolean; + isFirst: boolean; +}> = ({ span, loading, isFirst }) => { + return ( +
+ {loading && ( + + + {isFirst && } + + + )} +
+ ); +}; + +const CardList: React.FC = (props) => { + const { + dataList, + loading, + isFirst, + defaultSpan = 6, + resizable = true + } = props; + const [span, setSpan] = React.useState(defaultSpan); + + const getSpanByWidth = (width: number) => { + if (width < breakpoints.md) return 24; + if (width < breakpoints.lg) return 12; + return 6; + }; + + const handleResize = useCallback( + _.throttle((size: { width: number; height: number }) => { + setSpan(getSpanByWidth(size.width)); + }, 100), + [] + ); + + return ( +
+ +
+ + {dataList.map((item: any) => { + return ( + + {}} data={item} /> + + ); + })} + + +
+
+ +
+ ); +}; + +export default CardList; diff --git a/src/pages/backends/config/index.ts b/src/pages/backends/config/index.ts new file mode 100644 index 00000000..43c163a9 --- /dev/null +++ b/src/pages/backends/config/index.ts @@ -0,0 +1,33 @@ +import icons from '@/components/icon-font/icons'; + +export const backendActions = [ + { + label: 'Edit Versions', + value: 'version', + key: 'version', + icon: icons.EditOutlined, + locale: false + }, + { + label: 'Edit Parameters', + value: 'parameter', + key: 'parameter', + icon: icons.EditOutlined, + locale: false + }, + { + label: 'Edit by Yaml', + value: 'yaml', + key: 'edit', + icon: icons.EditOutlined, + locale: false + }, + { + label: 'Delete', + value: 'delete', + key: 'delete', + icon: icons.DeleteOutlined, + locale: false, + danger: true + } +]; diff --git a/src/pages/backends/config/test.ts b/src/pages/backends/config/test.ts new file mode 100644 index 00000000..bef7b5b0 --- /dev/null +++ b/src/pages/backends/config/test.ts @@ -0,0 +1,54 @@ +import nvidiaLogo from '@/assets/logo/nvidia.png'; +import SGLangLogo from '@/assets/logo/sglang.png'; +import vLLMLogo from '@/assets/logo/vllm.png'; + +export default [ + { + id: 1, + backend_name: 'SGlang', + icon: SGLangLogo, + version_configs: { + 'v1.0': { + image_name: 'example/image:v1.0', + run_command: 'run.sh' + } + }, + compatibility_type: 'template_string', + health_check_path: '/v1/models', + default_version: 'v1.0', + default_backend_param: ['--param1', '--param2'], + description: 'sglang description' + }, + { + id: 2, + backend_name: 'vLLM', + icon: vLLMLogo, + version_configs: { + 'v1.0': { + image_name: 'example/image:v1.0', + run_command: 'run.sh' + } + }, + compatibility_type: 'template_string', + health_check_path: '/v1/models', + default_version: 'v1.0', + default_backend_param: ['--param1', '--param2'], + description: 'vllm description' + }, + { + id: 3, + backend_name: 'TensorRT', + icon: nvidiaLogo, + version_configs: { + 'v1.0': { + image_name: 'example/image:v1.0', + run_command: 'run.sh' + } + }, + compatibility_type: 'template_string', + health_check_path: '/v1/models', + default_version: 'v1.0', + default_backend_param: ['--param1', '--param2'], + description: 'TensorRT description' + } +]; diff --git a/src/pages/backends/config/types.ts b/src/pages/backends/config/types.ts new file mode 100644 index 00000000..87800c40 --- /dev/null +++ b/src/pages/backends/config/types.ts @@ -0,0 +1,23 @@ +export interface VersionConfigs { + [key: string]: { + image_name: string; + run_command: string; + }; +} + +export interface FormData { + backend_name: string; + description?: string; + version_configs?: VersionConfigs; + compatibility_type?: string; + health_check_path?: string; + default_version?: string; + default_backend_param?: string[]; + allowed_proxy_uris?: string[]; +} + +export interface ListItem extends FormData { + id: number; + created_at: string; + updated_at: string; +} diff --git a/src/pages/backends/list.tsx b/src/pages/backends/list.tsx new file mode 100644 index 00000000..5fe163eb --- /dev/null +++ b/src/pages/backends/list.tsx @@ -0,0 +1,112 @@ +import { FilterBar } from '@/components/page-tools'; +import useTableFetch from '@/hooks/use-table-fetch'; +import { + DeleteOutlined, + ImportOutlined, + PlusOutlined +} from '@ant-design/icons'; +import { PageContainer } from '@ant-design/pro-components'; +import { useIntl } from '@umijs/max'; +import { Button, Space } from 'antd'; +import { + deleteBackend, + INFERENCE_BACKEND_API, + queryBackendsList +} from './apis'; +import BackendCardList from './components/backend-list'; +import dataList from './config/test'; +import { ListItem } from './config/types'; + +const BackendList = () => { + const intl = useIntl(); + + const { + dataSource, + rowSelection, + queryParams, + modalRef, + fetchData, + handleDelete, + handleDeleteBatch, + handlePageChange, + handleTableChange, + handleSearch, + handleNameChange, + handleQueryChange + } = useTableFetch({ + fetchAPI: queryBackendsList, + deleteAPI: deleteBackend, + API: INFERENCE_BACKEND_API, + watch: false, + contentForDelete: 'resources.modelfiles.modelfile' + }); + + const handleClickDropdown = () => {}; + + const handleDeleteByBatch = () => {}; + + return ( + + + + + + + } + > + + + + ); +}; + +export default BackendList; diff --git a/src/pages/cluster-management/components/provider-catalog.tsx b/src/pages/cluster-management/components/provider-catalog.tsx index 750c0b03..ffae42db 100644 --- a/src/pages/cluster-management/components/provider-catalog.tsx +++ b/src/pages/cluster-management/components/provider-catalog.tsx @@ -12,6 +12,9 @@ const Wrapper = styled.div<{ $cols?: number }>` .template-card-wrapper { padding: 16px; } + .template-card-inner { + justify-content: center; + } `; const Container = styled.div`