122 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|