// frontend/components/AdminTabs/SessionsTab.jsx import React, { useEffect, useMemo, useState, useCallback } from "react"; import { apiUrl, safeFetchJSON } from "../../utils/api.js"; /** * Sessions tab — admin-level table view of all saved sessions with * search, sort, and delete actions. * * Best practices applied: * - Fetch all sessions on mount * - Client-side search (useMemo for filtered list) * - Confirmation dialog before delete * - Row hover effect * - Empty / loading / error states * - Relative timestamps ("2 hours ago") * - Click row to open in workspace view */ function formatRelativeTime(iso) { if (!iso) return "—"; try { const d = new Date(iso); const diff = Date.now() - d.getTime(); if (diff < 60_000) return "just now"; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`; if (diff < 2_592_000_000) return `${Math.floor(diff / 86_400_000)}d ago`; return d.toLocaleDateString(); } catch { return "—"; } } export default function SessionsTab({ onSelectSession, showToast }) { const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [query, setQuery] = useState(""); const [deletingId, setDeletingId] = useState(null); const fetchSessions = useCallback(async () => { setError(null); try { const data = await safeFetchJSON(apiUrl("/api/sessions"), { timeout: 10000 }); setSessions(Array.isArray(data.sessions) ? data.sessions : []); } catch (err) { setError(err?.message || "Failed to load sessions"); } finally { setLoading(false); } }, []); useEffect(() => { fetchSessions(); }, [fetchSessions]); const handleDelete = async (session) => { if ( !window.confirm( `Delete session "${session.name || session.id?.slice(0, 8)}"? This cannot be undone.` ) ) { return; } setDeletingId(session.id); try { const res = await fetch(apiUrl(`/api/sessions/${session.id}`), { method: "DELETE", }); if (!res.ok) { throw new Error(`Delete failed (${res.status})`); } showToast?.("Session deleted", session.name || session.id); // Optimistic removal setSessions((prev) => prev.filter((s) => s.id !== session.id)); } catch (err) { setError(err?.message || "Failed to delete session"); } finally { setDeletingId(null); } }; const filtered = useMemo(() => { if (!query.trim()) return sessions; const q = query.toLowerCase(); return sessions.filter((s) => { return ( (s.name || "").toLowerCase().includes(q) || (s.repo || "").toLowerCase().includes(q) || (s.branch || "").toLowerCase().includes(q) || (s.id || "").toLowerCase().includes(q) ); }); }, [sessions, query]); return (

Sessions

All saved chat sessions ({sessions.length} total {query ? `, ${filtered.length} matching` : ""}).

setQuery(e.target.value)} placeholder="Search sessions..." style={{ padding: "6px 10px", background: "#0d0e15", border: "1px solid #2a2b36", borderRadius: "4px", color: "#fff", fontSize: "12px", width: "220px", }} />
{/* Loading state */} {loading && (
Loading sessions...
)} {/* Error state */} {error && !loading && (
Error: {error}
)} {/* Empty state */} {!loading && !error && sessions.length === 0 && (
💬
No sessions yet
Start chatting with GitPilot to create your first session.
)} {/* Table */} {!loading && filtered.length > 0 && (
{filtered.map((s) => ( (e.currentTarget.style.background = "#22232e") } onMouseLeave={(e) => (e.currentTarget.style.background = "transparent") } onClick={() => onSelectSession?.(s)} > ))}
Name Repository Branch Messages Status Updated Actions
{s.name || (unnamed)}
{s.id?.slice(0, 12)}
{s.repo || } {s.branch || } {s.message_count ?? 0} {s.status || "unknown"} {formatRelativeTime(s.updated_at)}
)} {/* No matches for search */} {!loading && sessions.length > 0 && filtered.length === 0 && (
No sessions match "{query}"
)}
); } const thStyle = { padding: "10px 12px", textAlign: "left", fontSize: "11px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.5px", opacity: 0.7, }; const tdStyle = { padding: "10px 12px", verticalAlign: "middle", };