### What problem does this PR solve? #643 feat: display the version and backend service status on the page ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.6.0
| @@ -0,0 +1,24 @@ | |||
| <svg t="1716195941333" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7780" | |||
| width="200" height="200"> | |||
| <path | |||
| d="M1024 534.4c0-85.76-53.12-160.96-133.44-190.08 3.52-17.92 5.44-36.16 5.44-55.04C896 129.92 766.4 0 606.72 0c-93.12 0-179.84 44.8-234.24 120A153.632 153.632 0 0 0 278.4 87.68a153.632 153.632 0 0 0-144 207.04c-79.68 28.8-134.4 105.6-134.4 190.72 0 86.4 53.44 161.6 133.76 190.72-3.52 17.92-5.12 36.48-5.12 55.04 0 159.04 129.6 288.64 288.64 288.64 93.44 0 180.16-44.8 234.24-120.64a152 152 0 0 0 94.08 32.64 153.632 153.632 0 0 0 144-207.04c79.68-28.48 134.4-105.28 134.4-190.4" | |||
| fill="#FFFFFF" p-id="7781"></path> | |||
| <path | |||
| d="M402.56 439.36l224 102.08 225.92-198.08c3.2-16.32 4.8-32.64 4.8-49.6 0-139.52-113.28-252.8-252.8-252.8-83.52 0-161.28 40.96-208.32 109.76l-37.76 195.2 44.16 93.44z" | |||
| fill="#FFD00A" p-id="7782"></path> | |||
| <path | |||
| d="M170.56 676.48c-3.2 16.32-4.8 33.28-4.8 50.56 0 139.84 113.6 253.44 253.44 253.44 84.16 0 162.24-41.28 209.28-111.04l37.44-194.56-49.92-95.04-224.96-102.4-220.48 199.04z" | |||
| fill="#20B9AF" p-id="7783"></path> | |||
| <path | |||
| d="M169.28 288.96l153.6 36.16 33.6-174.72c-21.12-16-47.04-24.96-73.6-24.96-66.88 0-120.96 54.4-120.96 120.96 0 15.04 2.56 29.12 7.36 42.56" | |||
| fill="#EE5096" p-id="7784"></path> | |||
| <path | |||
| d="M155.84 325.44c-68.48 22.72-116.16 88.64-116.16 160.96 0 70.4 43.52 133.44 108.8 158.08l215.36-194.88-39.68-84.48-168.32-39.68z" | |||
| fill="#12A5DF" p-id="7785"></path> | |||
| <path | |||
| d="M667.84 869.44c21.12 16.32 46.72 24.96 73.28 24.96 66.88 0 120.96-54.4 120.96-120.96 0-14.72-2.56-28.8-7.36-42.24l-153.28-35.84-33.6 174.08z" | |||
| fill="#90C640" p-id="7786"></path> | |||
| <path | |||
| d="M699.2 655.36l168.96 39.36c68.48-22.72 116.48-88.32 116.48-160.96 0-70.4-43.52-133.12-109.12-158.08l-220.8 193.6 44.48 86.08z" | |||
| fill="#05799F" p-id="7787"></path> | |||
| </svg> | |||
| @@ -0,0 +1,10 @@ | |||
| <svg t="1716195854453" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5857" | |||
| width="200" height="200"> | |||
| <path | |||
| d="M638.855218 56.525807s99.268136 159.838523 132.357514 216.623262a2.523766 2.523766 0 0 1 0 2.944394 2.383557 2.383557 0 0 1-3.50523 0L596.231612 97.326693l42.623606-40.800886z" | |||
| fill="#dd113c" p-id="5858"></path> | |||
| <path | |||
| d="M346.518971 639.655999a588.878771 588.878771 0 0 1 116.654081-165.446893 597.291325 597.291325 0 0 1 58.32704-51.176369v126.188308L346.518971 639.655999zM245.568325 756.590498l275.931767-140.209231v321.079139l62.112689 80.760517v-434.648616l37.716283-19.489084a187.179324 187.179324 0 0 0 51.456788-296.121896L530.753901 119.479752a31.547077 31.547077 0 0 1 1.542302-44.446327 31.687286 31.687286 0 0 1 44.586535 1.542302l19.909711 20.750966 42.062769-40.941095c-50.335114-65.337502-112.167385-57.065157-147.64032-24.396407a90.575163 90.575163 0 0 0-3.925859 127.870819l143.574253 149.60325a128.151237 128.151237 0 0 1-28.041846 197.414597l-19.489083 10.095065V314.090164A649.589368 649.589368 0 0 0 245.568325 755.889452v0.701046z" | |||
| fill="#dd113c" p-id="5859"></path> | |||
| <path d="M583.612781 583.432097v65.617921l-62.112689 31.547077v-65.197293z" fill="#dd113c" p-id="5860"></path> | |||
| </svg> | |||
| @@ -0,0 +1,9 @@ | |||
| <svg t="1716195691568" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4834" | |||
| width="200" height="200"> | |||
| <path | |||
| d="M1001.632 793.792c-7.84-13.856-26.016-37.536-93.12-83.2a1096.224 1096.224 0 0 0-125.152-74.144c-30.592-82.784-89.824-190.112-176.256-319.36-93.056-139.168-201.12-197.792-321.888-174.56a756.608 756.608 0 0 0-40.928-37.696C213.824 78.688 139.2 56.48 96.32 60.736c-19.424 1.952-34.016 9.056-43.36 21.088-21.664 27.904-14.432 68.064 85.504 198.912 19.008 55.616 23.072 84.672 23.072 99.296 0 30.912 15.968 66.368 49.984 110.752l-32 109.504c-28.544 97.792 23.328 224.288 71.616 268.384 25.76 23.552 47.456 20.032 58.176 15.84 21.504-8.448 38.848-29.472 50.048-89.504 5.728 14.112 11.808 29.312 18.208 45.6 34.56 87.744 68.352 136.288 106.336 152.736a32.032 32.032 0 0 0 25.44-58.688c-9.408-4.096-35.328-23.712-72.288-117.504-31.168-79.136-53.856-132.064-69.376-161.856a32.224 32.224 0 0 0-35.328-16.48 32.032 32.032 0 0 0-25.024 29.92c-3.872 91.04-13.056 130.4-19.2 147.008-26.496-30.464-68.128-125.984-47.232-197.536 20.768-71.232 32.992-112.928 36.64-125.248a31.936 31.936 0 0 0-5.888-29.28c-41.664-51.168-46.176-75.584-46.176-83.712 0-29.472-9.248-70.4-28.288-125.152a31.104 31.104 0 0 0-4.768-8.896c-53.824-70.112-73.6-105.216-80.832-121.888 25.632 1.216 74.336 15.04 91.008 29.376a660.8 660.8 0 0 1 49.024 46.304c8 8.448 19.968 11.872 31.232 8.928 100.192-25.92 188.928 21.152 271.072 144 87.808 131.328 146.144 238.048 173.408 317.216a32 32 0 0 0 16.384 18.432 1004.544 1004.544 0 0 1 128.8 75.264c7.392 5.024 14.048 9.696 20.064 14.016h-98.848a32.032 32.032 0 0 0-24.352 52.736 3098.752 3098.752 0 0 0 97.856 110.464 32 32 0 1 0 46.56-43.872 2237.6 2237.6 0 0 1-50.08-55.328h110.08a32.032 32.032 0 0 0 27.84-47.776z" | |||
| p-id="4835"></path> | |||
| <path | |||
| d="M320 289.472c12.672 21.76 22.464 37.344 29.344 46.784 8.288 16.256 21.184 29.248 29.44 45.536l2.016-1.984c14.528-9.952 25.92-49.504 2.752-75.488-12.032-18.176-51.04-17.664-63.552-14.848z" | |||
| p-id="4836"></path> | |||
| </svg> | |||
| @@ -0,0 +1,6 @@ | |||
| <svg t="1716195575286" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3818" | |||
| width="200" height="200"> | |||
| <path | |||
| d="M959.744 602.16l0.256 0.064v101.952c0 10.24-10.752 21.44-35.072 35.84-22.976 13.696-91.968 47.616-163.328 82.624l-35.712 17.536c-65.088 32-126.016 62.208-149.184 76.032-52.8 31.36-82.048 31.104-123.712 8.32-41.6-22.72-305.28-144.256-352.704-170.176-23.744-12.992-36.224-23.936-36.224-34.24v-103.424c0.384 10.368 12.48 21.248 36.224 34.24C147.776 676.8 411.328 798.4 452.992 821.12c41.664 22.784 70.912 23.04 123.712-8.32 52.672-31.36 300.416-147.712 348.224-176.128 23.232-13.824 34.56-24.768 34.88-34.56l-0.064 0.064z m0-168.576h0.192v101.952c0 10.24-10.752 21.44-35.072 35.968-47.808 28.416-295.552 144.768-348.224 176.128-52.8 31.36-82.048 31.04-123.712 8.32-41.6-22.72-305.28-144.32-352.704-170.24C76.48 572.8 64 561.92 64 551.536v-103.424c0.384 10.24 12.48 21.248 36.224 34.176 47.488 25.92 311.04 147.52 352.704 170.24 41.664 22.72 70.912 23.04 123.712-8.32 52.672-31.36 300.416-147.712 348.224-176.192 23.168-13.824 34.56-24.704 34.88-34.432zM462.656 81.84c55.36-22.72 74.56-23.488 121.664-3.776 47.168 19.776 293.376 131.648 339.968 151.104 24 10.048 35.84 19.2 35.456 29.632H960v101.952c0 10.176-10.816 21.44-35.072 35.904C877.056 425.072 629.376 541.44 576.64 572.8c-52.736 31.36-81.984 31.104-123.648 8.32-41.664-22.656-305.28-144.32-352.768-170.24C76.544 397.936 64 387.056 64 376.688V273.28c-0.32-10.304 11.072-19.968 34.368-30.464 46.656-20.8 308.8-138.24 364.288-160.896v-0.064z m129.792 238.4l-207.552 36.352 144.832 68.608 62.72-104.96z m128.704-113.6l-135.936 61.44 122.688 55.36 13.376-5.952 122.752-55.424-122.88-55.424z m-392.32 13.44c-61.248 0-110.912 22.016-110.912 49.152 0 27.072 49.664 49.088 110.976 49.088s110.912-21.952 110.912-49.088-49.6-49.088-110.912-49.088l-0.064-0.064z m134.656-101.888l20.096 42.304-66.88 27.52 89.6 9.216 28.032 53.248 17.408-47.744 77.632-9.216-60.16-25.728 16-43.712-59.136 22.08-62.592-27.968z" | |||
| fill="#D82A1F" p-id="3819"></path> | |||
| </svg> | |||
| @@ -4,6 +4,7 @@ export enum UserSettingRouteKey { | |||
| Profile = 'profile', | |||
| Password = 'password', | |||
| Model = 'model', | |||
| System = 'system', | |||
| Team = 'team', | |||
| Logout = 'logout', | |||
| } | |||
| @@ -12,6 +13,7 @@ export const UserSettingRouteMap = { | |||
| [UserSettingRouteKey.Profile]: 'Profile', | |||
| [UserSettingRouteKey.Password]: 'Password', | |||
| [UserSettingRouteKey.Model]: 'Model Providers', | |||
| [UserSettingRouteKey.System]: 'System Version', | |||
| [UserSettingRouteKey.Team]: 'Team', | |||
| [UserSettingRouteKey.Logout]: 'Log out', | |||
| }; | |||
| @@ -1,7 +1,8 @@ | |||
| import { ITenantInfo } from '@/interfaces/database/knowledge'; | |||
| import { IUserInfo } from '@/interfaces/database/userSetting'; | |||
| import { ISystemStatus, IUserInfo } from '@/interfaces/database/userSetting'; | |||
| import userService from '@/services/userService'; | |||
| import authorizationUtil from '@/utils/authorizationUtil'; | |||
| import { useCallback, useEffect, useMemo } from 'react'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import { history, useDispatch, useSelector } from 'umi'; | |||
| export const useFetchUserInfo = () => { | |||
| @@ -92,3 +93,41 @@ export const useSaveSetting = () => { | |||
| return saveSetting; | |||
| }; | |||
| export const useFetchSystemVersion = () => { | |||
| const [version, setVersion] = useState(''); | |||
| const [loading, setLoading] = useState(false); | |||
| const fetchSystemVersion = useCallback(async () => { | |||
| setLoading(true); | |||
| const { data } = await userService.getSystemVersion(); | |||
| if (data.retcode === 0) { | |||
| setVersion(data.data); | |||
| setLoading(false); | |||
| } | |||
| }, []); | |||
| return { fetchSystemVersion, version, loading }; | |||
| }; | |||
| export const useFetchSystemStatus = () => { | |||
| const [systemStatus, setSystemStatus] = useState<ISystemStatus>( | |||
| {} as ISystemStatus, | |||
| ); | |||
| const [loading, setLoading] = useState(false); | |||
| const fetchSystemStatus = useCallback(async () => { | |||
| setLoading(true); | |||
| const { data } = await userService.getSystemStatus(); | |||
| if (data.retcode === 0) { | |||
| setSystemStatus(data.data); | |||
| setLoading(false); | |||
| } | |||
| }, []); | |||
| return { | |||
| systemStatus, | |||
| fetchSystemStatus, | |||
| loading, | |||
| }; | |||
| }; | |||
| @@ -19,3 +19,31 @@ export interface IUserInfo { | |||
| update_date: string; | |||
| update_time: number; | |||
| } | |||
| export interface ISystemStatus { | |||
| es: Es; | |||
| minio: Minio; | |||
| mysql: Minio; | |||
| redis: Redis; | |||
| } | |||
| interface Redis { | |||
| status: string; | |||
| elapsed: number; | |||
| error: string; | |||
| pending: number; | |||
| } | |||
| export interface Minio { | |||
| status: string; | |||
| elapsed: number; | |||
| error: string; | |||
| } | |||
| interface Es { | |||
| status: string; | |||
| elapsed: number; | |||
| error: string; | |||
| number_of_nodes: number; | |||
| active_shards: number; | |||
| } | |||
| @@ -393,6 +393,7 @@ export default { | |||
| model: 'Model Providers', | |||
| modelDescription: 'Set the model parameter and API Key here.', | |||
| team: 'Team', | |||
| system: 'System', | |||
| logout: 'Log out', | |||
| username: 'Username', | |||
| usernameMessage: 'Please input your username!', | |||
| @@ -364,6 +364,7 @@ export default { | |||
| modelDescription: '在此設置模型參數和 API Key。', | |||
| team: '團隊', | |||
| logout: '登出', | |||
| system: '系統', | |||
| username: '使用者名稱', | |||
| usernameMessage: '請輸入用戶名', | |||
| photo: '頭像', | |||
| @@ -380,6 +380,7 @@ export default { | |||
| model: '模型提供商', | |||
| modelDescription: '在此设置模型参数和 API Key。', | |||
| team: '团队', | |||
| system: '系统', | |||
| logout: '登出', | |||
| username: '用户名', | |||
| usernameMessage: '请输入用户名', | |||
| @@ -4,11 +4,13 @@ import { ReactComponent as PasswordIcon } from '@/assets/svg/password.svg'; | |||
| import { ReactComponent as ProfileIcon } from '@/assets/svg/profile.svg'; | |||
| import { ReactComponent as TeamIcon } from '@/assets/svg/team.svg'; | |||
| import { UserSettingRouteKey } from '@/constants/setting'; | |||
| import { MonitorOutlined } from '@ant-design/icons'; | |||
| export const UserSettingIconMap = { | |||
| [UserSettingRouteKey.Profile]: <ProfileIcon />, | |||
| [UserSettingRouteKey.Password]: <PasswordIcon />, | |||
| [UserSettingRouteKey.Model]: <ModelIcon />, | |||
| [UserSettingRouteKey.System]: <MonitorOutlined style={{ fontSize: 24 }} />, | |||
| [UserSettingRouteKey.Team]: <TeamIcon />, | |||
| [UserSettingRouteKey.Logout]: <LogoutIcon />, | |||
| }; | |||
| @@ -3,6 +3,8 @@ | |||
| .outletWrapper { | |||
| padding: 32px 32px 0; | |||
| height: 100%; | |||
| overflow-y: auto; | |||
| } | |||
| .itemDescription { | |||
| @@ -0,0 +1,20 @@ | |||
| .systemInfo { | |||
| width: 100%; | |||
| .title { | |||
| font-size: 20px; | |||
| font-weight: 600; | |||
| } | |||
| .text { | |||
| height: 26px; | |||
| line-height: 26px; | |||
| } | |||
| .badge { | |||
| :global(.ant-badge-status-dot) { | |||
| width: 10px; | |||
| height: 10px; | |||
| } | |||
| } | |||
| .error { | |||
| color: red; | |||
| } | |||
| } | |||
| @@ -0,0 +1,94 @@ | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { useFetchSystemStatus } from '@/hooks/userSettingHook'; | |||
| import { ISystemStatus, Minio } from '@/interfaces/database/userSetting'; | |||
| import { Badge, Card, Flex, Spin, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import lowerCase from 'lodash/lowerCase'; | |||
| import upperFirst from 'lodash/upperFirst'; | |||
| import { useEffect } from 'react'; | |||
| import { toFixed } from '@/utils/commonUtil'; | |||
| import styles from './index.less'; | |||
| const { Text } = Typography; | |||
| enum Status { | |||
| 'green' = 'success', | |||
| 'red' = 'error', | |||
| 'yellow' = 'warning', | |||
| } | |||
| const TitleMap = { | |||
| es: 'Elasticsearch', | |||
| minio: 'MinIO Object Storage', | |||
| redis: 'Redis', | |||
| mysql: 'Mysql', | |||
| }; | |||
| const SystemInfo = () => { | |||
| const { | |||
| systemStatus, | |||
| fetchSystemStatus, | |||
| loading: statusLoading, | |||
| } = useFetchSystemStatus(); | |||
| useEffect(() => { | |||
| fetchSystemStatus(); | |||
| }, [fetchSystemStatus]); | |||
| return ( | |||
| <section className={styles.systemInfo}> | |||
| <Spin spinning={statusLoading}> | |||
| <Flex gap={16} vertical> | |||
| {Object.keys(systemStatus).map((key) => { | |||
| const info = systemStatus[key as keyof ISystemStatus]; | |||
| return ( | |||
| <Card | |||
| type="inner" | |||
| title={ | |||
| <Flex align="center" gap={10}> | |||
| <SvgIcon name={key} width={26}></SvgIcon> | |||
| <span className={styles.title}> | |||
| {TitleMap[key as keyof typeof TitleMap]} | |||
| </span> | |||
| <Badge | |||
| className={styles.badge} | |||
| status={Status[info.status as keyof typeof Status]} | |||
| /> | |||
| </Flex> | |||
| } | |||
| key={key} | |||
| > | |||
| {Object.keys(info) | |||
| .filter((x) => x !== 'status') | |||
| .map((x) => { | |||
| return ( | |||
| <Flex | |||
| key={x} | |||
| align="center" | |||
| gap={16} | |||
| className={styles.text} | |||
| > | |||
| <b>{upperFirst(lowerCase(x))}:</b> | |||
| <Text | |||
| className={classNames({ | |||
| [styles.error]: x === 'error', | |||
| })} | |||
| > | |||
| {toFixed(info[x as keyof Minio]) as any} | |||
| {x === 'elapsed' && ' ms'} | |||
| </Text> | |||
| </Flex> | |||
| ); | |||
| })} | |||
| </Card> | |||
| ); | |||
| })} | |||
| </Flex> | |||
| </Spin> | |||
| </section> | |||
| ); | |||
| }; | |||
| export default SystemInfo; | |||
| @@ -1,3 +1,6 @@ | |||
| .sideBarWrapper { | |||
| padding-top: 32px; | |||
| .version { | |||
| color: rgb(17, 206, 17); | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import { useSecondPathName } from '@/hooks/routeHook'; | |||
| import type { MenuProps } from 'antd'; | |||
| import { Menu } from 'antd'; | |||
| import React, { useMemo } from 'react'; | |||
| import { Flex, Menu } from 'antd'; | |||
| import React, { useEffect, useMemo } from 'react'; | |||
| import { useNavigate } from 'umi'; | |||
| import { | |||
| UserSettingBaseKey, | |||
| @@ -10,7 +10,7 @@ import { | |||
| } from '../constants'; | |||
| import { useTranslate } from '@/hooks/commonHooks'; | |||
| import { useLogout } from '@/hooks/userSettingHook'; | |||
| import { useFetchSystemVersion, useLogout } from '@/hooks/userSettingHook'; | |||
| import styles from './index.less'; | |||
| type MenuItem = Required<MenuProps>['items'][number]; | |||
| @@ -20,6 +20,11 @@ const SideBar = () => { | |||
| const pathName = useSecondPathName(); | |||
| const logout = useLogout(); | |||
| const { t } = useTranslate('setting'); | |||
| const { version, fetchSystemVersion } = useFetchSystemVersion(); | |||
| useEffect(() => { | |||
| fetchSystemVersion(); | |||
| }, [fetchSystemVersion]); | |||
| function getItem( | |||
| label: string, | |||
| @@ -32,7 +37,14 @@ const SideBar = () => { | |||
| key, | |||
| icon, | |||
| children, | |||
| label: t(label), | |||
| label: ( | |||
| <Flex justify={'space-between'}> | |||
| {t(label)} | |||
| <span className={styles.version}> | |||
| {label === 'system' && version} | |||
| </span> | |||
| </Flex> | |||
| ), | |||
| type, | |||
| } as MenuItem; | |||
| } | |||
| @@ -78,6 +78,10 @@ const routes = [ | |||
| path: '/user-setting/team', | |||
| component: '@/pages/user-setting/setting-team', | |||
| }, | |||
| { | |||
| path: '/user-setting/system', | |||
| component: '@/pages/user-setting/setting-system', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -16,6 +16,8 @@ const { | |||
| set_tenant_info, | |||
| add_llm, | |||
| delete_llm, | |||
| getSystemStatus, | |||
| getSystemVersion, | |||
| } = api; | |||
| const methods = { | |||
| @@ -71,6 +73,14 @@ const methods = { | |||
| url: delete_llm, | |||
| method: 'post', | |||
| }, | |||
| getSystemStatus: { | |||
| url: getSystemStatus, | |||
| method: 'get', | |||
| }, | |||
| getSystemVersion: { | |||
| url: getSystemVersion, | |||
| method: 'get', | |||
| }, | |||
| } as const; | |||
| const userService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -77,4 +77,8 @@ export default { | |||
| createFolder: `${api_host}/file/create`, | |||
| connectFileToKnowledge: `${api_host}/file2document/convert`, | |||
| getFile: `${api_host}/file/get`, | |||
| // system | |||
| getSystemVersion: `${api_host}/system/version`, | |||
| getSystemStatus: `${api_host}/system/status`, | |||
| }; | |||
| @@ -65,3 +65,10 @@ export const filterOptionsByInput = ( | |||
| input: string, | |||
| option: { label: string; value: string } | undefined, | |||
| ) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()); | |||
| export const toFixed = (value: unknown, fixed = 2) => { | |||
| if (typeof value === 'number') { | |||
| return value.toFixed(fixed); | |||
| } | |||
| return value; | |||
| }; | |||
| @@ -9,7 +9,7 @@ const registerServer = <T extends string>( | |||
| ) => { | |||
| const server: Service<T> = {} as Service<T>; | |||
| for (let key in opt) { | |||
| server[key] = (params: any, urlAppendix?: string) => { | |||
| server[key] = (params?: any, urlAppendix?: string) => { | |||
| let url = opt[key].url; | |||
| const requestOptions = opt[key]; | |||
| if (urlAppendix) { | |||