From 839943d9fde64daedb0b5f4646bba280029c4193 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 12 Feb 2026 13:33:50 +0100 Subject: [PATCH] Add navigation and smooth scrolling for alert toasts This update enables opening specific alerts via toast buttons, utilizing `useNavigate` to redirect and auto-expand the corresponding alert on the Alerts page. Includes enhancements for toast dismissal with animations and adds new styles for smooth transitions and better user interaction. --- frontend/src/App.jsx | 31 ++++++++++++++++++++++++----- frontend/src/pages/AlertsPage.jsx | 16 +++++++++++++++ frontend/src/state.jsx | 7 ++++++- frontend/src/styles.css | 33 +++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c2fde6c..3f9390b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { NavLink, Navigate, Route, Routes, useLocation } from "react-router-dom"; +import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "./state"; import { LoginPage } from "./pages/LoginPage"; import { DashboardPage } from "./pages/DashboardPage"; @@ -18,6 +18,7 @@ function Protected({ children }) { function Layout({ children }) { const { me, logout, uiMode, setUiMode, alertToasts, dismissAlertToast } = useAuth(); + const navigate = useNavigate(); const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`; return ( @@ -93,12 +94,32 @@ function Layout({ children }) { {children}
{alertToasts.map((toast) => ( -
+
{toast.severity === "alert" ? "New Alert" : "New Warning"} - +
+ + +
{toast.title}
{toast.target}
diff --git a/frontend/src/pages/AlertsPage.jsx b/frontend/src/pages/AlertsPage.jsx index dc55b6e..5efcd73 100644 --- a/frontend/src/pages/AlertsPage.jsx +++ b/frontend/src/pages/AlertsPage.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useState } from "react"; +import { useLocation } from "react-router-dom"; import { apiFetch } from "../api"; import { useAuth } from "../state"; @@ -75,6 +76,7 @@ function buildAlertSuggestions(item) { } export function AlertsPage() { + const location = useLocation(); const { tokens, refresh, me, alertStatus } = useAuth(); const [targets, setTargets] = useState([]); const [definitions, setDefinitions] = useState([]); @@ -206,6 +208,18 @@ export function AlertsPage() { setExpandedKey((prev) => (prev === key ? "" : key)); }; + useEffect(() => { + const params = new URLSearchParams(location.search); + const openKey = params.get("open"); + if (!openKey) return; + setExpandedKey(openKey); + const id = `alert-item-${openKey.replace(/[^a-zA-Z0-9_-]/g, "-")}`; + setTimeout(() => { + const el = document.getElementById(id); + if (el) el.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 120); + }, [location.search]); + if (loading) return
Loading alerts...
; return ( @@ -237,6 +251,7 @@ export function AlertsPage() {
toggleExpanded(item.alert_key)} role="button" tabIndex={0} @@ -287,6 +302,7 @@ export function AlertsPage() {
toggleExpanded(item.alert_key)} role="button" tabIndex={0} diff --git a/frontend/src/state.jsx b/frontend/src/state.jsx index 4279b60..1b0e568 100644 --- a/frontend/src/state.jsx +++ b/frontend/src/state.jsx @@ -95,7 +95,10 @@ export function AuthProvider({ children }) { }; const dismissAlertToast = (toastId) => { - setAlertToasts((prev) => prev.filter((t) => t.id !== toastId)); + setAlertToasts((prev) => prev.map((t) => (t.id === toastId ? { ...t, closing: true } : t))); + setTimeout(() => { + setAlertToasts((prev) => prev.filter((t) => t.id !== toastId)); + }, 220); }; useEffect(() => { @@ -114,10 +117,12 @@ export function AuthProvider({ children }) { const createdAt = Date.now(); const nextToasts = items.slice(0, 4).map((item, idx) => ({ id: `${createdAt}-${idx}-${item.alert_key}`, + alertKey: item.alert_key, severity: item.severity, title: item.name, target: item.target_name, message: item.message, + closing: false, })); setAlertToasts((prev) => [...nextToasts, ...prev].slice(0, 6)); for (const toast of nextToasts) { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d08140b..350a1c5 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1000,6 +1000,10 @@ td { animation: toastIn 0.22s ease; } +.alert-toast.closing { + animation: toastOut 0.22s ease forwards; +} + .alert-toast.warning { border-color: #db9125; background: linear-gradient(180deg, #4a2d0f, #35210d); @@ -1023,6 +1027,24 @@ td { letter-spacing: 0.02em; } +.toast-actions { + display: flex; + align-items: center; + gap: 6px; +} + +.toast-view { + border: 1px solid #5d80b1; + background: #0f274b; + border-radius: 8px; + font-size: 11px; + line-height: 1; + padding: 4px 7px; + display: inline-flex; + align-items: center; + justify-content: center; +} + .toast-close { border: 1px solid #5d80b1; background: #0f274b; @@ -1181,6 +1203,17 @@ select:-webkit-autofill { } } +@keyframes toastOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(8px); + } +} + .query { max-width: 400px; white-space: nowrap;