feat(dbprovider): added alert info when database is abnormal (#5881)

* temp

* upt

* test

* feat:Add alert info when database is abnormal

* temp

* upt

* test

* feat:Add alert info when database is abnormal

* fix:Added auth session
This commit is contained in:
asterRyan
2025-08-29 17:00:07 +08:00
committed by GitHub
parent 6f5d3e54c3
commit ca4ba29d1e
10 changed files with 287 additions and 74 deletions
@@ -103,6 +103,7 @@
"cancel": "Cancel",
"cannot_change_name": "Database name cannot be changed",
"change_log": "History",
"click_for_details": "Click for details",
"clipboard_read_failed": "Reading clipboard failed!",
"clipboard_unsupported": "Browser does not support Clipper API",
"collection": "Collection",
@@ -102,6 +102,7 @@
"cancel": "取消",
"cannot_change_name": "不允许修改数据库名称",
"change_log": "变更历史",
"click_for_details": "点击查看详情",
"clipboard_read_failed": "读取剪切板失败!",
"clipboard_unsupported": "浏览器不支持 Clipboard API",
"collection": "集合",
@@ -24,6 +24,19 @@ import { RequiredByKeys } from '@/utils/tools';
export const getMyDBList = () => GET<DBListItemType[]>('/api/getDBList');
// Database Alerts
export type DatabaseAlertItem = {
name: string;
status: string;
reason: string;
details: string;
};
export const getDatabaseAlerts = (namespace: string) =>
GET<DatabaseAlertItem[]>(`/api/proxy/get_db_alerts`, {
namespace
});
export const getDBByName = ({
name,
mock = false,
@@ -8,90 +8,147 @@ import {
ModalHeader,
ModalBody,
useDisclosure,
ModalCloseButton
ModalCloseButton,
IconButton,
Tooltip,
Text
} from '@chakra-ui/react';
import type { DBConditionItemType, DBStatusMapType } from '@/types/db';
import MyIcon from '../Icon';
import { useTranslation } from 'next-i18next';
import { formatPodTime } from '@/utils/tools';
import { I18nCommonKey } from '@/types/i18next';
import { DBStatusEnum } from '@/constants/db';
const DBStatusTag = ({
conditions = [],
status,
showBorder = false
showBorder = false,
alertReason,
alertDetails
}: {
conditions: DBConditionItemType[];
status: DBStatusMapType;
showBorder?: boolean;
alertReason?: string;
alertDetails?: string;
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const {
isOpen: isQuestionOpen,
onOpen: onQuestionOpen,
onClose: onQuestionClose
} = useDisclosure();
const label = t(status.label as I18nCommonKey);
// Check if status is not Running or Stopped
const shouldShowQuestionIcon =
status.value !== DBStatusEnum.Running && status.value !== DBStatusEnum.Stopped && alertReason;
return (
<>
<Flex
color={status.color}
backgroundColor={status.backgroundColor}
border={showBorder ? '1px solid' : 'none'}
borderColor={status.color}
py={2}
px={3}
borderRadius={'24px'}
fontSize={'xs'}
fontWeight={'bold'}
alignItems={'center'}
minW={'88px'}
maxW={'110px'}
whiteSpace={'nowrap'}
>
<Box w={'6px'} h={'6px'} borderRadius={'10px'} backgroundColor={status.dotColor}></Box>
<Box ml={2} flex={1}>
{label}
</Box>
<MyIcon ml={3} w={'16px'} name={'statusDetail'} cursor={'pointer'} onClick={onOpen} />
</Flex>
<Modal isOpen={isOpen} onClose={onClose} lockFocusAcrossFrames={false}>
<ModalOverlay />
<ModalContent minW={'520px'}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Box flex={1}>{label}</Box>
<ModalCloseButton top={'10px'} right={'10px'} />
</ModalHeader>
<ModalBody>
{conditions.map((item, i) => (
<Box
key={i}
pl={6}
pb={6}
ml={4}
borderLeft={`2px solid ${i === conditions.length - 1 ? 'transparent' : '#DCE7F1'}`}
position={'relative'}
_before={{
content: '""',
position: 'absolute',
left: '-6.5px',
w: '8px',
h: '8px',
borderRadius: '8px',
backgroundColor: '#fff',
border: '2px solid',
borderColor: item.status === 'False' ? '#D92D20' : '#039855'
}}
>
<Flex lineHeight={1} mb={2} alignItems={'center'}>
<Box fontWeight={'bold'}>
{item.reason},&ensp;Last Occur:{' '}
{formatPodTime(item.lastTransitionTime as unknown as Date)}
</Box>
</Flex>
<Box color={'blackAlpha.700'}>{item.message}</Box>
<Flex alignItems={'center'}>
<Flex
color={status.color}
backgroundColor={status.backgroundColor}
border={showBorder ? '1px solid' : 'none'}
borderColor={status.color}
py={2}
px={3}
borderRadius={'24px'}
fontSize={'xs'}
fontWeight={'bold'}
alignItems={'center'}
minW={'88px'}
maxW={'110px'}
whiteSpace={'nowrap'}
>
<Box w={'6px'} h={'6px'} borderRadius={'10px'} backgroundColor={status.dotColor}></Box>
<Box ml={2} flex={1}>
{label}
</Box>
<MyIcon ml={3} w={'16px'} name={'statusDetail'} cursor={'pointer'} onClick={onOpen} />
</Flex>
{shouldShowQuestionIcon && (
<Tooltip label={t('click_for_details')} placement="top">
<IconButton
ml={2}
size="xs"
variant="ghost"
aria-label="Question mark"
icon={<MyIcon name="help" w="14px" h="14px" color="#F04438" />}
onClick={onQuestionOpen}
color="#F04438"
backgroundColor="#FEF3F2"
w="14px"
h="14px"
minW="14px"
minH="14px"
/>
</Tooltip>
)}
{/* Status details modal */}
<Modal isOpen={isOpen} onClose={onClose} lockFocusAcrossFrames={false}>
<ModalOverlay />
<ModalContent minW={'520px'}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Box flex={1}>{label}</Box>
<ModalCloseButton top={'10px'} right={'10px'} />
</ModalHeader>
<ModalBody>
{conditions.map((item, i) => (
<Box
key={i}
pl={6}
pb={6}
ml={4}
borderLeft={`2px solid ${
i === conditions.length - 1 ? 'transparent' : '#DCE7F1'
}`}
position={'relative'}
_before={{
content: '""',
position: 'absolute',
left: '-6.5px',
w: '8px',
h: '8px',
borderRadius: '8px',
backgroundColor: '#fff',
border: '2px solid',
borderColor: item.status === 'False' ? '#D92D20' : '#039855'
}}
>
<Flex lineHeight={1} mb={2} alignItems={'center'}>
<Box fontWeight={'bold'}>
{item.reason},&ensp;Last Occur:{' '}
{formatPodTime(item.lastTransitionTime as unknown as Date)}
</Box>
</Flex>
<Box color={'blackAlpha.700'}>{item.message}</Box>
</Box>
))}
</ModalBody>
</ModalContent>
</Modal>
{/* Question mark modal */}
<Modal isOpen={isQuestionOpen} onClose={onQuestionClose} lockFocusAcrossFrames={false}>
<ModalOverlay />
<ModalContent minW={'520px'}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Box flex={1}>{alertReason || 'Status Details'}</Box>
<ModalCloseButton top={'10px'} right={'10px'} />
</ModalHeader>
<ModalBody>
<Box whiteSpace={'pre-wrap'} color={'gray.700'}>
{alertDetails || ''}
</Box>
))}
</ModalBody>
</ModalContent>
</Modal>
</ModalBody>
</ModalContent>
</Modal>
</Flex>
</>
);
};
@@ -0,0 +1,65 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/services/backend/response';
import { authSession } from '@/services/backend/auth';
import { ResponseCode, ResponseMessages } from '@/types/response';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
res.status(405).end();
return;
}
try {
const kubeconfig = await authSession(req);
if (!kubeconfig) {
return jsonRes(res, {
code: ResponseCode.UNAUTHORIZED,
message: ResponseMessages[ResponseCode.UNAUTHORIZED]
});
}
} catch (error) {
return jsonRes(res, {
code: ResponseCode.UNAUTHORIZED,
message: ResponseMessages[ResponseCode.UNAUTHORIZED]
});
}
const { namespace } = req.query;
const apiUrl = `${
process.env.DATABASE_ALERT_URL || 'http://database-alert.sealos.svc:8000'
}/v1/databases?namespace=${namespace}`;
try {
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(req.headers.authorization ? { Authorization: req.headers.authorization } : {}),
...(req.headers['auth_user']
? {
auth_user: Array.isArray(req.headers['auth_user'])
? req.headers['auth_user'][0]
: req.headers['auth_user']
}
: {}),
'Time-Zone': Array.isArray(req.headers['time-zone'])
? req.headers['time-zone'][0]
: req.headers['time-zone'] || 'Asia/Shanghai'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
jsonRes(res, data);
} catch (error) {
console.error('Database alerts proxy error:', error);
jsonRes(res, {
code: 500,
error: 'Failed to fetch database alerts',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
@@ -1,4 +1,10 @@
import { delDBServiceByName, pauseDBByName, restartDB, startDBByName } from '@/api/db';
import {
delDBServiceByName,
pauseDBByName,
restartDB,
startDBByName,
type DatabaseAlertItem
} from '@/api/db';
import DBStatusTag from '@/components/DBStatusTag';
import MyIcon from '@/components/Icon';
import { defaultDBDetail } from '@/constants/db';
@@ -35,12 +41,14 @@ const Header = ({
db = defaultDBDetail,
conn,
isLargeScreen = true,
setShowSlider
setShowSlider,
alerts = {}
}: {
db: DBDetailType;
conn: ConnectionInfo | null;
isLargeScreen: boolean;
setShowSlider: Dispatch<boolean>;
alerts: Record<string, DatabaseAlertItem>;
}) => {
const { t } = useTranslation();
const router = useRouter();
@@ -285,7 +293,12 @@ const Header = ({
{router.query.name || db.dbName}
</Box>
</Flex>
<DBStatusTag status={db.status} conditions={db.conditions} />
<DBStatusTag
status={db.status}
conditions={db.conditions}
alertReason={alerts[db.dbName]?.reason}
alertDetails={alerts[db.dbName]?.details}
/>
{/* {!isLargeScreen && (
<Box mx={4}>
<Button
@@ -10,7 +10,7 @@ import { track } from '@sealos/gtm';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import AppBaseInfo, { ConnectionInfo } from './components/AppBaseInfo';
import BackupTable, { type ComponentRef } from './components/BackupTable';
import Header from './components/Header';
@@ -23,6 +23,7 @@ import MyIcon from '@/components/Icon';
import { BackupSupportedDBTypeList } from '@/constants/db';
import DataImport from './components/DataImport';
import OperationLog from './components/OperationLog';
import { type DatabaseAlertItem } from '@/api/db';
enum TabEnum {
pod = 'pod',
@@ -127,11 +128,17 @@ const AppDetail = ({
const { Loading } = useLoading();
const { screenWidth } = useGlobalStore();
const isLargeScreen = useMemo(() => screenWidth > 1280, [screenWidth]);
const { dbDetail, loadDBDetail } = useDBStore();
const { dbDetail, loadDBDetail, alerts, loadAlerts } = useDBStore();
const [showSlider, setShowSlider] = useState(false);
const [podsCount, setPodsCount] = useState(0);
const [connInfo, setConnInfo] = useState<ConnectionInfo | null>(null);
// Load alerts data once when page loads
useQuery(['databaseAlertsDetail'], loadAlerts, {
staleTime: Infinity,
cacheTime: 5 * 60 * 1000
});
useQuery(
['loadDBDetail', 'intervalLoadPods', dbName],
() => loadDBDetail(dbName, router?.query?.guide === 'true'),
@@ -171,6 +178,7 @@ const AppDetail = ({
conn={connInfo}
setShowSlider={setShowSlider}
isLargeScreen={isLargeScreen}
alerts={alerts}
/>
</Box>
<Flex position={'relative'} flex={'1 0 0'} h={0} gap={'8px'} minH={'600px'}>
@@ -2,6 +2,7 @@ import { pauseDBByName, restartDB, startDBByName, getDBSecret } from '@/api/db';
import { BaseTable } from '@/components/BaseTable/baseTable';
import { CustomMenu } from '@/components/BaseTable/customMenu';
import DBStatusTag from '@/components/DBStatusTag';
import type { DatabaseAlertItem } from '@/api/db';
import MyIcon from '@/components/Icon';
import { DBStatusEnum, DBTypeList } from '@/constants/db';
import { applistDriverObj, startDriver } from '@/hooks/driver';
@@ -70,10 +71,12 @@ const DelModal = dynamic(() => import('@/pages/db/detail/components/DelModal'));
const DBList = ({
dbList = [],
refetchApps
refetchApps,
alerts = {}
}: {
dbList: DBListItemType[];
refetchApps: () => void;
alerts?: Record<string, DatabaseAlertItem>;
}) => {
const { t } = useTranslation();
const { setLoading } = useGlobalStore();
@@ -100,6 +103,10 @@ const DBList = ({
content: t('pause_hint')
});
useEffect(() => {
console.log('alerts', alerts);
}, []);
const handleRestartApp = useCallback(
async (db: DBListItemType) => {
try {
@@ -457,7 +464,12 @@ const DBList = ({
accessorKey: 'status',
header: () => t('status'),
cell: ({ row }) => (
<DBStatusTag conditions={row.original.conditions} status={row.original.status} />
<DBStatusTag
conditions={row.original.conditions}
status={row.original.status}
alertReason={alerts[row.original.name]?.reason}
alertDetails={alerts[row.original.name]?.details}
/>
)
},
{
@@ -6,13 +6,14 @@ import { useLoading } from '@/hooks/useLoading';
import { useEffect, useState } from 'react';
import { serviceSideProps } from '@/utils/i18n';
import Sidebar from '@/components/Sidebar';
import { Flex } from '@chakra-ui/react';
import { Box, Flex, Switch, Text } from '@chakra-ui/react';
import { useGuideStore } from '@/store/guide';
import { useRouter } from 'next/router';
import { track } from '@sealos/gtm';
import { type DatabaseAlertItem } from '@/api/db';
function Home() {
const { dbList, setDBList } = useDBStore();
const { dbList, setDBList, alerts, loadAlerts } = useDBStore();
const { Loading } = useLoading();
const [initialized, setInitialized] = useState(false);
const router = useRouter();
@@ -25,6 +26,12 @@ function Home() {
}
});
// Fetch database alerts once when page loads
useQuery(['databaseAlerts'], loadAlerts, {
staleTime: Infinity,
cacheTime: 5 * 60 * 1000
});
useEffect(() => {
if (router.isReady) {
const { action } = router.query as { action?: string };
@@ -44,7 +51,7 @@ function Home() {
{dbList.length === 0 && initialized ? (
<Empty />
) : (
<DBList dbList={dbList} refetchApps={refetch} />
<DBList dbList={dbList} refetchApps={refetch} alerts={alerts} />
)}
</Flex>
<Loading loading={!initialized} />
+38 -2
View File
@@ -3,8 +3,16 @@ import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { persist } from 'zustand/middleware';
import type { DBDetailType, DBListItemType, PodDetailType } from '@/types/db';
import { getMyDBList, getPodsByDBName, getDBByName, getMonitorData } from '@/api/db';
import {
getMyDBList,
getPodsByDBName,
getDBByName,
getMonitorData,
getDatabaseAlerts,
type DatabaseAlertItem
} from '@/api/db';
import { defaultDBDetail } from '@/constants/db';
import { getUserNamespace } from '@/utils/user';
type State = {
dbList: DBListItemType[];
@@ -16,6 +24,8 @@ type State = {
dataSourceIds: Record<string, number>;
setDataSourceId: (dbName: string, dataSourceId: number) => void;
getDataSourceId: (dbName: string) => number | undefined;
alerts: Record<string, DatabaseAlertItem>;
loadAlerts: () => Promise<Record<string, DatabaseAlertItem>>;
};
const getDiskOverflowStatus = async (dbName: string, dbType: string): Promise<boolean> => {
@@ -90,7 +100,33 @@ export const useDBStore = create<State>()(
set((state) => {
state.dataSourceIds[dbName] = dataSourceId;
}),
getDataSourceId: (dbName: string) => get().dataSourceIds[dbName]
getDataSourceId: (dbName: string) => get().dataSourceIds[dbName],
alerts: {},
loadAlerts: async () => {
try {
const namespace = getUserNamespace();
const alertsList = await getDatabaseAlerts(namespace);
const alertsMap = (alertsList || []).reduce(
(acc, cur) => {
acc[cur.name] = cur;
return acc;
},
{} as Record<string, DatabaseAlertItem>
);
set((state) => {
state.alerts = alertsMap;
});
return alertsMap;
} catch (error) {
console.error('Failed to load database alerts:', error);
const emptyAlerts = {} as Record<string, DatabaseAlertItem>;
set((state) => {
state.alerts = emptyAlerts;
});
return emptyAlerts;
}
}
})),
{
name: 'db-store',