diff --git a/prisma/seed.dev.ts b/prisma/seed.dev.ts index eecb156..df6687d 100644 --- a/prisma/seed.dev.ts +++ b/prisma/seed.dev.ts @@ -250,7 +250,7 @@ async function main() { await prisma.breachRecord.upsert({ where: { employeeId_breachId: { employeeId: employee.id, breachId: breach.id } }, update: {}, - create: { employeeId: employee.id, breachId: breach.id, exposedData: r.data, detectedAt: new Date(r.date) }, + create: { employeeId: employee.id, breachId: breach.id, exposedData: r.data, artifacts: [], sources: ["HIBP"], detectedAt: new Date(r.date) }, }) } diff --git a/src/app/api/dashboard/detail/[kind]/[id]/route.ts b/src/app/api/dashboard/detail/[kind]/[id]/route.ts new file mode 100644 index 0000000..494806e --- /dev/null +++ b/src/app/api/dashboard/detail/[kind]/[id]/route.ts @@ -0,0 +1,202 @@ +import { NextResponse } from "next/server" +import { requireAuth } from "@/lib/apiAuth" +import { prisma } from "@/lib/prisma" + +// Rich detail for the dashboard drawer. Each widget element links to one of +// these entities; the drawer fetches the full record on click so it can show +// far more than the compact row carries. + +type Field = { label: string; value: string | number } +type Group = { heading?: string; fields: Field[] } +type DetailResponse = { + title: string + subtitle?: string + variant?: string + tags?: string[] + groups: Group[] +} + +const fmtDate = (d: Date | string) => + new Date(d).toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" }) + +const prettyList = (xs: string[]) => + xs.length ? xs.map((x) => x.replace(/_/g, " ")).join(", ") : "None" + +export async function GET( + _req: Request, + { params }: { params: Promise<{ kind: string; id: string }> } +) { + const { session, error } = await requireAuth() + if (error) return error + + const { kind, id } = await params + const companyId = session.user.companyId + + if (kind === "employee") { + const emp = await prisma.employee.findFirst({ + where: { id, companyId }, + include: { + breachRecords: { include: { breach: true }, orderBy: { detectedAt: "desc" } }, + alerts: { include: { breach: true }, orderBy: { createdAt: "desc" } }, + }, + }) + if (!emp) return NextResponse.json({ error: "Not found" }, { status: 404 }) + + const openAlerts = emp.alerts.filter((a) => a.status === "OPEN").length + const groups: Group[] = [ + { + fields: [ + { label: "Email", value: emp.email }, + { label: "Department", value: emp.department ?? "Unknown" }, + { label: "MFA", value: emp.mfaEnabled == null ? "Unknown" : emp.mfaEnabled ? "Enabled" : "Disabled" }, + { label: "Breaches", value: emp.breachRecords.length }, + { label: "Open alerts", value: openAlerts }, + ], + }, + ...emp.breachRecords.map((r) => ({ + heading: r.breach.name, + fields: [ + { label: "Breach date", value: fmtDate(r.breach.breachDate) }, + { label: "Detected", value: fmtDate(r.detectedAt) }, + { label: "Exposed data", value: prettyList(r.exposedData) }, + ...(r.artifacts.length ? [{ label: "Artifacts", value: prettyList(r.artifacts) }] : []), + { label: "Reported by", value: r.sources.length ? r.sources.join(", ") : r.breach.source }, + ...(r.malwareFamily ? [{ label: "Malware", value: r.malwareFamily }] : []), + ...(r.machineId ? [{ label: "Machine ID", value: r.machineId }] : []), + ...(r.capturedAt ? [{ label: "Captured", value: fmtDate(r.capturedAt) }] : []), + ], + })), + ] + + const empRemediations = await prisma.remediationAction.findMany({ + where: { employeeId: id, companyId }, + orderBy: { createdAt: "desc" }, + take: 20, + }) + if (empRemediations.length) { + groups.push({ + heading: "Remediation", + fields: empRemediations.map((a) => ({ + label: `${a.action} (${a.status})`, + value: fmtDate(a.createdAt), + })), + }) + } + + return NextResponse.json({ + title: `${emp.firstName} ${emp.lastName}`, + subtitle: emp.department ?? emp.email, + variant: openAlerts > 0 ? "critical" : emp.breachRecords.length > 0 ? "high" : "ok", + groups, + } satisfies DetailResponse) + } + + if (kind === "breach") { + const breach = await prisma.breach.findFirst({ + where: { id }, + include: { + records: { include: { employee: true }, orderBy: { detectedAt: "desc" } }, + }, + }) + // Scope: only expose breaches that hit this company's employees. + const own = breach?.records.filter((r) => r.employee.companyId === companyId) ?? [] + if (!breach || own.length === 0) return NextResponse.json({ error: "Not found" }, { status: 404 }) + + const groups: Group[] = [ + { + fields: [ + { label: "Source", value: breach.source }, + { label: "Breach date", value: fmtDate(breach.breachDate) }, + { label: "Affected employees", value: own.length }, + { label: "Data types", value: prettyList(breach.dataTypes) }, + ...(breach.description ? [{ label: "Description", value: breach.description }] : []), + ], + }, + ...own.slice(0, 50).map((r) => ({ + heading: `${r.employee.firstName} ${r.employee.lastName}`, + fields: [ + { label: "Department", value: r.employee.department ?? "Unknown" }, + { label: "Exposed data", value: prettyList(r.exposedData) }, + ...(r.artifacts.length ? [{ label: "Artifacts", value: prettyList(r.artifacts) }] : []), + { label: "Detected", value: fmtDate(r.detectedAt) }, + ], + })), + ] + + return NextResponse.json({ + title: breach.name, + subtitle: String(breach.source), + variant: "medium", + tags: breach.dataTypes.map((t) => t.replace(/_/g, " ")), + groups, + } satisfies DetailResponse) + } + + if (kind === "alert") { + const alert = await prisma.alert.findFirst({ + where: { id, companyId }, + include: { + employee: { include: { breachRecords: { include: { breach: true } } } }, + breach: true, + }, + }) + if (!alert) return NextResponse.json({ error: "Not found" }, { status: 404 }) + + const groups: Group[] = [ + { + fields: [ + { label: "Severity", value: alert.severity }, + { label: "Confidence", value: alert.confidence }, + { label: "Status", value: alert.status }, + { label: "Created", value: fmtDate(alert.createdAt) }, + { label: "Message", value: alert.message }, + ], + }, + ] + if (alert.employee) { + groups.push({ + heading: `${alert.employee.firstName} ${alert.employee.lastName}`, + fields: [ + { label: "Email", value: alert.employee.email }, + { label: "Department", value: alert.employee.department ?? "Unknown" }, + { label: "Total breaches", value: alert.employee.breachRecords.length }, + { label: "MFA", value: alert.employee.mfaEnabled == null ? "Unknown" : alert.employee.mfaEnabled ? "Enabled" : "Disabled" }, + ], + }) + } + if (alert.breach) { + groups.push({ + heading: alert.breach.name, + fields: [ + { label: "Source", value: alert.breach.source }, + { label: "Breach date", value: fmtDate(alert.breach.breachDate) }, + { label: "Data types", value: prettyList(alert.breach.dataTypes) }, + ], + }) + } + + const alertRemediations = await prisma.remediationAction.findMany({ + where: { alertId: id, companyId }, + orderBy: { createdAt: "desc" }, + take: 20, + }) + if (alertRemediations.length) { + groups.push({ + heading: "Remediation", + fields: alertRemediations.map((a) => ({ + label: `${a.action} (${a.status})`, + value: fmtDate(a.createdAt), + })), + }) + } + + return NextResponse.json({ + title: alert.employee ? `${alert.employee.firstName} ${alert.employee.lastName}` : "Alert", + subtitle: alert.breach?.name ?? String(alert.severity), + variant: String(alert.severity).toLowerCase(), + groups, + } satisfies DetailResponse) + } + + return NextResponse.json({ error: "Unknown detail kind" }, { status: 400 }) +} diff --git a/src/components/dashboard/AlertStatusBreakdown.tsx b/src/components/dashboard/AlertStatusBreakdown.tsx index 877e119..bb6dac0 100644 --- a/src/components/dashboard/AlertStatusBreakdown.tsx +++ b/src/components/dashboard/AlertStatusBreakdown.tsx @@ -34,10 +34,9 @@ export function AlertStatusBreakdown({ data }: { data: StatusCounts }) { const total = data.open + data.acknowledged + data.resolved return ( -
+

{title}

-

{total} total alerts

{total === 0 ? ( diff --git a/src/components/dashboard/AlertVelocity.tsx b/src/components/dashboard/AlertVelocity.tsx index 14fbd45..9082df6 100644 --- a/src/components/dashboard/AlertVelocity.tsx +++ b/src/components/dashboard/AlertVelocity.tsx @@ -29,11 +29,10 @@ export function AlertVelocity({ data }: { data: VelocityData[] }) { const isEmpty = total === 0 return ( -
+

{title}

-

Daily alerts — last 30 days

diff --git a/src/components/dashboard/AlertsByDepartment.tsx b/src/components/dashboard/AlertsByDepartment.tsx index 3cdeeaf..426bc35 100644 --- a/src/components/dashboard/AlertsByDepartment.tsx +++ b/src/components/dashboard/AlertsByDepartment.tsx @@ -27,10 +27,9 @@ export function AlertsByDepartment({ data }: { data: DeptData[] }) { const isEmpty = data.length === 0 || data.every((d) => d.total === 0) return ( -
+

{title}

-

Alert volume per department

{isEmpty ? ( diff --git a/src/components/dashboard/AlertsByMonth.tsx b/src/components/dashboard/AlertsByMonth.tsx index effa9ee..1d6e4f2 100644 --- a/src/components/dashboard/AlertsByMonth.tsx +++ b/src/components/dashboard/AlertsByMonth.tsx @@ -37,11 +37,10 @@ export function AlertsByMonth({ data }: { data: MonthData[] }) { const isEmpty = sliced.every((d) => d.critical + d.high + d.medium + d.low === 0) return ( -
+

{title}

-

Alert volume — {period.label}

{editing && ( ))}
)} diff --git a/src/components/dashboard/BreachSourceDonut.tsx b/src/components/dashboard/BreachSourceDonut.tsx index ff21027..c111801 100644 --- a/src/components/dashboard/BreachSourceDonut.tsx +++ b/src/components/dashboard/BreachSourceDonut.tsx @@ -35,10 +35,9 @@ export function BreachSourceDonut({ data }: { data: BreachSource[] }) { const total = data.length return ( -
+

{title}

-

{total} breach{total !== 1 ? "es" : ""} by origin

{total === 0 ? ( diff --git a/src/components/dashboard/BreachSourcesList.tsx b/src/components/dashboard/BreachSourcesList.tsx index 31e983f..47e563a 100644 --- a/src/components/dashboard/BreachSourcesList.tsx +++ b/src/components/dashboard/BreachSourcesList.tsx @@ -1,6 +1,7 @@ "use client" import { useWidgetTitle } from "@/hooks/useWidgetTitle" +import { useDetailDrawer } from "@/contexts/DetailDrawerContext" import { cn } from "@/lib/utils" import { ShieldAlert, Globe, Eye } from "lucide-react" @@ -27,13 +28,13 @@ const SOURCE_COLORS: Record = { export function BreachSourcesList({ data }: { data: BreachSource[] }) { const { title } = useWidgetTitle("breach-sources", "Breach Sources") + const { openRef } = useDetailDrawer() return ( -
+

{title}

-

Sites that exposed your company data

@@ -45,9 +46,19 @@ export function BreachSourcesList({ data }: { data: BreachSource[] }) { ) : (
{data.map((breach) => ( -
+ openRef({ + kind: "breach", + id: breach.id, + title: breach.name, + subtitle: SOURCE_LABELS[breach.source] ?? breach.source, + variant: "medium", + }) + } + className="flex w-full items-start justify-between gap-3 rounded-lg border border-border bg-background p-3 text-left transition-colors hover:border-primary/40 hover:bg-muted" >
@@ -82,7 +93,7 @@ export function BreachSourcesList({ data }: { data: BreachSource[] }) { emp.
-
+ ))}
)} diff --git a/src/components/dashboard/BreachTimeline.tsx b/src/components/dashboard/BreachTimeline.tsx index f1e2930..1a946ec 100644 --- a/src/components/dashboard/BreachTimeline.tsx +++ b/src/components/dashboard/BreachTimeline.tsx @@ -25,10 +25,9 @@ export function BreachTimeline({ data }: { data: BreachSource[] }) { const filtered = config.filter === "ALL" ? sorted : sorted.filter((b) => b.source === config.filter) return ( -
+

{title}

-

{filtered.length} breaches

{/* Source filter */} diff --git a/src/components/dashboard/CriticalAlertsList.tsx b/src/components/dashboard/CriticalAlertsList.tsx index e0ee45a..6e61599 100644 --- a/src/components/dashboard/CriticalAlertsList.tsx +++ b/src/components/dashboard/CriticalAlertsList.tsx @@ -2,6 +2,7 @@ import { useWidgetConfig } from "@/hooks/useWidgetConfig" import { useWidgetTitle } from "@/hooks/useWidgetTitle" +import { useDetailDrawer, type DetailVariant } from "@/contexts/DetailDrawerContext" import { cn } from "@/lib/utils" type UrgentAlert = { @@ -22,15 +23,15 @@ const SEV_BADGE_CLASSES: Record = { export function CriticalAlertsList({ data }: { data: UrgentAlert[] }) { const { title } = useWidgetTitle("critical-alerts", "Urgent Alerts") + const { openRef } = useDetailDrawer() const [config, setConfig] = useWidgetConfig<{ filter: Filter }>("critical-alerts", { filter: "ALL" }) const filtered = config.filter === "ALL" ? data : data.filter((a) => a.severity === config.filter) return ( -
+

{title}

-

{filtered.length} open — action required

@@ -57,9 +58,19 @@ export function CriticalAlertsList({ data }: { data: UrgentAlert[] }) { ) : (
{filtered.map((alert) => ( -
+ openRef({ + kind: "alert", + id: alert.id, + title: alert.employeeName ?? "Unknown employee", + subtitle: alert.breachName ?? undefined, + variant: alert.severity.toLowerCase() as DetailVariant, + }) + } + className="flex w-full items-start gap-2.5 rounded-lg border border-border bg-background px-3 py-2.5 text-left transition-colors hover:border-primary/40 hover:bg-muted" > {alert.severity} @@ -78,7 +89,7 @@ export function CriticalAlertsList({ data }: { data: UrgentAlert[] }) { {new Date(alert.createdAt).toLocaleDateString("en-US", { month: "short", day: "numeric" })} -
+ ))}
)} diff --git a/src/components/dashboard/DashboardCanvas.tsx b/src/components/dashboard/DashboardCanvas.tsx index f9c484c..b05ee6a 100644 --- a/src/components/dashboard/DashboardCanvas.tsx +++ b/src/components/dashboard/DashboardCanvas.tsx @@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { DashboardEditContext } from "@/contexts/DashboardEditContext" import { DashboardConfigContext } from "@/contexts/DashboardConfigContext" +import { DetailDrawerProvider } from "@/contexts/DetailDrawerContext" import { type GridItemLayout, type WidgetMeta, type DashboardPreset, SOURCE_FILTERABLE_WIDGETS } from "@/types/dashboard" export type SourceOption = { id: string; label: string } @@ -358,6 +359,7 @@ export function DashboardCanvas({ : false return ( +
@@ -575,5 +577,6 @@ export function DashboardCanvas({
+
) } diff --git a/src/components/dashboard/DataTypeBreakdown.tsx b/src/components/dashboard/DataTypeBreakdown.tsx index 10b4344..6b64649 100644 --- a/src/components/dashboard/DataTypeBreakdown.tsx +++ b/src/components/dashboard/DataTypeBreakdown.tsx @@ -6,6 +6,7 @@ import { useWidgetConfig } from "@/hooks/useWidgetConfig" import { PRESET_DATA_TYPES } from "@/lib/dataTypes" import { useDashboardEditing } from "@/contexts/DashboardEditContext" import { useWidgetTitle } from "@/hooks/useWidgetTitle" +import { useDetailDrawer } from "@/contexts/DetailDrawerContext" import { cn } from "@/lib/utils" type DataItem = { type: string; count: number; percentage: number } @@ -22,6 +23,7 @@ export function DataTypeBreakdown({ data }: DataTypeBreakdownProps) { const [showSettings, setShowSettings] = useState(false) const [newType, setNewType] = useState("") const { title } = useWidgetTitle("data-type-breakdown", "Exposed Data Types") + const { open } = useDetailDrawer() const toggle = (key: string) => { const already = config.trackedTypes.includes(key) @@ -57,11 +59,10 @@ export function DataTypeBreakdown({ data }: DataTypeBreakdownProps) { const customTypes = config.trackedTypes.filter((t) => !presetKeys.has(t as never)) return ( -
+

{title}

-

Distribution of compromised data categories

{editing &&
+ ))}
)} diff --git a/src/components/dashboard/DataTypeRadar.tsx b/src/components/dashboard/DataTypeRadar.tsx index 387bd3c..484f779 100644 --- a/src/components/dashboard/DataTypeRadar.tsx +++ b/src/components/dashboard/DataTypeRadar.tsx @@ -23,10 +23,9 @@ export function DataTypeRadar({ data }: { data: DataType[] }) { const isEmpty = data.length === 0 return ( -
+

{title}

-

Compromised data category spread

{isEmpty ? ( diff --git a/src/components/dashboard/DepartmentRisk.tsx b/src/components/dashboard/DepartmentRisk.tsx index 35bca84..7001855 100644 --- a/src/components/dashboard/DepartmentRisk.tsx +++ b/src/components/dashboard/DepartmentRisk.tsx @@ -2,18 +2,19 @@ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from "recharts" import { useWidgetTitle } from "@/hooks/useWidgetTitle" +import { useDetailDrawer, type DetailVariant } from "@/contexts/DetailDrawerContext" type DeptData = { department: string; total: number; compromised: number; percentage: number } export function DepartmentRisk({ data }: { data: DeptData[] }) { const { title } = useWidgetTitle("department-risk", "Department Exposure") + const { open } = useDetailDrawer() return ( -
+

{title}

-

Compromised employees by department

@@ -62,6 +63,23 @@ export function DepartmentRisk({ data }: { data: DeptData[] }) { {data.map((entry) => ( + open({ + title: entry.department, + subtitle: `${entry.percentage}% exposed`, + variant: (entry.percentage >= 50 + ? "critical" + : entry.percentage >= 25 + ? "high" + : "medium") as DetailVariant, + fields: [ + { label: "Compromised", value: entry.compromised }, + { label: "Total employees", value: entry.total }, + { label: "Exposure", value: `${entry.percentage}%` }, + ], + }) + } fill={ entry.percentage >= 50 ? "oklch(var(--severity-critical))" diff --git a/src/components/dashboard/EmployeeExposure.tsx b/src/components/dashboard/EmployeeExposure.tsx index a42b56d..3e9acdf 100644 --- a/src/components/dashboard/EmployeeExposure.tsx +++ b/src/components/dashboard/EmployeeExposure.tsx @@ -36,10 +36,9 @@ export function EmployeeExposure({ data, totalEmployees }: { data: ExposureLevel ] return ( -
+

{title}

-

{totalEmployees} employees total

diff --git a/src/components/dashboard/MfaCoverage.tsx b/src/components/dashboard/MfaCoverage.tsx index b793f2a..6cb8756 100644 --- a/src/components/dashboard/MfaCoverage.tsx +++ b/src/components/dashboard/MfaCoverage.tsx @@ -13,9 +13,6 @@ export type MfaCoverageData = { export function MfaCoverage({ data }: { data: MfaCoverageData }) { const { title } = useWidgetTitle("mfa-coverage", "MFA Coverage") - const known = data.enabled + data.disabled - const coverage = known > 0 ? Math.round((data.enabled / known) * 100) : 0 - const tiers = [ { label: "MFA on", count: data.enabled, icon: ShieldCheck, color: "oklch(var(--severity-low))" }, { label: "MFA off", count: data.disabled, icon: ShieldX, color: "oklch(var(--severity-critical))" }, @@ -23,10 +20,9 @@ export function MfaCoverage({ data }: { data: MfaCoverageData }) { ] return ( -
+

{title}

-

{coverage}% of known accounts enrolled

diff --git a/src/components/dashboard/RiskGauge.tsx b/src/components/dashboard/RiskGauge.tsx index 39043eb..848ee1a 100644 --- a/src/components/dashboard/RiskGauge.tsx +++ b/src/components/dashboard/RiskGauge.tsx @@ -56,10 +56,9 @@ export function RiskGauge({ riskScore }: { riskScore: number }) { }) return ( -
+

{title}

-

Company security score

diff --git a/src/components/dashboard/SeverityDonut.tsx b/src/components/dashboard/SeverityDonut.tsx index 1cc7457..d498e59 100644 --- a/src/components/dashboard/SeverityDonut.tsx +++ b/src/components/dashboard/SeverityDonut.tsx @@ -2,6 +2,7 @@ import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, Legend } from "recharts" import { useWidgetTitle } from "@/hooks/useWidgetTitle" +import { useDetailDrawer, type DetailVariant } from "@/contexts/DetailDrawerContext" type AlertSeverity = { critical: number; high: number; medium: number; low: number } @@ -14,6 +15,7 @@ const SEVERITY = [ export function SeverityDonut({ data }: { data: AlertSeverity }) { const { title } = useWidgetTitle("severity-donut", "Alert Severity") + const { open } = useDetailDrawer() const chartData = SEVERITY .map((s) => ({ name: s.label, value: data[s.key], color: s.color })) @@ -22,11 +24,10 @@ export function SeverityDonut({ data }: { data: AlertSeverity }) { const total = Object.values(data).reduce((a, b) => a + b, 0) return ( -
+

{title}

-

{total} open alerts

@@ -48,7 +49,26 @@ export function SeverityDonut({ data }: { data: AlertSeverity }) { dataKey="value" > {chartData.map((entry) => ( - + + open({ + title: `${entry.name} alerts`, + variant: entry.name.toLowerCase() as DetailVariant, + fields: [ + { label: "Count", value: entry.value }, + { + label: "Share", + value: total > 0 ? `${Math.round((entry.value / total) * 100)}%` : "0%", + }, + { label: "Total alerts", value: total }, + ], + }) + } + /> ))} label left, value right. + if (!Icon && !description) { + return ( +
+
+

{label}

+
+

{value}

+ {delta && } +
+
+
+ ) + } + + // Detailed layout (reports): label/value left, boxed icon right. return ( -
+

@@ -68,9 +64,11 @@ export function StatCard({ )} {delta && }

-
- -
+ {Icon && ( +
+ +
+ )}
) diff --git a/src/components/dashboard/StatsRow.tsx b/src/components/dashboard/StatsRow.tsx index 1354f1f..026fe75 100644 --- a/src/components/dashboard/StatsRow.tsx +++ b/src/components/dashboard/StatsRow.tsx @@ -4,7 +4,6 @@ import { useState, useEffect } from "react" import { Settings2, Check } from "lucide-react" import { StatCard } from "@/components/dashboard/StatCard" import { getRiskLevel } from "@/lib/risk" -import { Users, Bell, Database, ShieldAlert } from "lucide-react" import { useWidgetConfig } from "@/hooks/useWidgetConfig" import { useDashboardEditing } from "@/contexts/DashboardEditContext" import { useDashboardConfig } from "@/contexts/DashboardConfigContext" @@ -35,7 +34,6 @@ interface StatsRowProps { export function StatsRow({ compromisedEmployees, - totalEmployees, openAlerts, recentBreaches, riskScore, @@ -71,32 +69,24 @@ export function StatsRow({ key: "employees" as CardKey, label: "Employees at risk", value: compromisedEmployees, - description: `out of ${totalEmployees} monitored`, - icon: Users, variant: (compromisedEmployees > 0 ? "critical" : "ok") as "critical" | "ok", }, { key: "alerts" as CardKey, label: "Active alerts", value: openAlerts, - description: "requiring attention", - icon: Bell, variant: (openAlerts > 0 ? "high" : "ok") as "high" | "ok", }, { key: "detections" as CardKey, label: "New detections", value: recentBreaches, - description: "in the last 30 days", - icon: Database, variant: (recentBreaches > 0 ? "medium" : "ok") as "medium" | "ok", }, { key: "risk" as CardKey, label: "Risk score", value: `${riskScore} / 100`, - description: risk.label, - icon: ShieldAlert, variant: risk.variant, }, ] diff --git a/src/components/dashboard/TopBreaches.tsx b/src/components/dashboard/TopBreaches.tsx index 6411a3c..9dcf376 100644 --- a/src/components/dashboard/TopBreaches.tsx +++ b/src/components/dashboard/TopBreaches.tsx @@ -4,6 +4,7 @@ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, } from "recharts" import { useWidgetTitle } from "@/hooks/useWidgetTitle" +import { useDetailDrawer } from "@/contexts/DetailDrawerContext" type BreachSource = { id: string; name: string; source: string @@ -18,6 +19,7 @@ const SOURCE_COLOR: Record = { export function TopBreaches({ data }: { data: BreachSource[] }) { const { title } = useWidgetTitle("top-breaches", "Top Breaches by Impact") + const { openRef } = useDetailDrawer() const sorted = [...data] .sort((a, b) => b.affectedEmployees - a.affectedEmployees) @@ -27,10 +29,9 @@ export function TopBreaches({ data }: { data: BreachSource[] }) { const isEmpty = sorted.length === 0 return ( -
+

{title}

-

Ranked by affected employees

{isEmpty ? ( @@ -77,7 +78,20 @@ export function TopBreaches({ data }: { data: BreachSource[] }) { /> {sorted.map((entry) => ( - + + openRef({ + kind: "breach", + id: entry.id, + title: entry.name, + subtitle: entry.source, + variant: "medium", + }) + } + fill={SOURCE_COLOR[entry.source] ?? "oklch(var(--primary))"} + /> ))} diff --git a/src/components/dashboard/TopRiskyEmployees.tsx b/src/components/dashboard/TopRiskyEmployees.tsx index 6f08852..75fbbab 100644 --- a/src/components/dashboard/TopRiskyEmployees.tsx +++ b/src/components/dashboard/TopRiskyEmployees.tsx @@ -1,6 +1,7 @@ "use client" import { useWidgetTitle } from "@/hooks/useWidgetTitle" +import { useDetailDrawer } from "@/contexts/DetailDrawerContext" import { cn } from "@/lib/utils" import { ShieldAlert, AlertTriangle } from "lucide-react" @@ -33,13 +34,13 @@ const SCORE_BAR: Record = { export function TopRiskyEmployees({ data }: { data: Employee[] }) { const { title } = useWidgetTitle("top-risky-employees", "Top Employees at Risk") + const { openRef } = useDetailDrawer() return ( -
+

{title}

-

Employees to contact first

@@ -51,7 +52,19 @@ export function TopRiskyEmployees({ data }: { data: Employee[] }) { ) : (
{data.map((emp, i) => ( -
+
-
+ ))}
)} diff --git a/src/components/dashboard/TrendChart.tsx b/src/components/dashboard/TrendChart.tsx index f529858..f32a9e8 100644 --- a/src/components/dashboard/TrendChart.tsx +++ b/src/components/dashboard/TrendChart.tsx @@ -36,13 +36,10 @@ export function TrendChart({ data }: TrendChartProps) { const isEmpty = sliced.every((d) => d.count === 0) return ( -
+

{title}

-

- New detections — {selectedPeriod.label} -

{editing && ( +
+ +
+ {payload.tags && payload.tags.length > 0 && ( +
+ {payload.tags.map((t) => ( + + {t} + + ))} +
+ )} + + {payload.fields && } + + {payload.groups?.map((g, gi) => ( +
0 && "mt-5")}> + {g.heading && ( +

{g.heading}

+ )} + +
+ ))} + + {loading && (!payload.groups || payload.groups.length === 0) && !payload.fields && ( +

Loading details...

+ )} +
+ + )} + +
, + document.body + )} + + ) +} + +function FieldList({ fields }: { fields: DetailField[] }) { + return ( +
+ {fields.map((f, i) => ( +
+
+ {f.label} +
+
{f.value}
+
+ ))} +
+ ) +}