Browse Source

feat: 完成模型部署UI

pull/46/head
cp3hnu 1 year ago
parent
commit
aee8384209
12 changed files with 895 additions and 9 deletions
  1. +14
    -5
      react-ui/config/routes.ts
  2. +11
    -0
      react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less
  3. +39
    -0
      react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx
  4. +17
    -0
      react-ui/src/pages/ModelDeployment/create.less
  5. +297
    -0
      react-ui/src/pages/ModelDeployment/create.tsx
  6. +53
    -0
      react-ui/src/pages/ModelDeployment/info.less
  7. +149
    -0
      react-ui/src/pages/ModelDeployment/info.tsx
  8. +21
    -0
      react-ui/src/pages/ModelDeployment/list.less
  9. +283
    -0
      react-ui/src/pages/ModelDeployment/list.tsx
  10. +1
    -1
      react-ui/src/pages/Workspace/components/QuickStart/index.less
  11. +2
    -1
      react-ui/src/pages/Workspace/components/QuickStart/index.tsx
  12. +8
    -2
      react-ui/src/utils/modal.tsx

+ 14
- 5
react-ui/config/routes.ts View File

@@ -188,14 +188,23 @@ export default [
],
},
{
name: 'modelDseployment',
path: '/modelDseployment',
name: 'modelDeployment',
path: '/modelDeployment',
routes: [
{
name: '模型部署',
name: '模型列表',
path: '',
key: 'modelDseployment',
component: './missingPage.jsx',
component: './ModelDeployment/list',
},
{
name: '镜像详情',
path: ':id',
component: './ModelDeployment/info',
},
{
name: '创建镜像',
path: 'create',
component: './ModelDeployment/create',
},
],
},


+ 11
- 0
react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less View File

@@ -0,0 +1,11 @@
.mirror-status-cell {
color: @text-color;

&--success {
color: @success-color;
}

&--error {
color: @error-color;
}
}

+ 39
- 0
react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx View File

@@ -0,0 +1,39 @@
/*
* @Author: 赵伟
* @Date: 2024-04-18 18:35:41
* @Description:
*/
import { MirrorVersionStatus } from '@/enums';
import styles from './index.less';

type MirrorVersionStatusKeys = keyof typeof MirrorVersionStatus;
type MirrorVersionStatusValues = (typeof MirrorVersionStatus)[MirrorVersionStatusKeys];

export type MirrorVersionStatusInfo = {
text: string;
classname: string;
};

const statusInfo: Record<MirrorVersionStatusValues, MirrorVersionStatusInfo> = {
[MirrorVersionStatus.Building]: {
text: '构建中',
classname: styles['mirror-status-cell'],
},
[MirrorVersionStatus.Available]: {
classname: styles['mirror-status-cell--success'],
text: '可用',
},
[MirrorVersionStatus.Failed]: {
classname: styles['mirror-status-cell--error'],
text: '构建失败',
},
};

function MirrorStatusCell(status: MirrorVersionStatus) {
if (status === null || status === undefined || !statusInfo[status]) {
return <span>--</span>;
}
return <span className={statusInfo[status].classname}>{statusInfo[status].text}</span>;
}

export default MirrorStatusCell;

+ 17
- 0
react-ui/src/pages/ModelDeployment/create.less View File

@@ -0,0 +1,17 @@
.model-deployment-create {
height: 100%;

&__content {
height: calc(100% - 60px);
margin-top: 10px;
padding: 30px 30px 10px;
overflow: auto;
background-color: white;
border-radius: 10px;

&__type {
color: @text-color;
font-size: @font-size-input-lg;
}
}
}

+ 297
- 0
react-ui/src/pages/ModelDeployment/create.tsx View File

@@ -0,0 +1,297 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建模型部署
*/
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { createMirrorReq } from '@/services/mirror';
import { getComputingResourceReq } from '@/services/pipeline';
import { to } from '@/utils/promise';
import { getSessionItemThenRemove, mirrorNameKey } from '@/utils/sessionStorage';
import { validateUploadFiles } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import { Button, Col, Form, Input, Row, Select, UploadFile, message, type SelectProps } from 'antd';
import { omit } from 'lodash';
import { useEffect, useState } from 'react';
import styles from './create.less';

type FormData = {
name: string;
tag: string;
description: string;
path?: string;
upload_type: string;
fileList?: UploadFile[];
};

function ModelDeploymentCreate() {
const navgite = useNavigate();
const [form] = Form.useForm();
const [nameDisabled, setNameDisabled] = useState(false);
const [resourceStandardList, setResourceStandardList] = useState([]);

useEffect(() => {
const name = getSessionItemThenRemove(mirrorNameKey);
if (name) {
form.setFieldValue('name', name);
setNameDisabled(true);
}
getComputingResource();
}, []);

const getComputingResource = async () => {
const params = {
page: 0,
size: 1000,
resource_type: '',
};
const [res] = await to(getComputingResourceReq(params));
if (res && res.data && res.data.content) {
setResourceStandardList(res.data.content);
}
};

const filterResourceStandard: SelectProps['filterOption'] = (
input: string,
{ computing_resource = '' },
) => {
return computing_resource.toLocaleLowerCase().includes(input.toLocaleLowerCase());
};

// 创建公网、本地镜像
const createPublicMirror = async (formData: FormData) => {
const upload_type = formData['upload_type'];
let params;
if (upload_type === CommonTabKeys.Public) {
params = {
...omit(formData, ['upload_type']),
upload_type: 0,
image_type: 0,
};
} else {
const fileList = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const file = fileList[0];
params = {
...omit(formData, ['fileList', 'upload_type']),
path: file.response.data.url,
file_size: file.response.data.fileSize,
upload_type: 1,
image_type: 0,
};
}
}

const [res] = await to(createMirrorReq(params));
if (res) {
message.success('创建成功');
navgite(-1);
}
};

// 提交
const handleSubmit = (values: FormData) => {
createPublicMirror(values);
};

// 取消
const cancel = () => {
navgite(-1);
};

return (
<div className={styles['model-deployment-create']}>
<PageTitle title="创建推理服务"></PageTitle>
<div className={styles['model-deployment-create__content']}>
<div>
<Form
name="model-deployment-create"
labelCol={{ flex: '130px' }}
wrapperCol={{ flex: 1 }}
labelAlign="left"
form={form}
initialValues={{ upload_type: CommonTabKeys.Public }}
onFinish={handleSubmit}
size="large"
>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="服务名称"
name="name"
rules={[
{
required: true,
message: '请输入服务名称',
},
]}
>
<Input
placeholder="请输入服务名称"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={20}>
<Form.Item
label="描  述"
name="description"
rules={[
{
required: true,
message: '请输入描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入描述,最长128字符"
maxLength={128}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<SubAreaTitle
title="部署构建"
image={require('@/assets/img/mirror-version.png')}
style={{ marginTop: '20px', marginBottom: '24px' }}
></SubAreaTitle>

<Row gutter={10}>
<Col span={10}>
<Form.Item
label="选择模型"
name="name"
rules={[
{
required: true,
message: '请输入模型',
},
]}
>
<Input
placeholder="请输入模型"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="选择镜像"
name="name"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<Input
placeholder="请输入镜像"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="资源规格"
name="name"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<Select
showSearch
placeholder="请选择资源规格"
filterOption={filterResourceStandard}
options={resourceStandardList}
fieldNames={{
label: 'description',
value: 'standard',
}}
/>
</Form.Item>
</Col>
</Row>

<Row gutter={10}>
<Col span={10}>
<Form.Item
label="副本数量"
name="name"
rules={[
{
required: true,
message: '请输入副本数量',
},
]}
>
<Input
placeholder="请输入副本数量"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>

<Row gutter={10}>
<Col span={10}>
<Form.Item label="环境变量" name="name">
<Button type="link" style={{ padding: '0' }}>
添加环境变量
</Button>
</Form.Item>
</Col>
</Row>

<Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit">
确定
</Button>
<Button
type="default"
htmlType="button"
onClick={cancel}
style={{ marginLeft: '20px' }}
>
取消
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
}

export default ModelDeploymentCreate;

+ 53
- 0
react-ui/src/pages/ModelDeployment/info.less View File

@@ -0,0 +1,53 @@
.model-deployment-info {
height: 100%;

&__basic {
&__item {
display: flex;
align-items: flex-start;
font-size: 16px;
line-height: 1.6;

.label {
width: 80px;
color: @text-color-secondary;
}

.value {
flex: 1;
color: @text-color;
}
}
}

&__content {
height: calc(100% - 60px);
margin-top: 10px;
padding: 30px 30px 0;
background-color: white;
border-radius: 10px;

&__title {
display: flex;
align-items: center;
}

&__table {
:global {
.ant-table-wrapper {
height: 100%;
.ant-spin-nested-loading {
height: 100%;
}
.ant-spin-container {
height: 100%;
}
.ant-table {
height: calc(100% - 74px);
overflow: auto;
}
}
}
}
}
}

+ 149
- 0
react-ui/src/pages/ModelDeployment/info.tsx View File

@@ -0,0 +1,149 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 镜像详情
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { getMirrorInfoReq } from '@/services/mirror';
import { to } from '@/utils/promise';
import { useNavigate, useParams } from '@umijs/max';
import { Col, Row, Tabs, type TabsProps } from 'antd';
import dayjs from 'dayjs';
import { useEffect, useState } from 'react';
import styles from './info.less';

type MirrorInfoData = {
name?: string;
description?: string;
version_count?: string;
create_time?: string;
};

type MirrorVersionData = {
id: number;
version: string;
url: string;
status: string;
file_size: string;
create_time: string;
};

const tabItems = [
{
key: '1',
label: '预测',
icon: <KFIcon type="icon-yuce" />,
},
{
key: '2',
label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />,
},
{
key: '3',
label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />,
},
];

function ModelDeploymentInfo() {
const navigate = useNavigate();
const urlParams = useParams();

const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({});

const [activeTab, setActiveTab] = useState<string>('1');
useEffect(() => {
getMirrorInfo();
}, []);

// 获取镜像详情
const getMirrorInfo = async () => {
const id = Number(urlParams.id);
const [res] = await to(getMirrorInfoReq(id));
if (res && res.data) {
const { name = '', description = '', version_count = '', create_time: time } = res.data;
let create_time =
time && dayjs(time).isValid() ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '--';
setMirrorInfo({
name,
description,
version_count,
create_time,
});
}
};

// 切换 Tab,重置数据
const hanleTabChange: TabsProps['onChange'] = (value) => {
setActiveTab(value);
};

return (
<div className={styles['model-deployment-info']}>
<PageTitle title="服务详情"></PageTitle>
<div className={styles['model-deployment-info__content']}>
<div>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['model-deployment-info__basic']}>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>服务名称:</div>
<div className={styles['value']}>{mirrorInfo.name}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>镜像:</div>
<div className={styles['value']}>{mirrorInfo.version_count ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>状态:</div>
<div className={styles['value']}>{mirrorInfo.name}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>模型:</div>
<div className={styles['value']}>{mirrorInfo.version_count ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>环境变量:</div>
<div className={styles['value']}>{mirrorInfo.name}</div>
</div>
</Col>
</Row>
<Row gutter={40}>
<Col span={24}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>描述:</div>
<div className={styles['value']}>{mirrorInfo.description}</div>
</div>
</Col>
</Row>
</div>
<div>
<Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} />
</div>
</div>
</div>
</div>
);
}

export default ModelDeploymentInfo;

+ 21
- 0
react-ui/src/pages/ModelDeployment/list.less View File

@@ -0,0 +1,21 @@
.model-deployment {
height: 100%;
&__content {
height: calc(100% - 60px);
margin-top: 10px;
padding: 20px 30px 0;
background-color: white;
border-radius: 10px;

&__filter {
display: flex;
align-items: center;
justify-content: space-between;
}

&__table {
height: calc(100% - 32px - 28px);
margin-top: 28px;
}
}
}

+ 283
- 0
react-ui/src/pages/ModelDeployment/list.tsx View File

@@ -0,0 +1,283 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 模型部署列表
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { useCacheState } from '@/hooks/pageCacheState';
import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Table,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import styles from './list.less';

export type MirrorData = {
id: number;
name: string;
description: string;
create_time: string;
};

function ModelDeployment() {
const navigate = useNavigate();
const { message } = App.useApp();
const [cacheState, setCacheState] = useCacheState();
const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<MirrorData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<Required<TablePaginationConfig>>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);

useEffect(() => {
getMirrorList();
}, [pagination, searchText]);

// 获取镜像列表
const getMirrorList = async () => {
const params: Record<string, any> = {
page: pagination.current - 1,
size: pagination.pageSize,
name: searchText,
image_type: 1,
};
const [res] = await to(getMirrorListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

// 删除镜像
const deleteMirror = async (id: number) => {
const [res] = await to(deleteMirrorReq(id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getMirrorList();
}
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 查看详情
const toDetail = (record: MirrorData) => {
navigate(`/modelDeployment/${record.id}`);
setCacheState({
pagination,
searchText,
});
};

// 处理删除
const handleMirrorDelete = (record: MirrorData) => {
modalConfirm({
title: '删除后,该镜像将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteMirror(record.id);
},
});
};

// 创建镜像
const createMirror = () => {
navigate(`/modelDeployment/create`);
setCacheState({
pagination,
searchText,
});
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => {
if (action === 'paginate') {
setPagination(pagination);
}
// console.log(pagination, filters, sorter, action);
};

const columns: TableProps<MirrorData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 100,
align: 'center',
render(text, record, index) {
return <span>{(pagination.current - 1) * pagination.pageSize + index + 1}</span>;
},
},
{
title: '服务名称',
dataIndex: 'name',
key: 'name',
width: '30%',
render: CommonTableCell(),
},
{
title: '模型',
dataIndex: 'version_count',
key: 'version_count',
width: '20%',
render: CommonTableCell(),
},
{
title: '状态',
dataIndex: 'version_count',
key: 'version_count',
width: '10%',
render: CommonTableCell(),
},
{
title: '创建人',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(true),
width: '20%',
ellipsis: { showTitle: false },
},
{
title: '更新时间',
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
render: DateTableCell,
},
{
title: '操作',
dataIndex: 'operation',
width: 350,
key: 'operation',
render: (_: any, record: MirrorData) => (
<div>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => toDetail(record)}
>
编辑
</Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => toDetail(record)}
>
启动
</Button>
<Button
type="link"
size="small"
key="stop"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => toDetail(record)}
>
停止
</Button>
<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleMirrorDelete(record)}
>
删除
</Button>
</ConfigProvider>
</div>
),
},
];

return (
<div className={styles['model-deployment']}>
<PageTitle title="模型列表"></PageTitle>
<div className={styles['model-deployment__content']}>
<div className={styles['model-deployment__filter']}>
<Input.Search
placeholder="按数据集名称筛选"
allowClear
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
value={inputText}
/>
<Button
style={{ marginLeft: '20px' }}
type="default"
onClick={createMirror}
icon={<KFIcon type="icon-xinjian2" />}
>
创建推理服务
</Button>
</div>
<div
className={classNames(
'vertical-scroll-table',
styles['model-deployment__content__table'],
)}
>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default ModelDeployment;

+ 1
- 1
react-ui/src/pages/Workspace/components/QuickStart/index.less View File

@@ -1,5 +1,5 @@
.quick-start {
width: calc(100% - 326px);
width: calc(100% - 326px - 15px);
padding: 20px 30px;
background-color: white;
border-radius: 4px;


+ 2
- 1
react-ui/src/pages/Workspace/components/QuickStart/index.tsx View File

@@ -29,9 +29,10 @@ function QuickStart() {
}
};

changeScale();

const debounceFunc = debounce(changeScale, 16);
window.addEventListener('resize', debounceFunc);
changeScale();
return () => {
window.removeEventListener('resize', debounceFunc);
};


+ 8
- 2
react-ui/src/utils/modal.tsx View File

@@ -1,10 +1,11 @@
/*
* @Author: 赵伟
* @Date: 2024-04-13 10:08:35
* @Description:
* @Description: 以函数的方式打开 Modal
*/
import { ConfigProvider, type ModalProps } from 'antd';
import { globalConfig } from 'antd/es/config-provider';
import zhCN from 'antd/locale/zh_CN';
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';

@@ -59,7 +60,12 @@ export const openAntdModal = <T extends ModalProps>(
);

root.render(
<ConfigProvider prefixCls={rootPrefixCls} iconPrefixCls={iconPrefixCls} theme={theme}>
<ConfigProvider
prefixCls={rootPrefixCls}
iconPrefixCls={iconPrefixCls}
theme={theme}
locale={zhCN}
>
{global.holderRender ? global.holderRender(dom) : dom}
</ConfigProvider>,
);


Loading…
Cancel
Save