734bd9529a
- Show notification.message in notification-item.tsx - Add Details toggle to expand metadata (exit_code, previous_status, etc.) - Add CSS styles for message and metadata display Quality gates: tsc --noEmit pass, 80/80 tests pass
108 lines
2.9 KiB
TypeScript
108 lines
2.9 KiB
TypeScript
import { useState } from "react";
|
|
import { Icon } from "../../icon";
|
|
import { formatRelativeTime } from "../../../utils/time";
|
|
import type { NotificationItem as NotificationItemType } from "../../../api/notifications";
|
|
|
|
export interface NotificationItemProps {
|
|
notification: NotificationItemType;
|
|
onMarkRead: (id: string) => void;
|
|
onDismiss: (id: string) => void;
|
|
}
|
|
|
|
import type { IconName } from "../../../utils/icons";
|
|
|
|
const severityIconMap: Record<string, IconName> = {
|
|
info: "info",
|
|
warning: "warning",
|
|
error: "error",
|
|
success: "success",
|
|
};
|
|
|
|
function formatMetadataValue(value: unknown): string {
|
|
if (value === null || value === undefined) return "—";
|
|
if (typeof value === "string") return value;
|
|
if (typeof value === "number") return String(value);
|
|
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
export function NotificationItem({
|
|
notification,
|
|
onMarkRead,
|
|
onDismiss,
|
|
}: NotificationItemProps) {
|
|
const isUnread = notification.read_at === null;
|
|
const iconName = severityIconMap[notification.severity] ?? "info";
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const hasDetails =
|
|
!!notification.message ||
|
|
Object.keys(notification.metadata || {}).length > 0;
|
|
|
|
return (
|
|
<li
|
|
role="listitem"
|
|
className={`notification-item ${isUnread ? "notification-item--unread" : "notification-item--read"}`}
|
|
>
|
|
<div className="notification-item-icon">
|
|
<Icon name={iconName} size="md" />
|
|
</div>
|
|
<div className="notification-item-content">
|
|
<div className="notification-item-title">{notification.title}</div>
|
|
{notification.message && (
|
|
<div className="notification-item-message">
|
|
{notification.message}
|
|
</div>
|
|
)}
|
|
<div className="notification-item-time">
|
|
{formatRelativeTime(notification.created_at)}
|
|
</div>
|
|
{expanded && notification.metadata && (
|
|
<dl className="notification-item-metadata">
|
|
{Object.entries(notification.metadata).map(([key, value]) => (
|
|
<div key={key} className="notification-metadata-row">
|
|
<dt>{key.replace(/_/g, " ")}</dt>
|
|
<dd>{formatMetadataValue(value)}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
)}
|
|
</div>
|
|
<div className="notification-item-actions">
|
|
{hasDetails && (
|
|
<button
|
|
type="button"
|
|
className="notification-item-action"
|
|
onClick={() => setExpanded(!expanded)}
|
|
aria-label={expanded ? "Hide details" : "Show details"}
|
|
>
|
|
{expanded ? "Less" : "Details"}
|
|
</button>
|
|
)}
|
|
{isUnread && (
|
|
<button
|
|
type="button"
|
|
className="notification-item-action"
|
|
onClick={() => onMarkRead(notification.id)}
|
|
aria-label="Mark read"
|
|
>
|
|
Mark read
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="notification-item-action"
|
|
onClick={() => onDismiss(notification.id)}
|
|
aria-label="Dismiss"
|
|
>
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|