import { useMemo, useState } from "react" import { RefreshCw, ShieldAlert } from "lucide-react" import { useCspReports, useCspSummary } from "../hooks/useCspReports" import { useObservabilityData } from "../hooks/useObservabilityData" import { formatRelativeTime } from "../lib/timeUtils" import { URLLowerHost } from "../lib/csp-url" import { Segmented } from "./common/Segmented" import { DetailField, SectionLabel } from "./common/DetailField" import { FilterBar, type FilterChip } from "./FilterBar" import { MasterDetail } from "./MasterDetail" import type { CspReport } from "../lib/types" type Window = "24h" | "7d" | "30d" | "all" const WINDOW_HOURS: Record = { "24h": 24, "7d": 24 * 7, "30d": 24 * 30, all: null } const dateTime = (iso: string) => new Date(iso).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", }) /** Neutral directive badge; tone only escalates for enforced blocks. */ const DirectiveBadge = ({ report }: { report: CspReport }) => { const directive = report.effectiveDirective || report.violatedDirective || "unknown" const enforced = report.disposition === "enforce" return ( {directive} ) } const StatCard = ({ label, value }: { label: string; value: string | number }) => (
{value} {label}
) const CspListItem = ({ report }: { report: CspReport }) => (
{URLLowerHost(report.blockedUrl ?? "") || report.blockedUrl || "(no blocked URL)"}
{report.site} {formatRelativeTime(Date.parse(report.timestamp))}
) const CspReportDetail = ({ report }: { report: CspReport }) => (
{report.disposition && ( {report.disposition} )} {report.reportType && ( {report.reportType} )} {dateTime(report.timestamp)}
Violation
{report.violatedDirective && report.violatedDirective !== report.effectiveDirective && ( )} {report.statusCode ? : null}
{report.sample && (
Script sample
          {report.sample}
        
)} {report.originalPolicy && (
Policy
          {report.originalPolicy}
        
)}
Context
) export function CspReportsVisualizer() { const [window, setWindow] = useState("7d") const [site, setSite] = useState(null) const [directive, setDirective] = useState(null) const { searchQuery, setSearchQuery } = useObservabilityData() const hours = WINDOW_HOURS[window] const { reports, loading, error, refresh } = useCspReports({ site: site ?? undefined, directive: directive ?? undefined, hours: hours ?? undefined }) const { summary } = useCspSummary(hours ?? 24 * 365) const filtered = useMemo(() => { const q = searchQuery.trim().toLowerCase() if (!q) return reports return reports.filter((r) => `${r.site} ${r.documentUrl} ${r.blockedUrl ?? ""} ${r.referrer ?? ""} ${r.sample ?? ""} ${r.effectiveDirective ?? ""}` .toLowerCase() .includes(q), ) }, [reports, searchQuery]) const selected = filtered[0] ?? null const chip = (id: string, filterKey: string, value: string, onRemove: () => void): FilterChip => ({ id, filterKey, value, onRemove }) const chips: FilterChip[] = [ ...(searchQuery ? [chip("query", "query", searchQuery, () => setSearchQuery(""))] : []), ...(site ? [chip("site", "site", site, () => setSite(null))] : []), ...(directive ? [chip("directive", "directive", directive, () => setDirective(null))] : []), ] const addFilterOptions = [ ...(summary?.bySite ?? []).filter((s) => s.name !== site).slice(0, 6).map((s) => ({ value: `site:${s.name}`, label: `site=${s.name} (${s.count})` })), ...(summary?.byDirective ?? []).filter((d) => d.name && d.name !== directive).slice(0, 8).map((d) => ({ value: `dir:${d.name}`, label: `directive=${d.name} (${d.count})` })), ] const onAddFilter = (value: string) => { if (value.startsWith("site:")) setSite(value.slice(5)) else if (value.startsWith("dir:")) setDirective(value.slice(4)) } const clearAll = () => { setSearchQuery("") setSite(null) setDirective(null) } const matchText = `Matching ${filtered.length}${summary ? ` of ${summary.total}` : ""} reports · ${chips.length} filter${chips.length === 1 ? "" : "s"}` if (error) { return (
) } if (loading && reports.length === 0) { return
Loading CSP reports…
} return (
0 ? addFilterOptions : undefined} onAddFilter={addFilterOptions.length > 0 ? onAddFilter : undefined} matchText={matchText} onClearAll={chips.length > 0 ? clearAll : undefined} /> {filtered.length === 0 ? (
) : ( r.id} selectedKey={selected?.id ?? null} onSelect={() => {}} label="CSP reports" storageKey="csp-reports" listSize={{ default: 380, min: 300, max: 560 }} listHeader={

CSP violations

} renderItem={(report) => } renderDetail={(report) => } /> )}
) } export default CspReportsVisualizer