import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { History, ShieldCheck, SlidersHorizontal } from "lucide-react"; import { toast } from "sonner"; import { getWorkspaceLogDetail, listWorkspaceLogs, type WorkspaceLogDetail, type WorkspaceLogItem, } from "../api/logs"; import { fetchWorkspaceMemberships, type WorkspaceMembership } from "../api/workspaces"; import { LogDetailsPanel } from "../components/logs/LogDetailsPanel"; import { LogsFeed } from "../components/logs/LogsFeed"; import { LogsFilterBar, type LogsFilterDraft } from "../components/logs/LogsFilterBar"; import { useWorkspace } from "../context/WorkspaceContext"; import { useTranslation } from "../hooks/useTranslation"; import { readStringParam, updateQueryParams } from "../lib/queryParams"; import { canWorkspace, WORKSPACE_LOGS_VIEW } from "../lib/permissions"; const DEFAULT_FILTERS: LogsFilterDraft = { search: "", section: "", event: "", actor: "", from: "", to: "", ordering: "-timestamp", }; const DEFAULT_QUERY_FILTERS: Record = { search: DEFAULT_FILTERS.search, section: DEFAULT_FILTERS.section, event: DEFAULT_FILTERS.event, actor: DEFAULT_FILTERS.actor, from: DEFAULT_FILTERS.from, to: DEFAULT_FILTERS.to, ordering: DEFAULT_FILTERS.ordering, }; const PAGE_SIZE = 20; export default function Logs() { const { t, lang } = useTranslation(); const { activeWorkspace } = useWorkspace(); const [searchParams, setSearchParams] = useSearchParams(); const [memberships, setMemberships] = useState([]); const [logs, setLogs] = useState([]); const [totalLogs, setTotalLogs] = useState(0); const [isLoading, setIsLoading] = useState(false); const [isLoadingUsers, setIsLoadingUsers] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [selectedLogId, setSelectedLogId] = useState(null); const [selectedLog, setSelectedLog] = useState(null); const [isLoadingDetail, setIsLoadingDetail] = useState(false); const workspaceRole = activeWorkspace?.my_role; const canViewLogs = canWorkspace(workspaceRole, WORKSPACE_LOGS_VIEW); const isWorkspaceRoleResolved = Boolean(workspaceRole); const filters = useMemo( () => ({ search: readStringParam(searchParams, "search", DEFAULT_FILTERS.search), section: readStringParam(searchParams, "section", DEFAULT_FILTERS.section) as LogsFilterDraft["section"], event: readStringParam(searchParams, "event", DEFAULT_FILTERS.event) as LogsFilterDraft["event"], actor: readStringParam(searchParams, "actor", DEFAULT_FILTERS.actor), from: readStringParam(searchParams, "from", DEFAULT_FILTERS.from), to: readStringParam(searchParams, "to", DEFAULT_FILTERS.to), ordering: readStringParam(searchParams, "ordering", DEFAULT_FILTERS.ordering) as LogsFilterDraft["ordering"], }), [searchParams], ); useEffect(() => { setLogs([]); setTotalLogs(0); setSelectedLogId(null); setSelectedLog(null); }, [activeWorkspace?.id]); useEffect(() => { if (!activeWorkspace?.id || !isWorkspaceRoleResolved || !canViewLogs) { setMemberships([]); setIsLoadingUsers(false); return; } const loadUsers = async () => { setIsLoadingUsers(true); try { const response = await fetchWorkspaceMemberships({ workspace: activeWorkspace.id, limit: 200, offset: 0, }); setMemberships(response.results || []); } catch { setMemberships([]); toast.error(t.logs?.loadFiltersError || "Failed to load log filters."); } finally { setIsLoadingUsers(false); } }; void loadUsers(); }, [activeWorkspace?.id, canViewLogs, isWorkspaceRoleResolved, t.logs?.loadFiltersError]); useEffect(() => { if (!activeWorkspace?.id || !isWorkspaceRoleResolved || !canViewLogs) { setLogs([]); setTotalLogs(0); setIsLoading(false); return; } const loadLogs = async () => { setIsLoading(true); try { const response = await listWorkspaceLogs( { workspace: activeWorkspace.id, search: filters.search || undefined, section: filters.section || undefined, actor: filters.actor || undefined, event: filters.event || undefined, from: filters.from || undefined, to: filters.to || undefined, ordering: filters.ordering, }, { limit: PAGE_SIZE, offset: 0 }, ); setLogs(response.results || []); setTotalLogs(response.count || 0); if (selectedLogId && !(response.results || []).some((item) => item.id === selectedLogId)) { setSelectedLogId(null); setSelectedLog(null); } } catch { setLogs([]); setTotalLogs(0); toast.error(t.logs?.loadError || "Failed to load logs."); } finally { setIsLoading(false); } }; void loadLogs(); }, [ activeWorkspace?.id, canViewLogs, filters.actor, filters.event, filters.from, filters.ordering, filters.search, filters.section, filters.to, isWorkspaceRoleResolved, t.logs?.loadError, ]); const handleLoadMore = async () => { if (!activeWorkspace?.id || isLoadingMore || logs.length >= totalLogs) return; setIsLoadingMore(true); try { const response = await listWorkspaceLogs( { workspace: activeWorkspace.id, search: filters.search || undefined, section: filters.section || undefined, actor: filters.actor || undefined, event: filters.event || undefined, from: filters.from || undefined, to: filters.to || undefined, ordering: filters.ordering, }, { limit: PAGE_SIZE, offset: logs.length }, ); setLogs((current) => [...current, ...(response.results || [])]); setTotalLogs(response.count || 0); } catch { toast.error(t.logs?.loadError || "Failed to load logs."); } finally { setIsLoadingMore(false); } }; const handleOpenLog = async (id: number) => { setSelectedLogId(id); setIsLoadingDetail(true); try { const detail = await getWorkspaceLogDetail(id); setSelectedLog(detail); } catch { toast.error(t.logs?.loadDetailsError || "Failed to load log details."); setSelectedLog(null); setSelectedLogId(null); } finally { setIsLoadingDetail(false); } }; const activeFiltersCount = useMemo( () => [filters.search, filters.section, filters.event, filters.actor, filters.from, filters.to].filter(Boolean).length, [filters.actor, filters.event, filters.from, filters.search, filters.section, filters.to], ); const latestActivityLabel = useMemo(() => { if (!logs[0]?.timestamp) return "-"; return new Intl.DateTimeFormat(lang === "fa" ? "fa-IR-u-ca-persian" : "en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }).format(new Date(logs[0].timestamp)); }, [lang, logs]); const formatNumber = (value: number) => new Intl.NumberFormat(lang === "fa" ? "fa-IR" : "en-US").format(value); if (!activeWorkspace) { return (
{t.logs?.selectWorkspace || "Please select a workspace first."}
); } if (!isWorkspaceRoleResolved) { return (
{t.loading || "Loading..."}
); } if (!canViewLogs) { return (

{t.logs?.title || "Activity logs"}

{t.logs?.unauthorized || "Only owners and admins can access workspace activity logs."}

); } return (

{t.logs?.title || "Activity logs"}

{t.logs?.description?.(activeWorkspace.name) || `Review what has happened inside ${activeWorkspace.name}.`}

{t.logs?.totalLogs || "Total logs"}

{formatNumber(totalLogs)}

{t.logs?.activeFilters || "Active filters"}

{formatNumber(activeFiltersCount)}

{t.logs?.latestActivity || "Latest activity"}

{latestActivityLabel}

setSearchParams( (current) => updateQueryParams( current, { search: nextFilters.search, section: nextFilters.section, event: nextFilters.event, actor: nextFilters.actor, from: nextFilters.from, to: nextFilters.to, ordering: nextFilters.ordering, }, DEFAULT_QUERY_FILTERS, ), { replace: true }, ) } /> void handleOpenLog(id)} onLoadMore={() => void handleLoadMore()} /> { setSelectedLogId(null); setSelectedLog(null); }} />
); }