diff --git a/frontend/src/components/AlertsBell.jsx b/frontend/src/components/AlertsBell.jsx
index 52dc993..4302632 100644
--- a/frontend/src/components/AlertsBell.jsx
+++ b/frontend/src/components/AlertsBell.jsx
@@ -1,3 +1,5 @@
+import { useState, useRef, useEffect, useLayoutEffect } from 'react';
+import { createPortal } from 'react-dom';
import { Link } from 'react-router-dom';
import { IconBell, IconAlertTriangle, IconServerOff, IconCpu, IconDeviceDesktop, IconDatabase } from '@tabler/icons-react';
import { useAlerts } from '../contexts/AlertsContext.jsx';
@@ -21,11 +23,182 @@ function AlertIcon({ type, severity }) {
return ;
}
+const PANEL_WIDTH = 320;
+const PANEL_MAX_HEIGHT = 320;
+const Z_INDEX = 1050;
+
+function AlertsPanelContent({ loading, refresh, alerts, onClose }) {
+ return (
+ <>
+
+ Оповещения
+
+
+
+ {alerts.length === 0 && !loading && (
+
+ Нет активных оповещений
+
+ )}
+ {alerts.slice(0, 20).map((alert) => (
+
+
+
+
+
+
+
{alert.title}
+
+ {alert.entity}
+
+
+
+
+ ))}
+ {alerts.length > 20 && (
+
+ и ещё {alerts.length - 20}…
+
+ )}
+
+
+
+ Настройки
+
+
+ Панель
+
+
+ >
+ );
+}
+
export default function AlertsBell({ dropup = false }) {
const { alerts, total, loading, refresh } = useAlerts();
+ const [open, setOpen] = useState(false);
+ const triggerRef = useRef(null);
+ const panelRef = useRef(null);
+ const [position, setPosition] = useState({ top: 0, left: 0 });
+
+ const updatePosition = () => {
+ const el = triggerRef.current;
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ const padding = 4;
+ const maxLeft = window.innerWidth - PANEL_WIDTH - padding;
+ if (dropup) {
+ setPosition({
+ left: Math.max(padding, Math.min(rect.left, maxLeft)),
+ bottom: window.innerHeight - rect.top + padding,
+ });
+ } else {
+ setPosition({
+ top: rect.bottom + padding,
+ left: Math.max(padding, Math.min(rect.left, maxLeft)),
+ });
+ }
+ };
+
+ useLayoutEffect(() => {
+ if (open) updatePosition();
+ }, [open, dropup]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onScroll = () => updatePosition();
+ const onResize = () => updatePosition();
+ const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
+ const onClickOutside = (e) => {
+ const panel = panelRef.current;
+ const trigger = triggerRef.current;
+ if (!panel || !trigger) return;
+ if (!panel.contains(e.target) && !trigger.contains(e.target)) setOpen(false);
+ };
+ window.addEventListener('scroll', onScroll, true);
+ window.addEventListener('resize', onResize);
+ document.addEventListener('keydown', onKey);
+ document.addEventListener('mousedown', onClickOutside);
+ return () => {
+ window.removeEventListener('scroll', onScroll, true);
+ window.removeEventListener('resize', onResize);
+ document.removeEventListener('keydown', onKey);
+ document.removeEventListener('mousedown', onClickOutside);
+ };
+ }, [open]);
+
+ const handleTriggerClick = (e) => {
+ e.preventDefault();
+ if (dropup) {
+ setOpen((v) => !v);
+ }
+ };
+
+ if (dropup) {
+ return (
+ <>
+
+ {open && typeof document !== 'undefined' && createPortal(
+ ,
+ document.body
+ )}
+ >
+ );
+ }
return (
-
+
-
-
- Оповещения
-
-
-
- {alerts.length === 0 && !loading && (
-
- Нет активных оповещений
-
- )}
- {alerts.slice(0, 20).map((alert) => (
-
-
-
-
-
-
-
{alert.title}
-
- {alert.entity}
-
-
-
-
- ))}
- {alerts.length > 20 && (
-
- и ещё {alerts.length - 20}…
-
- )}
-
-
-
- Настройки
-
-
- Панель
-
-
+
);