From aee83842097fd1db9685cc1e9680b871891e372b Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 14 May 2024 08:55:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=83=A8=E7=BD=B2UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/config/routes.ts | 19 +- .../components/MirrorStatusCell/index.less | 11 + .../components/MirrorStatusCell/index.tsx | 39 +++ .../src/pages/ModelDeployment/create.less | 17 + react-ui/src/pages/ModelDeployment/create.tsx | 297 ++++++++++++++++++ react-ui/src/pages/ModelDeployment/info.less | 53 ++++ react-ui/src/pages/ModelDeployment/info.tsx | 149 +++++++++ react-ui/src/pages/ModelDeployment/list.less | 21 ++ react-ui/src/pages/ModelDeployment/list.tsx | 283 +++++++++++++++++ .../components/QuickStart/index.less | 2 +- .../Workspace/components/QuickStart/index.tsx | 3 +- react-ui/src/utils/modal.tsx | 10 +- 12 files changed, 895 insertions(+), 9 deletions(-) create mode 100644 react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less create mode 100644 react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx create mode 100644 react-ui/src/pages/ModelDeployment/create.less create mode 100644 react-ui/src/pages/ModelDeployment/create.tsx create mode 100644 react-ui/src/pages/ModelDeployment/info.less create mode 100644 react-ui/src/pages/ModelDeployment/info.tsx create mode 100644 react-ui/src/pages/ModelDeployment/list.less create mode 100644 react-ui/src/pages/ModelDeployment/list.tsx diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index 855ad15c..0e502c59 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -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', }, ], }, diff --git a/react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less b/react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less new file mode 100644 index 00000000..043bf411 --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less @@ -0,0 +1,11 @@ +.mirror-status-cell { + color: @text-color; + + &--success { + color: @success-color; + } + + &--error { + color: @error-color; + } +} diff --git a/react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx b/react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx new file mode 100644 index 00000000..3702825f --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx @@ -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 = { + [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 --; + } + return {statusInfo[status].text}; +} + +export default MirrorStatusCell; diff --git a/react-ui/src/pages/ModelDeployment/create.less b/react-ui/src/pages/ModelDeployment/create.less new file mode 100644 index 00000000..63c00764 --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/create.less @@ -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; + } + } +} diff --git a/react-ui/src/pages/ModelDeployment/create.tsx b/react-ui/src/pages/ModelDeployment/create.tsx new file mode 100644 index 00000000..cc2c43ff --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/create.tsx @@ -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 ( +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ ); +} + +export default ModelDeploymentCreate; diff --git a/react-ui/src/pages/ModelDeployment/info.less b/react-ui/src/pages/ModelDeployment/info.less new file mode 100644 index 00000000..c77a7070 --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/info.less @@ -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; + } + } + } + } + } +} diff --git a/react-ui/src/pages/ModelDeployment/info.tsx b/react-ui/src/pages/ModelDeployment/info.tsx new file mode 100644 index 00000000..3bc67279 --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/info.tsx @@ -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: , + }, + { + key: '2', + label: '调用指南', + icon: , + }, + { + key: '3', + label: '服务日志', + icon: , + }, +]; + +function ModelDeploymentInfo() { + const navigate = useNavigate(); + const urlParams = useParams(); + + const [mirrorInfo, setMirrorInfo] = useState({}); + + const [activeTab, setActiveTab] = useState('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 ( +
+ +
+
+ +
+ + +
+
服务名称:
+
{mirrorInfo.name}
+
+ + +
+
镜像:
+
{mirrorInfo.version_count ?? '--'}
+
+ +
+ + +
+
状态:
+
{mirrorInfo.name}
+
+ + +
+
模型:
+
{mirrorInfo.version_count ?? '--'}
+
+ +
+ + +
+
环境变量:
+
{mirrorInfo.name}
+
+ +
+ + +
+
描述:
+
{mirrorInfo.description}
+
+ +
+
+
+ +
+
+
+
+ ); +} + +export default ModelDeploymentInfo; diff --git a/react-ui/src/pages/ModelDeployment/list.less b/react-ui/src/pages/ModelDeployment/list.less new file mode 100644 index 00000000..9e521f70 --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/list.less @@ -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; + } + } +} diff --git a/react-ui/src/pages/ModelDeployment/list.tsx b/react-ui/src/pages/ModelDeployment/list.tsx new file mode 100644 index 00000000..bfad5a22 --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/list.tsx @@ -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([]); + const [total, setTotal] = useState(0); + const [pagination, setPagination] = useState>( + cacheState?.pagination ?? { + current: 1, + pageSize: 10, + }, + ); + + useEffect(() => { + getMirrorList(); + }, [pagination, searchText]); + + // 获取镜像列表 + const getMirrorList = async () => { + const params: Record = { + 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['columns'] = [ + { + title: '序号', + dataIndex: 'index', + key: 'index', + width: 100, + align: 'center', + render(text, record, index) { + return {(pagination.current - 1) * pagination.pageSize + index + 1}; + }, + }, + { + 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) => ( +
+ + + + + + +
+ ), + }, + ]; + + return ( +
+ +
+
+ setInputText(e.target.value)} + style={{ width: 300 }} + value={inputText} + /> + +
+
+ + + + + ); +} + +export default ModelDeployment; diff --git a/react-ui/src/pages/Workspace/components/QuickStart/index.less b/react-ui/src/pages/Workspace/components/QuickStart/index.less index 4fba7728..5932d971 100644 --- a/react-ui/src/pages/Workspace/components/QuickStart/index.less +++ b/react-ui/src/pages/Workspace/components/QuickStart/index.less @@ -1,5 +1,5 @@ .quick-start { - width: calc(100% - 326px); + width: calc(100% - 326px - 15px); padding: 20px 30px; background-color: white; border-radius: 4px; diff --git a/react-ui/src/pages/Workspace/components/QuickStart/index.tsx b/react-ui/src/pages/Workspace/components/QuickStart/index.tsx index d7078a6c..c992c84a 100644 --- a/react-ui/src/pages/Workspace/components/QuickStart/index.tsx +++ b/react-ui/src/pages/Workspace/components/QuickStart/index.tsx @@ -29,9 +29,10 @@ function QuickStart() { } }; + changeScale(); + const debounceFunc = debounce(changeScale, 16); window.addEventListener('resize', debounceFunc); - changeScale(); return () => { window.removeEventListener('resize', debounceFunc); }; diff --git a/react-ui/src/utils/modal.tsx b/react-ui/src/utils/modal.tsx index 4a3b765f..577c1ec3 100644 --- a/react-ui/src/utils/modal.tsx +++ b/react-ui/src/utils/modal.tsx @@ -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 = ( ); root.render( - + {global.holderRender ? global.holderRender(dom) : dom} , );