import { IconDots, IconDownload, IconExternalLink, IconTable, } from "@tabler/icons-react"; import { requestAgentSidebarOpen } from "../../agent-sidebar-state.js"; import { appPath } from "../../api-path.js"; import { startAgentChatViewTransition } from "../../chat-view-transition.js"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../../components/ui/dropdown-menu.js"; import { downloadFile, toCSVTable } from "../../db-admin/export-utils.js"; import { cn } from "../../utils.js"; import type { DataTableWidget as DataTableWidgetData } from "./data-widget-types.js"; const SAFE_ACTION_PROTOCOLS = new Set(["http:", "https:", "mailto:"]); function formatCell(value: unknown): string { if (value === undefined || value === null || value === "") return "—"; if (typeof value === "boolean") return value ? "Yes" : "No"; if (typeof value === "number") return value.toLocaleString(); if (Array.isArray(value)) return value.map(formatCell).join(", "); if (typeof value === "object") return JSON.stringify(value); const text = String(value); const timestamp = Date.parse(text); if (Number.isFinite(timestamp) && /^\d{4}-\d{2}-\d{2}T/.test(text)) { return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }).format(new Date(timestamp)); } return text; } function normalizeActionHref(href: string): string | null { const trimmed = href.trim(); if (!trimmed) return null; if (trimmed.startsWith("/") && !trimmed.startsWith("//")) { return appPath(trimmed); } const base = typeof window === "undefined" ? "http://agent-native.local/" : window.location.href; try { const url = new URL(trimmed, base); if (!SAFE_ACTION_PROTOCOLS.has(url.protocol)) return null; if ( typeof window !== "undefined" && url.origin === window.location.origin ) { return appPath(`${url.pathname}${url.search}${url.hash}`); } return url.toString(); } catch { return null; } } function isSameAppHref(href: string): boolean { if (typeof window === "undefined") return false; try { const url = new URL(href, window.location.href); return ( url.origin === window.location.origin && url.pathname.startsWith("/") ); } catch { return false; } } function navigateSameAppHref(href: string): void { if (typeof window === "undefined") return; const url = new URL(href, window.location.href); startAgentChatViewTransition(() => { requestAgentSidebarOpen(); window.history.pushState( window.history.state, "", `${url.pathname}${url.search}${url.hash}`, ); window.dispatchEvent(new PopStateEvent("popstate")); }); } function slugifyFilenamePart(value: string | undefined): string { const slug = (value ?? "data-table") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return slug || "data-table"; } export function DataTableWidget({ table, action, }: { table: DataTableWidgetData; action?: { label: string; href: string }; }) { const rows = table.rows ?? []; const actionHref = action ? normalizeActionHref(action.href) : null; const canExportCsv = table.columns.length > 0; const exportCsv = () => { if (!canExportCsv) return; const stamp = new Date().toISOString().slice(0, 10); downloadFile( `${slugifyFilenamePart(table.title)}-${stamp}.csv`, "text/csv;charset=utf-8", toCSVTable( table.columns.map((column) => column.label), rows.map((row) => table.columns.map((column) => row[column.key])), ), ); }; return (
{table.title ?? "Data table"}
{typeof table.totalRows === "number" ? `${table.totalRows.toLocaleString()} row${table.totalRows === 1 ? "" : "s"}` : `${rows.length.toLocaleString()} row${rows.length === 1 ? "" : "s"}`} {table.truncated ? " sampled" : ""}
{action && actionHref ? ( { if ( event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !isSameAppHref(actionHref) ) { return; } event.preventDefault(); navigateSameAppHref(actionHref); }} className="inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2 py-1 text-[11px] font-medium text-foreground no-underline hover:bg-muted hover:no-underline" > {action.label} ) : null} Download CSV
{table.columns.map((column) => ( ))} {rows.length === 0 ? ( ) : ( rows.map((row, rowIndex) => ( {table.columns.map((column) => ( ))} )) )}
{column.label}
No rows
{formatCell(row[column.key])}
); }