import { For, Show } from "solid-js"; import type { AuditLog, ProjectOverview, ProjectSummary } from "@primora/api-client"; import { IconStorage, IconKey, IconMembers, IconAudit, IconCollections, IconIntegrations, IconChevronRight, IconTerminal, } from "./Icons"; interface ProjectDashboardProps { project: ProjectSummary; overview?: ProjectOverview; recentAudit?: AuditLog[]; onNavigate: (view: string) => void; onAgentSetup?: () => void; } function formatBytes(bytes?: number): string { if (bytes === undefined || bytes === null) return "0 B"; if (bytes < 1024) return `${bytes} B`; const units = ["KB", "MB", "GB", "TB"]; let value = bytes; let unit = -1; do { value /= 1024; unit++; } while (value >= 1024 && unit < units.length - 1); return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`; } function formatRelative(value?: string | null): string { if (!value) return "never"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "never"; const diff = Date.now() - date.getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } function actionTone(action: string): string { if (action.includes("delet") || action.includes("revok") || action.includes("remov")) return "var(--error)"; if (action.includes("creat") || action.includes("upload") || action.includes("invit")) return "var(--success)"; return "var(--text-3)"; } export function ProjectDashboard(props: ProjectDashboardProps) { const overview = () => props.overview; const metrics = () => [ { label: "Members", icon: IconMembers, value: overview()?.member_count, sub: `${overview()?.pending_invitation_count ?? 0} pending`, view: "members", }, { label: "API keys", icon: IconKey, value: overview()?.active_api_key_count, sub: "active credentials", view: "settings", }, { label: "Objects", icon: IconStorage, value: overview()?.object_count, sub: `${overview()?.bucket_count ?? 0} buckets · ${formatBytes(overview()?.object_bytes_total)}`, view: "storage", }, { label: "Events 24h", icon: IconAudit, value: overview()?.audit_events_24h, sub: `last ${formatRelative(overview()?.last_audit_at)}`, view: "audit", }, ]; const resources = () => [ { title: "Storage", sub: `${overview()?.bucket_count ?? 0} buckets`, icon: IconStorage, view: "storage", }, { title: "Collections", sub: "JSON documents", icon: IconCollections, view: "collections", }, { title: "Credentials", sub: `${overview()?.active_api_key_count ?? 0} active keys`, icon: IconKey, view: "settings", }, { title: "Members", sub: `${overview()?.member_count ?? 0} in project`, icon: IconMembers, view: "members", }, { title: "Integrations", sub: `${overview()?.integration_count ?? 0} connectors · ${overview()?.webhook_count ?? 0} webhooks`, icon: IconIntegrations, view: "integrations", }, ]; const audit = () => (props.recentAudit ?? []).slice(0, 6); return (
{/* metric strip — hairline divided, mono numerals */}
{(m) => (
props.onNavigate(m.view)} role="button" tabIndex={0} onKeyDown={(e) => e.key === "Enter" && props.onNavigate(m.view)} >
{m.label}
} >
{m.value}
{m.sub}
)}
{/* recent activity — real audit feed */}

Recent activity

0} fallback={
No events yet — mutations against this project will appear here.
} >
{(event) => (
)}
{/* resources — hairline rows */}

Resources

{(r) => (
props.onNavigate(r.view)} role="button" tabIndex={0} onKeyDown={(e) => e.key === "Enter" && props.onNavigate(r.view)} >
{r.title}
{r.sub}
)}
); }