feat: add backend pages
This commit is contained in:
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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%;
|
||||
`;
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -31,5 +31,6 @@ export default {
|
||||
'menu.clusterManagement.clusters': '集群',
|
||||
'menu.clusterManagement.credentials': '云凭证',
|
||||
'menu.clusterManagement.clusterDetail': '集群详情',
|
||||
'menu.clusterManagement.clusterCreate': '创建集群'
|
||||
'menu.clusterManagement.clusterCreate': '创建集群',
|
||||
'menu.models.backendsList': '后端'
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user