import type { OutputColumnMetadata } from "@internal/clickhouse"; import type { ChartBlock } from "@internal/dashboard-agent"; import { useEffect, useState } from "react"; import { QueryResultsChart } from "~/components/code/QueryResultsChart"; import type { ChartConfiguration } from "~/components/metrics/QueryWidget"; import { Spinner } from "~/components/primitives/Spinner"; import { useOptionalEnvironment } from "~/hooks/useEnvironment"; import { useOptionalOrganization } from "~/hooks/useOrganizations"; import { useOptionalProject } from "~/hooks/useProject"; // Render an agent "chart" block by running its TRQL query through the dashboard's // own /resources/metric endpoint (session-authed, returns rows + real column // metadata) and feeding the result into QueryResultsChart. So the chart is live // and matches the Query page exactly: the agent only emits the query + chart // config, never the rows. Runs against the project/env the panel is open in. type MetricResponse = | { success: false; error: string } | { success: true; data: { rows: Record[]; columns: OutputColumnMetadata[]; timeRange: { from: string; to: string }; }; }; type ChartState = | { status: "loading" } | { status: "error"; error: string } | { status: "ready"; rows: Record[]; columns: OutputColumnMetadata[]; timeRange?: { from: string; to: string }; }; export function AgentChart({ block }: { block: ChartBlock }) { const organization = useOptionalOrganization(); const project = useOptionalProject(); const environment = useOptionalEnvironment(); const [state, setState] = useState({ status: "loading" }); const organizationId = organization?.id; const projectId = project?.id; const environmentId = environment?.id; useEffect(() => { // The block can render before its `query` has finished streaming in; wait // for it rather than POST an empty query (which 400s). if (!block.query) return; if (!organizationId || !projectId || !environmentId) { setState({ status: "error", error: "No environment context to run the query." }); return; } const controller = new AbortController(); setState({ status: "loading" }); fetch("/resources/metric", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: block.query, organizationId, projectId, environmentId, scope: "environment", period: block.period ?? null, from: null, to: null, }), signal: controller.signal, }) .then(async (res) => (await res.json()) as MetricResponse) .then((data) => { if (controller.signal.aborted) return; if (!data.success) { setState({ status: "error", error: data.error }); } else { setState({ status: "ready", rows: data.data.rows, columns: data.data.columns, timeRange: data.data.timeRange, }); } }) .catch((err) => { if (controller.signal.aborted) return; setState({ status: "error", error: err?.message ?? "The query failed to run." }); }); return () => controller.abort(); }, [block.query, block.period, organizationId, projectId, environmentId]); const config: ChartConfiguration = { chartType: block.chartType, xAxisColumn: block.xAxisColumn, yAxisColumns: block.yAxisColumns ?? [], groupByColumn: block.groupByColumn ?? null, stacked: block.stacked ?? false, sortByColumn: null, sortDirection: "desc", aggregation: block.aggregation ?? "sum", }; return (
{block.title ? (
{block.title}
) : null}
{state.status === "loading" ? (
Running query…
) : state.status === "error" ? (
{state.error}
) : ( )}
); }