feat: add backend pages

This commit is contained in:
jialin
2025-10-13 14:40:26 +08:00
parent 1a693f70ac
commit e863b84016
18 changed files with 500 additions and 10 deletions
+10
View File
@@ -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'
}
]
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+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_1ahssp5gmym.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_wt33o3w25g.js'
});
export default IconFont;
+6 -2
View File
@@ -63,7 +63,7 @@ interface FilterBarProps {
actionItems?: ActionItem[];
selectOptions?: Global.BaseOption<string | number>[];
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<FilterBarProps> = (props) => {
selectHolder,
showPrimaryButton = true,
showDeleteButton = true,
right,
left,
width
} = props;
const intl = useIntl();
@@ -195,7 +199,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
></Button>
</Space>
}
right={renderRight}
right={right || renderRight}
></PageTools>
);
};
+1 -1
View File
@@ -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%;
`;
+2 -1
View File
@@ -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'
};
+4 -2
View File
@@ -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 ==========
+3 -2
View File
@@ -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 ==========
+2 -1
View File
@@ -31,5 +31,6 @@ export default {
'menu.clusterManagement.clusters': '集群',
'menu.clusterManagement.credentials': '云凭证',
'menu.clusterManagement.clusterDetail': '集群详情',
'menu.clusterManagement.clusterCreate': '创建集群'
'menu.clusterManagement.clusterCreate': '创建集群',
'menu.models.backendsList': '后端'
};
+52
View File
@@ -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<Global.PageResponse<ListItem>>(`${INFERENCE_BACKEND_API}`, {
method: 'GET',
params
});
}
export async function createBackend(params: { data: FormData }) {
return request<ListItem>(`${INFERENCE_BACKEND_API}`, {
method: 'POST',
data: params.data
});
}
export async function updateBackend(id: number, params: { data: FormData }) {
return request<ListItem>(`${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<ListItem>(`${INFERENCE_BACKEND_API}${FROM_YAML_API}`, {
method: 'POST',
data: params.data
});
}
export async function updateBackendFromYAML(
id: number,
params: { data: { content: string } }
) {
return request<ListItem>(`${INFERENCE_BACKEND_API}/${id}${FROM_YAML_API}`, {
method: 'PUT',
data: params.data
});
}
@@ -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<BackendCardProps> = ({
onClick,
data,
onSelect
}) => {
return (
<Card
onClick={() => onClick(data)}
clickable={true}
disabled={false}
height={205}
ghost
header={
<Header>
<div className="title">
<img src={data.icon} alt={data.backend_name} height={20} />
</div>
<span>
<DropDownActions
menu={{
items: backendActions,
onClick: onSelect
}}
>
<Button
icon={<IconFont type="icon-more"></IconFont>}
size="small"
type="text"
></Button>
</DropDownActions>
</span>
</Header>
}
>
<Content>
<div className="title">{data.backend_name}</div>
<div>This is a description</div>
<div>{data.default_version}</div>
<div>
{data.version_configs?.[data.default_version]?.image_name}
</div>
</Content>
</Card>
);
};
export default BackendCard;
@@ -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<number>;
isFirst: boolean;
}
const InnerSkeleton: React.FC<{
span: number;
}> = (props) => {
return <CardSkeleton {...props} />;
};
const ListSkeleton: React.FC<{
span: number;
loading: boolean;
isFirst: boolean;
}> = ({ span, loading, isFirst }) => {
return (
<div>
{loading && (
<SpinWrapper>
<Spin
spinning={loading}
style={{
width: '100%'
}}
wrapperClassName="skelton-wrapper"
>
{isFirst && <InnerSkeleton span={span}></InnerSkeleton>}
</Spin>
</SpinWrapper>
)}
</div>
);
};
const CardList: React.FC<BackendListProps> = (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 (
<div className="relative" style={{ width: '100%' }}>
<ResizeObserver onResize={handleResize} disabled={!resizable}>
<div style={{ width: '100%' }}>
<Row gutter={[16, 16]}>
{dataList.map((item: any) => {
return (
<Col span={span} key={item.id}>
<BackendCard onClick={() => {}} data={item} />
</Col>
);
})}
</Row>
<ListSkeleton span={span} loading={loading} isFirst={isFirst} />
</div>
</ResizeObserver>
<FloatButton.BackTop visibilityHeight={1000} />
</div>
);
};
export default CardList;
+33
View File
@@ -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
}
];
+54
View File
@@ -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'
}
];
+23
View File
@@ -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;
}
+112
View File
@@ -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<ListItem>({
fetchAPI: queryBackendsList,
deleteAPI: deleteBackend,
API: INFERENCE_BACKEND_API,
watch: false,
contentForDelete: 'resources.modelfiles.modelfile'
});
const handleClickDropdown = () => {};
const handleDeleteByBatch = () => {};
return (
<PageContainer
ghost
header={{
title: intl.formatMessage({ id: 'menu.models.backendsList' }),
style: {
paddingInline: 'var(--layout-content-header-inlinepadding)'
},
breadcrumb: {}
}}
extra={[]}
>
<FilterBar
marginBottom={22}
marginTop={30}
width={{
input: 300
}}
actionType="dropdown"
inputHolder={intl.formatMessage({ id: 'common.filter.name' })}
buttonText={intl.formatMessage({
id: 'resources.modelfiles.download'
})}
handleDeleteByBatch={handleDeleteByBatch}
handleClickPrimary={handleClickDropdown}
handleSearch={handleSearch}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
showSelect={false}
right={
<Space size={20}>
<Button type="primary" icon={<PlusOutlined />}>
Add Backend
</Button>
<Button type="primary" icon={<ImportOutlined />}>
Import backend from yaml
</Button>
<Button
icon={<DeleteOutlined />}
danger
onClick={handleDeleteByBatch}
disabled={!rowSelection?.selectedRowKeys?.length}
>
<span>
{intl?.formatMessage?.({ id: 'common.button.delete' })}
{rowSelection?.selectedRowKeys?.length > 0 && (
<span>({rowSelection?.selectedRowKeys?.length})</span>
)}
</span>
</Button>
</Space>
}
></FilterBar>
<BackendCardList
dataList={dataList}
loading={dataSource.loading}
activeId={false}
isFirst={!dataSource.loadend}
></BackendCardList>
</PageContainer>
);
};
export default BackendList;
@@ -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`