From 3906fada847ce7ac7fb74e9a2a78d48961afc2f0 Mon Sep 17 00:00:00 2001 From: vivektekwani021-bot Date: Fri, 31 Jul 2026 11:45:57 +0530 Subject: [PATCH] feat(ui): add ConfigHistory page to display audit logs --- apps/web-dashboard/src/App.jsx | 3 + .../src/components/Layout/Sidebar.jsx | 5 +- .../web-dashboard/src/pages/ConfigHistory.jsx | 428 ++++++++++++++++++ 3 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 apps/web-dashboard/src/pages/ConfigHistory.jsx diff --git a/apps/web-dashboard/src/App.jsx b/apps/web-dashboard/src/App.jsx index b066a4bf4..8a3bd1c99 100644 --- a/apps/web-dashboard/src/App.jsx +++ b/apps/web-dashboard/src/App.jsx @@ -34,6 +34,7 @@ import AdminProRequests from './pages/AdminProRequests'; import Onboarding from './pages/Onboarding'; import AdminMetrics from './pages/AdminMetrics'; import TeamMembers from './pages/TeamMembers'; +import ConfigHistory from './pages/ConfigHistory'; import { LayoutProvider } from './context/LayoutContext'; import { PlanProvider } from './context/PlanContext'; @@ -138,6 +139,8 @@ function AppContent() { } /> } /> + + } /> } /> diff --git a/apps/web-dashboard/src/components/Layout/Sidebar.jsx b/apps/web-dashboard/src/components/Layout/Sidebar.jsx index 325edfd12..0ea4b91c9 100644 --- a/apps/web-dashboard/src/components/Layout/Sidebar.jsx +++ b/apps/web-dashboard/src/components/Layout/Sidebar.jsx @@ -3,7 +3,7 @@ import { useAuth } from '../../context/AuthContext'; import { LayoutDashboard, Database, Shield, HardDrive, Settings, BarChart2, ArrowLeft, LogOut, X, Rocket, Webhook, Users, Mail, ChevronLeft, ChevronRight, - LayoutTemplate + LayoutTemplate, History } from 'lucide-react'; import ThemeToggle from '../ThemeToggle'; @@ -72,6 +72,9 @@ function Sidebar({ logo, isOpen, onClose, isCollapsed, onToggleCollapse }) { Team + + Audit Log + Settings diff --git a/apps/web-dashboard/src/pages/ConfigHistory.jsx b/apps/web-dashboard/src/pages/ConfigHistory.jsx new file mode 100644 index 000000000..6ec526caa --- /dev/null +++ b/apps/web-dashboard/src/pages/ConfigHistory.jsx @@ -0,0 +1,428 @@ +import { useState, useEffect, useCallback } from "react"; +import { useParams } from "react-router-dom"; +import api from "../utils/api"; +import toast from "react-hot-toast"; +import { + History, Shield, Globe, Key, Users, Database, Server, + ToggleLeft, ToggleRight, Mail, FileText, ChevronLeft, + ChevronRight, Filter, RefreshCw, AlertCircle +} from "lucide-react"; + +// ─── Category metadata ──────────────────────────────────────────────────────── + +const CATEGORIES = [ + { value: "", label: "All Categories" }, + { value: "project_info", label: "Project Info" }, + { value: "api_key", label: "API Key" }, + { value: "auth", label: "Authentication" }, + { value: "public_signup", label: "Public Signup" }, + { value: "auth_providers", label: "OAuth Providers" }, + { value: "allowed_domains", label: "Allowed Domains" }, + { value: "byod_db", label: "External Database" }, + { value: "byod_storage", label: "External Storage" }, + { value: "collection_schema","label": "Collection Schema" }, + { value: "collection_rls", label: "RLS Settings" }, + { value: "mail_template", label: "Mail Template" }, + { value: "resend", label: "Resend / Mail" }, + { value: "member", label: "Team Member" }, +]; + +const CATEGORY_META = { + project_info: { icon: FileText, color: "#7c8cf8" }, + api_key: { icon: Key, color: "#f59e0b" }, + auth: { icon: Shield, color: "#3ecf8e" }, + public_signup: { icon: ToggleRight,color: "#3ecf8e" }, + auth_providers: { icon: ToggleLeft, color: "#8b5cf6" }, + allowed_domains: { icon: Globe, color: "#06b6d4" }, + byod_db: { icon: Database, color: "#f97316" }, + byod_storage: { icon: Server, color: "#f97316" }, + collection_schema:{ icon: FileText, color: "#7c8cf8" }, + collection_rls: { icon: Shield, color: "#ec4899" }, + mail_template: { icon: Mail, color: "#3ecf8e" }, + resend: { icon: Mail, color: "#f59e0b" }, + member: { icon: Users, color: "#8b5cf6" }, +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function formatTime(iso) { + const d = new Date(iso); + return d.toLocaleString(undefined, { + month: "short", day: "numeric", + hour: "2-digit", minute: "2-digit", + }); +} + +function timeAgo(iso) { + const diff = Date.now() - new Date(iso).getTime(); + const m = Math.floor(diff / 60000); + if (m < 1) return "just now"; + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + return `${Math.floor(h / 24)}d ago`; +} + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +function CategoryBadge({ category }) { + const meta = CATEGORY_META[category] || { icon: History, color: "#6b7280" }; + const Icon = meta.icon; + const label = CATEGORIES.find(c => c.value === category)?.label || category; + return ( + + + {label} + + ); +} + +function DiffTable({ diff }) { + if (!diff || !Array.isArray(diff) || diff.length === 0) return null; + return ( +
+ + + + + + + + + {diff.map((d, i) => ( + + + + + ))} + +
FieldNew Value
{d.field} + {typeof d.to === "boolean" + ? {String(d.to)} + : Array.isArray(d.to) + ? d.to.join(", ") || empty + : d.to ?? } +
+
+ ); +} + +function LogRow({ log }) { + const [expanded, setExpanded] = useState(false); + const meta = CATEGORY_META[log.category] || { icon: History, color: "#6b7280" }; + const Icon = meta.icon; + + return ( +
log.diff && setExpanded(p => !p)} + onMouseEnter={e => e.currentTarget.style.background = "rgba(255,255,255,0.02)"} + onMouseLeave={e => e.currentTarget.style.background = "transparent"} + > +
+ {/* Icon dot */} +
+ +
+ +
+
+ + + {log.label} + +
+ +
+ + by {log.changedByEmail || "Unknown"} + + · + + {timeAgo(log.changedAt)} + +
+ + {expanded && } +
+ + {log.diff && log.diff.length > 0 && ( + + {expanded ? "▲" : "▼"} + + )} +
+
+ ); +} + +function Skeleton() { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+ ); +} + +// ─── Main Page ──────────────────────────────────────────────────────────────── + +export default function ConfigHistory() { + const { projectId } = useParams(); + + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const [pagination, setPagination] = useState({ total: 0, totalPages: 1 }); + const [category, setCategory] = useState(""); + const [refreshing, setRefreshing] = useState(false); + + const LIMIT = 20; + + const fetchLogs = useCallback(async (pg = page, cat = category, showRefresh = false) => { + if (showRefresh) setRefreshing(true); + else setLoading(true); + setError(null); + try { + const params = { page: pg, limit: LIMIT }; + if (cat) params.category = cat; + const res = await api.get(`/api/projects/${projectId}/config-logs`, { params }); + setLogs(res.data.data.logs); + setPagination(res.data.data.pagination); + } catch (err) { + const msg = err.response?.data?.message || "Failed to load config history"; + setError(msg); + toast.error(msg); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [projectId, page, category]); + + useEffect(() => { + let isMounted = true; + const loadInitialData = async () => { + setLoading(true); + try { + const params = { page: 1, limit: LIMIT }; + if (category) params.category = category; + const res = await api.get(`/api/projects/${projectId}/config-logs`, { params }); + if (isMounted) { + setLogs(res.data.data.logs); + setPagination(res.data.data.pagination); + } + } catch (err) { + if (isMounted) { + setError(err.response?.data?.message || "Failed to load config history"); + } + } finally { + if (isMounted) setLoading(false); + } + }; + loadInitialData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [category, projectId]); + + const handleCategoryChange = (e) => { + setCategory(e.target.value); + setPage(1); + }; + + const handlePageChange = (p) => { + setPage(p); + fetchLogs(p, category); + window.scrollTo({ top: 0, behavior: "smooth" }); + }; + + return ( +
+ + {/* Header */} +
+
+ +
+
+

+ Config History +

+

+ Audit trail of all project configuration changes +

+
+ + {/* Refresh button */} + +
+ + {/* Filter bar */} +
+ + Filter: + + + {pagination.total > 0 && ( + + {pagination.total} event{pagination.total !== 1 ? "s" : ""} + + )} +
+ + {/* Log list */} +
+ {loading ? ( + + ) : error ? ( +
+ +

{error}

+ +
+ ) : logs.length === 0 ? ( +
+ +

+ {category ? "No events for this category yet." : "No configuration changes recorded yet."} +

+
+ ) : ( + logs.map(log => ) + )} +
+ + {/* Pagination */} + {!loading && !error && pagination.totalPages > 1 && ( +
+ + + + Page {page} of {pagination.totalPages} + + + +
+ )} + + +
+ ); +}