diff --git a/internal/site/src/components/active-alerts.tsx b/internal/site/src/components/active-alerts.tsx index 41af5f35d..e00efd269 100644 --- a/internal/site/src/components/active-alerts.tsx +++ b/internal/site/src/components/active-alerts.tsx @@ -1,10 +1,11 @@ import { alertInfo } from "@/lib/alerts" import { $alerts, $allSystemsById } from "@/lib/stores" +import { useBrowserStorage } from "@/lib/utils" import type { AlertRecord } from "@/types" import { Plural, Trans } from "@lingui/react/macro" import { useStore } from "@nanostores/react" import { getPagePath } from "@nanostores/router" -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { AlertBannerSheet, AlertBannerSheetItem } from "./alert-banner-sheet" import { $router } from "./router" @@ -49,6 +50,9 @@ export const ActiveAlerts = ({ className }: { className?: string }) => { const alerts = useStore($alerts) const systems = useStore($allSystemsById) const [open, setOpen] = useState(false) + // ids of the alerts that were active when the banner was last dismissed. + // session storage because a retrigger while the page is closed keeps the same id. + const [dismissedIds, setDismissedIds] = useBrowserStorage("dismissedAlerts", [], sessionStorage) const { activeAlerts, systemCount, alertsKey } = useMemo(() => { const activeAlerts: AlertRecord[] = [] @@ -69,9 +73,24 @@ export const ActiveAlerts = ({ className }: { className?: string }) => { return { activeAlerts, systemCount: systemIds.size, alertsKey: alertsKey.join("") } }, [alerts]) + // forget dismissed alerts once they resolve so they show again if they retrigger. + // skipped while alerts are still loading so a reload doesn't clear the dismissal, + // and re-run once loaded in case they resolved while the page was closed. + const alertsLoaded = Object.keys(alerts).length > 0 + useEffect(() => { + if (!alertsLoaded) { + return + } + const activeIds = new Set(activeAlerts.map((alert) => alert.id)) + if (dismissedIds.some((id) => !activeIds.has(id))) { + setDismissedIds(dismissedIds.filter((id) => activeIds.has(id))) + } + }, [alertsKey, alertsLoaded]) + return useMemo(() => { const alertCount = activeAlerts.length - if (alertCount === 0) { + // stay hidden after dismissing until an alert triggers that wasn't active at the time + if (alertCount === 0 || activeAlerts.every((alert) => dismissedIds.includes(alert.id))) { return null } // name the alert directly in the banner when there is only one @@ -80,6 +99,7 @@ export const ActiveAlerts = ({ className }: { className?: string }) => { setDismissedIds(activeAlerts.map((alert) => alert.id))} className={className} title={ alertCount === 1 ? ( @@ -117,5 +137,5 @@ export const ActiveAlerts = ({ className }: { className?: string }) => { })} ) - }, [alertsKey, systemCount, systems, open, className]) + }, [alertsKey, systemCount, systems, open, className, dismissedIds]) } diff --git a/internal/site/src/components/alert-banner-sheet.tsx b/internal/site/src/components/alert-banner-sheet.tsx index ab066f954..a109fe973 100644 --- a/internal/site/src/components/alert-banner-sheet.tsx +++ b/internal/site/src/components/alert-banner-sheet.tsx @@ -1,5 +1,5 @@ -import { Trans } from "@lingui/react/macro" -import { ChevronRightIcon, type LucideIcon, TriangleAlertIcon } from "lucide-react" +import { Trans, useLingui } from "@lingui/react/macro" +import { ChevronRightIcon, type LucideIcon, TriangleAlertIcon, XIcon } from "lucide-react" import type { ReactNode } from "react" import { cn } from "@/lib/utils" import { Link } from "./router" @@ -20,6 +20,7 @@ export function AlertBannerSheet({ sheetTitle, sheetDescription, icon: Icon = TriangleAlertIcon, + onDismiss, className, children, }: { @@ -31,9 +32,12 @@ export function AlertBannerSheet({ sheetTitle: ReactNode sheetDescription?: ReactNode icon?: LucideIcon + /** Shows a dismiss button when provided */ + onDismiss?: () => void className?: string children: ReactNode }) { + const { t } = useLingui() return ( @@ -42,7 +46,7 @@ export function AlertBannerSheet({
{title} {description && ( - {description} + {description} )}
@@ -50,12 +54,24 @@ export function AlertBannerSheet({ + {onDismiss && ( + + )}
@@ -88,7 +104,7 @@ export function AlertBannerSheetItem({ onClick={onClick} className="group flex items-start gap-3 rounded-lg border p-3 transition-colors hover:bg-accent/60" > -
+
diff --git a/internal/site/src/components/ui/alert.tsx b/internal/site/src/components/ui/alert.tsx index 52e922370..2b187be5d 100644 --- a/internal/site/src/components/ui/alert.tsx +++ b/internal/site/src/components/ui/alert.tsx @@ -10,7 +10,7 @@ const alertVariants = cva( variant: { default: "bg-background text-foreground [&>svg]:text-foreground", destructive: - "bg-destructive/5 border-destructive/50 text-destructive dark:bg-destructive/6 dark:text-red-500 dark:border-destructive/70 [&>svg]:text-destructive", + "bg-destructive/5 border-destructive/50 text-destructive dark:bg-destructive/6 dark:text-red-400 dark:border-destructive/50 [&>svg]:text-destructive dark:[&>svg]:text-red-400", }, }, defaultVariants: { diff --git a/internal/site/src/lib/utils.ts b/internal/site/src/lib/utils.ts index 4860c7060..a85bed083 100644 --- a/internal/site/src/lib/utils.ts +++ b/internal/site/src/lib/utils.ts @@ -229,14 +229,14 @@ function getStorageValue(key: string, defaultValue: unknown, storageInterface: S /** Hook to sync value in local or session storage */ export function useBrowserStorage(key: string, defaultValue: T, storageInterface: Storage = localStorage) { key = `besz-${key}` - const [value, setValue] = useState(() => { + const [value, setValue] = useState(() => { return getStorageValue(key, defaultValue, storageInterface) }) useEffect(() => { storageInterface?.setItem(key, JSON.stringify(value)) }, [key, value]) - return [value, setValue] + return [value, setValue] as const } /** Format temperature to user's preferred unit */