mirror of
https://github.com/labring/sealos.git
synced 2026-10-29 17:25:58 +08:00
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:
@@ -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}, 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}, 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} />
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user