mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-01 05:47:48 +02:00
feat(site): allow dismissing the active alerts banner and soften its red (#2478)
This commit is contained in:
@@ -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<string[]>("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 }) => {
|
||||
<AlertBannerSheet
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onDismiss={() => setDismissedIds(activeAlerts.map((alert) => alert.id))}
|
||||
className={className}
|
||||
title={
|
||||
alertCount === 1 ? (
|
||||
@@ -117,5 +137,5 @@ export const ActiveAlerts = ({ className }: { className?: string }) => {
|
||||
})}
|
||||
</AlertBannerSheet>
|
||||
)
|
||||
}, [alertsKey, systemCount, systems, open, className])
|
||||
}, [alertsKey, systemCount, systems, open, className, dismissedIds])
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<Alert variant="destructive" className={cn("flex items-center gap-3 py-3 max-sm:flex-wrap", className)}>
|
||||
@@ -42,7 +46,7 @@ export function AlertBannerSheet({
|
||||
<div className="min-w-0">
|
||||
<AlertTitle className="m-0">{title}</AlertTitle>
|
||||
{description && (
|
||||
<AlertDescription className="text-destructive/80 dark:text-red-500/80">{description}</AlertDescription>
|
||||
<AlertDescription className="text-destructive/80 dark:text-red-400/80">{description}</AlertDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,12 +54,24 @@ export function AlertBannerSheet({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 bg-transparent border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive dark:text-red-500 dark:hover:bg-destructive/15 dark:hover:text-red-500 max-sm:w-full"
|
||||
className="shrink-0 bg-transparent border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive dark:text-red-400 dark:hover:bg-destructive/15 dark:hover:text-red-400 max-sm:w-full max-sm:order-last"
|
||||
>
|
||||
{buttonLabel ?? <Trans>View details</Trans>}
|
||||
<ChevronRightIcon className="size-4 ms-1 -me-1" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
{onDismiss && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onDismiss}
|
||||
aria-label={t`Dismiss`}
|
||||
title={t`Dismiss`}
|
||||
className="size-8 shrink-0 -me-1.5 text-destructive/70 hover:bg-destructive/10 hover:text-destructive dark:text-red-400/70 dark:hover:bg-destructive/15 dark:hover:text-red-400"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</Alert>
|
||||
<SheetContent className="w-140 !max-w-full gap-0">
|
||||
<SheetHeader className="p-4 sm:p-6 pb-3 sm:pb-4 border-b">
|
||||
@@ -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"
|
||||
>
|
||||
<div className="rounded-md bg-destructive/10 p-2 text-destructive shrink-0">
|
||||
<div className="rounded-md bg-destructive/10 p-2 text-destructive dark:text-red-400 shrink-0">
|
||||
<Icon className="size-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -229,14 +229,14 @@ function getStorageValue(key: string, defaultValue: unknown, storageInterface: S
|
||||
/** Hook to sync value in local or session storage */
|
||||
export function useBrowserStorage<T>(key: string, defaultValue: T, storageInterface: Storage = localStorage) {
|
||||
key = `besz-${key}`
|
||||
const [value, setValue] = useState(() => {
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
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 */
|
||||
|
||||
Reference in New Issue
Block a user