Files
headquarter/apps/web/src/components/features/notification/notification-item.tsx
T
Developer 734bd9529a feat: add error message and expandable metadata to notification items
- 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
2026-06-06 09:58:24 +00:00

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>
);
}