203 lines
6.4 KiB
TypeScript
203 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Card } from "@/components/ui/card";
|
|
|
|
export type AlertSeverity = "critical" | "warning" | "info" | "success";
|
|
|
|
export interface AlertCardItem {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
severity: AlertSeverity;
|
|
timestamp?: string;
|
|
icon?: React.ReactNode;
|
|
actions?: React.ReactNode;
|
|
}
|
|
|
|
export interface AlertCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
alerts: AlertCardItem[];
|
|
title?: string;
|
|
onDismiss?: (id: string) => void;
|
|
maxVisible?: number;
|
|
emptyText?: string;
|
|
}
|
|
|
|
const alertSeverityConfig: Record<
|
|
AlertSeverity,
|
|
{ iconBg: string; iconColor: string; dot: string; label: string }
|
|
> = {
|
|
critical: {
|
|
iconBg: "bg-destructive/10",
|
|
iconColor: "text-destructive",
|
|
dot: "bg-destructive",
|
|
label: "Critical",
|
|
},
|
|
warning: {
|
|
iconBg: "bg-accent/10",
|
|
iconColor: "text-accent",
|
|
dot: "bg-accent",
|
|
label: "Warning",
|
|
},
|
|
info: {
|
|
iconBg: "bg-blue-50",
|
|
iconColor: "text-blue-500",
|
|
dot: "bg-blue-400",
|
|
label: "Info",
|
|
},
|
|
success: {
|
|
iconBg: "bg-primary/10",
|
|
iconColor: "text-primary",
|
|
dot: "bg-primary",
|
|
label: "OK",
|
|
},
|
|
};
|
|
|
|
const DefaultAlertIcon = ({ severity }: { severity: AlertSeverity }) => {
|
|
if (severity === "critical" || severity === "warning") {
|
|
return (
|
|
<svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
|
|
<path
|
|
d="M8 2L14.5 13H1.5L8 2Z"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
<path d="M8 7V9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
<circle cx="8" cy="11.5" r="0.75" fill="currentColor" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (severity === "info") {
|
|
return (
|
|
<svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
|
|
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
|
|
<path d="M8 7V11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
<circle cx="8" cy="5" r="0.75" fill="currentColor" />
|
|
</svg>
|
|
);
|
|
}
|
|
return (
|
|
<svg className="h-4 w-4" viewBox="0 0 16 16" fill="none">
|
|
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
|
|
<path d="M5 8L7 10L11 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
const AlertCard = React.forwardRef<HTMLDivElement, AlertCardProps>(
|
|
(
|
|
{
|
|
className,
|
|
alerts,
|
|
title = "Smart Alert",
|
|
onDismiss,
|
|
maxVisible,
|
|
emptyText = "暂无告警",
|
|
...props
|
|
},
|
|
ref
|
|
) => {
|
|
const visible = maxVisible ? alerts.slice(0, maxVisible) : alerts;
|
|
|
|
return (
|
|
<Card ref={ref} className={cn("overflow-hidden", className)} {...props}>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-5 py-3.5 border-b border-border">
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
|
{title}
|
|
</p>
|
|
{alerts.length > 0 && (
|
|
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive/10 px-1.5 text-[10px] font-bold text-destructive">
|
|
{alerts.length}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Alert list */}
|
|
<div className="divide-y divide-border/50">
|
|
{visible.length === 0 ? (
|
|
<div className="px-5 py-8 text-center text-sm text-muted-foreground">
|
|
{emptyText}
|
|
</div>
|
|
) : (
|
|
visible.map((alert) => {
|
|
const config = alertSeverityConfig[alert.severity];
|
|
return (
|
|
<div
|
|
key={alert.id}
|
|
className="group flex items-start gap-3 px-5 py-3.5 transition-colors duration-150 hover:bg-muted/30"
|
|
>
|
|
{/* Icon */}
|
|
<div
|
|
className={cn(
|
|
"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg",
|
|
config.iconBg,
|
|
config.iconColor
|
|
)}
|
|
>
|
|
{alert.icon ?? <DefaultAlertIcon severity={alert.severity} />}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="text-sm font-semibold text-foreground leading-snug truncate">
|
|
{alert.title}
|
|
</p>
|
|
{alert.timestamp && (
|
|
<span className="shrink-0 text-xs text-muted-foreground">{alert.timestamp}</span>
|
|
)}
|
|
</div>
|
|
{alert.description && (
|
|
<p className="mt-0.5 text-xs text-muted-foreground leading-snug">
|
|
{alert.description}
|
|
</p>
|
|
)}
|
|
{/* Actions */}
|
|
<div className="mt-2 flex items-center gap-2">
|
|
{alert.actions}
|
|
{onDismiss && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onDismiss(alert.id)}
|
|
className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground transition-colors duration-150"
|
|
>
|
|
Dismiss
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* severity dot */}
|
|
<div
|
|
className={cn(
|
|
"mt-2 h-1.5 w-1.5 shrink-0 rounded-full",
|
|
config.dot,
|
|
alert.severity === "critical" && "animate-pulse"
|
|
)}
|
|
/>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* overflow hint */}
|
|
{maxVisible && alerts.length > maxVisible && (
|
|
<div className="px-5 py-2.5 border-t border-border text-xs text-muted-foreground text-center">
|
|
还有 {alerts.length - maxVisible} 条告警
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
);
|
|
AlertCard.displayName = "AlertCard";
|
|
|
|
export { AlertCard };
|