import { useT } from "@agent-native/core/client/i18n"; import { useMemo } from "react"; import { useSearchParams } from "react-router"; import { SqlChart } from "@/components/dashboard/SqlChart"; import type { SqlPanel } from "@/pages/adhoc/sql-dashboard/types"; export function meta() { return [{ title: "Chart" }]; } const VALID_CHART_TYPES = new Set([ "line", "area", "bar", "metric", "table", "pie", ]); // Embed URLs accept external sources plus the restricted first-party analytics // source. They intentionally do not expose arbitrary app database querying. const VALID_SOURCES = new Set([ "bigquery", "ga4", "amplitude", "first-party", "demo", "prometheus", ]); function decodePanel(raw: string): SqlPanel | { error: string } { try { const normalized = raw.replace(/-/g, "+").replace(/_/g, "/"); const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4); const json = atob(padded); const parsed = JSON.parse(json) as unknown; if (!parsed || typeof parsed !== "object") { return { error: "Invalid panel payload" }; } const p = parsed as Record; if (typeof p.sql !== "string" || !p.sql.trim()) { return { error: "Panel is missing sql" }; } if (typeof p.source !== "string" || !VALID_SOURCES.has(p.source)) { return { error: "Panel source must be bigquery, ga4, amplitude, first-party, demo, or prometheus.", }; } if ( typeof p.chartType !== "string" || !VALID_CHART_TYPES.has(p.chartType) ) { return { error: "Panel chartType is not recognized" }; } return { id: typeof p.id === "string" ? p.id : "embed", title: typeof p.title === "string" ? p.title : "", sql: p.sql, source: p.source as SqlPanel["source"], chartType: p.chartType as SqlPanel["chartType"], width: typeof p.width === "number" && Number.isFinite(p.width) && p.width >= 1 ? Math.floor(p.width) : 1, config: (p.config && typeof p.config === "object" ? p.config : undefined) as SqlPanel["config"], }; } catch (e) { return { error: e instanceof Error ? e.message : "failedToDecodePanel" }; } } function ChartError({ message }: { message: string }) { const t = useT(); const displayMessage = message === "failedToDecodePanel" ? t("common.failedToDecodePanel") : message; return (
{t("common.chartUnavailable")}
{displayMessage}
); } export default function ChartRoute() { const [params] = useSearchParams(); const raw = params.get("panel"); const result = useMemo(() => { if (!raw) return { error: "Missing panel parameter" }; return decodePanel(raw); }, [raw]); if ("error" in result) { return ; } return (
{result.title && (
{result.title}
)}
); }