54 lines
1.7 KiB
TypeScript
54 lines
1.7 KiB
TypeScript
"use client";
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { useSSE } from "@/lib/use-sse";
|
|
|
|
interface Toast {
|
|
id: number;
|
|
message: string;
|
|
type: "info" | "warning" | "error";
|
|
}
|
|
|
|
const ALERT_TYPES = ["container_unhealthy", "container_restarted", "drift_found"];
|
|
|
|
export function ToastProvider() {
|
|
const events = useSSE("/api/activity", 5);
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
const [seen, setSeen] = useState(new Set<string>());
|
|
|
|
const addToast = useCallback((message: string, type: Toast["type"]) => {
|
|
const id = Date.now();
|
|
setToasts(prev => [...prev, { id, message, type }]);
|
|
setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 5000);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
for (const event of events) {
|
|
const key = `${event.type}-${event.timestamp}`;
|
|
if (seen.has(key)) continue;
|
|
if (ALERT_TYPES.includes(event.type)) {
|
|
setSeen(prev => new Set(prev).add(key));
|
|
addToast(event.raw || `${event.type}: ${event.source}`,
|
|
event.type.includes("unhealthy") ? "error" : "warning");
|
|
}
|
|
}
|
|
}, [events, seen, addToast]);
|
|
|
|
if (toasts.length === 0) return null;
|
|
|
|
const colors = {
|
|
info: "border-blue-500/20 bg-blue-500/5 backdrop-blur-xl",
|
|
warning: "border-amber-500/20 bg-amber-500/5 backdrop-blur-xl",
|
|
error: "border-red-500/20 bg-red-500/5 backdrop-blur-xl",
|
|
};
|
|
|
|
return (
|
|
<div className="fixed bottom-4 right-4 z-50 space-y-2 max-w-sm">
|
|
{toasts.map(t => (
|
|
<div key={t.id} className={`rounded-lg border px-4 py-3 text-xs shadow-lg backdrop-blur-md animate-slide-in ${colors[t.type]}`}>
|
|
<p className="text-foreground">{t.message}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|