Pipeline/frontend/src/components/dashboard/RecentActivitySection.tsx

122 lines
4.5 KiB
TypeScript

import { type KeyboardEvent, type MouseEvent } from "react";
import { Shield } from "lucide-react";
import { DashboardSection } from "./DashboardSection";
import { DashboardEmptyState } from "./DashboardEmptyState";
import { Markdown } from "@/components/atoms/Markdown";
import { cn } from "@/lib/utils";
import type { ActivityEventRead } from "@/api/generated/model";
export type ActivityEvent = ActivityEventRead;
interface RecentActivitySectionProps {
events: ActivityEvent[];
feedHref: string;
onRowClick: (e: MouseEvent<HTMLDivElement>, href: string) => void;
onRowKeyDown: (e: KeyboardEvent<HTMLDivElement>, href: string) => void;
buildHref: (event: ActivityEvent) => string;
formatRelative: (ts: string) => string;
formatTimestamp: (ts: string) => string;
}
const eventTone = (eventType: string) => {
const normalized = eventType.toLowerCase();
if (normalized.includes("error") || normalized.includes("fail")) {
return {
rail: "border-l-[color:var(--danger)]",
dot: "bg-[color:var(--danger)]",
row: "hover:border-[color:rgba(248,113,113,0.35)] hover:bg-[color:rgba(248,113,113,0.08)]",
};
}
if (normalized.includes("approval") || normalized.includes("review")) {
return {
rail: "border-l-[color:var(--warning)]",
dot: "bg-[color:var(--warning)]",
row: "hover:border-[color:rgba(251,191,36,0.35)] hover:bg-[color:rgba(251,191,36,0.08)]",
};
}
if (normalized.includes("complete") || normalized.includes("sync")) {
return {
rail: "border-l-[color:var(--success)]",
dot: "bg-[color:var(--success)]",
row: "hover:border-[color:rgba(52,211,153,0.35)] hover:bg-[color:rgba(52,211,153,0.08)]",
};
}
return {
rail: "border-l-[color:var(--accent)]",
dot: "bg-[color:var(--accent)]",
row: "hover:border-[color:rgba(96,165,250,0.35)] hover:bg-[color:rgba(96,165,250,0.08)]",
};
};
export function RecentActivitySection({
events,
feedHref,
onRowClick,
onRowKeyDown,
buildHref,
formatRelative,
formatTimestamp,
}: RecentActivitySectionProps) {
return (
<DashboardSection
title="Recent Activity"
action={{ label: "Open feed", href: feedHref }}
tone="accent"
>
<div className="max-h-[310px] space-y-2 overflow-x-hidden overflow-y-auto pr-1">
{events.length > 0 ? (
events.map((event) => {
const href = buildHref(event);
const tone = eventTone(event.event_type);
return (
<div
key={event.id}
role="link"
tabIndex={0}
aria-label={`Open related context for ${event.event_type} activity`}
onClick={(e) => onRowClick(e, href)}
onKeyDown={(e) => onRowKeyDown(e, href)}
className={cn(
"cursor-pointer overflow-hidden rounded-lg border border-l-4 border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]",
tone.rail,
tone.row,
)}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 overflow-hidden">
<div className="break-words text-sm font-medium text-strong [&_ol]:mb-0 [&_p]:mb-0 [&_pre]:my-1 [&_pre]:max-w-full [&_pre]:overflow-x-auto [&_ul]:mb-0">
<Markdown
content={event.message?.trim() || event.event_type}
variant="comment"
/>
</div>
<p className="mt-0.5 text-xs uppercase tracking-wider text-muted">
<span
className={cn(
"mr-2 inline-block h-1.5 w-1.5 rounded-full",
tone.dot,
)}
/>
{event.event_type}
</p>
</div>
<div className="shrink-0 text-right text-[11px] text-muted">
<p>{formatRelative(event.created_at)}</p>
<p>{formatTimestamp(event.created_at)}</p>
</div>
</div>
</div>
);
})
) : (
<DashboardEmptyState
icon={<Shield className="h-5 w-5" />}
message="No activity yet"
sub="Activity appears here when events are emitted."
/>
)}
</div>
</DashboardSection>
);
}