Co-authored-by: Jason Woltje <[email protected]> Co-committed-by: Jason Woltje <[email protected]>
193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import type { ReactElement } from "react";
|
|
import { Card, SectionHeader, Badge } from "@mosaic/ui";
|
|
import type { RecentActivity } from "@/lib/api/dashboard";
|
|
|
|
type BadgeVariantType =
|
|
| "badge-amber"
|
|
| "badge-red"
|
|
| "badge-teal"
|
|
| "badge-blue"
|
|
| "badge-muted"
|
|
| "badge-purple"
|
|
| "badge-pulse";
|
|
|
|
interface ActivityDisplayItem {
|
|
id: string;
|
|
icon: string;
|
|
iconBg: string;
|
|
title: string;
|
|
highlight: string;
|
|
rest: string;
|
|
timestamp: string;
|
|
badge?:
|
|
| {
|
|
text: string;
|
|
variant: BadgeVariantType;
|
|
}
|
|
| undefined;
|
|
}
|
|
|
|
export interface ActivityFeedProps {
|
|
items?: RecentActivity[] | undefined;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Mapping helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function getIconForAction(action: string): { icon: string; iconBg: string } {
|
|
const lower = action.toLowerCase();
|
|
if (lower.includes("complet") || lower.includes("finish") || lower.includes("success")) {
|
|
return { icon: "\u2713", iconBg: "rgba(20,184,166,0.15)" };
|
|
}
|
|
if (lower.includes("fail") || lower.includes("error")) {
|
|
return { icon: "\u2717", iconBg: "rgba(229,72,77,0.15)" };
|
|
}
|
|
if (lower.includes("warn") || lower.includes("limit")) {
|
|
return { icon: "\u26A0", iconBg: "rgba(245,158,11,0.15)" };
|
|
}
|
|
if (lower.includes("start") || lower.includes("creat")) {
|
|
return { icon: "\u2191", iconBg: "rgba(47,128,255,0.15)" };
|
|
}
|
|
if (lower.includes("update") || lower.includes("modif")) {
|
|
return { icon: "\u21BB", iconBg: "rgba(139,92,246,0.15)" };
|
|
}
|
|
return { icon: "\u2022", iconBg: "rgba(100,116,139,0.15)" };
|
|
}
|
|
|
|
function getBadgeForAction(action: string): ActivityDisplayItem["badge"] {
|
|
const lower = action.toLowerCase();
|
|
if (lower.includes("fail") || lower.includes("error")) {
|
|
return { text: "error", variant: "badge-red" };
|
|
}
|
|
if (lower.includes("warn") || lower.includes("limit")) {
|
|
return { text: "warn", variant: "badge-amber" };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function formatRelativeTime(isoDate: string): string {
|
|
const now = Date.now();
|
|
const then = new Date(isoDate).getTime();
|
|
const diffMs = now - then;
|
|
|
|
if (Number.isNaN(diffMs) || diffMs < 0) return "just now";
|
|
|
|
const minutes = Math.floor(diffMs / 60_000);
|
|
if (minutes < 1) return "just now";
|
|
if (minutes < 60) return `${String(minutes)}m ago`;
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${String(hours)}h ago`;
|
|
|
|
const days = Math.floor(hours / 24);
|
|
return `${String(days)}d ago`;
|
|
}
|
|
|
|
function mapActivityToDisplay(activity: RecentActivity): ActivityDisplayItem {
|
|
const { icon, iconBg } = getIconForAction(activity.action);
|
|
return {
|
|
id: activity.id,
|
|
icon,
|
|
iconBg,
|
|
title: "",
|
|
highlight: activity.entityType,
|
|
rest: ` ${activity.action} (${activity.entityId})`,
|
|
timestamp: formatRelativeTime(activity.createdAt),
|
|
badge: getBadgeForAction(activity.action),
|
|
};
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Components */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
interface ActivityItemRowProps {
|
|
item: ActivityDisplayItem;
|
|
}
|
|
|
|
function ActivityItemRow({ item }: ActivityItemRowProps): ReactElement {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "flex-start",
|
|
gap: 12,
|
|
padding: "10px 0",
|
|
borderBottom: "1px solid var(--border)",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 6,
|
|
flexShrink: 0,
|
|
background: item.iconBg,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: "0.8rem",
|
|
color: "var(--text)",
|
|
}}
|
|
>
|
|
{item.icon}
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div
|
|
style={{
|
|
fontSize: "0.8rem",
|
|
color: "var(--text-2)",
|
|
lineHeight: 1.4,
|
|
}}
|
|
>
|
|
{item.title}
|
|
<strong style={{ color: "var(--text)" }}>{item.highlight}</strong>
|
|
{item.rest}
|
|
{item.badge !== undefined && (
|
|
<Badge variant={item.badge.variant} style={{ marginLeft: 6 }}>
|
|
{item.badge.text}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: "0.7rem",
|
|
fontFamily: "var(--mono)",
|
|
color: "var(--muted)",
|
|
marginTop: 2,
|
|
}}
|
|
>
|
|
{item.timestamp}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ActivityFeed({ items }: ActivityFeedProps): ReactElement {
|
|
const displayItems = items ? items.map(mapActivityToDisplay) : [];
|
|
|
|
return (
|
|
<Card>
|
|
<SectionHeader title="Activity Feed" subtitle="Recent agent events" />
|
|
<div>
|
|
{displayItems.length > 0 ? (
|
|
displayItems.map((item) => <ActivityItemRow key={item.id} item={item} />)
|
|
) : (
|
|
<div
|
|
style={{
|
|
padding: "24px 0",
|
|
textAlign: "center",
|
|
fontSize: "0.8rem",
|
|
color: "var(--muted)",
|
|
}}
|
|
>
|
|
No recent activity
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|