chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,75 @@
import { useState } from "react";
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
import { AIQueryInput } from "~/components/code/AIQueryInput";
import { Header3 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import type { AITimeFilter } from "./types";
export function AITabContent({
onQueryGenerated,
onTimeFilterChange,
getCurrentQuery,
aiFixRequest,
}: {
onQueryGenerated: (query: string) => void;
onTimeFilterChange?: (filter: AITimeFilter) => void;
getCurrentQuery: () => string;
aiFixRequest: { prompt: string; key: number } | null;
}) {
const [examplePromptRequest, setExamplePromptRequest] = useState<{
prompt: string;
key: number;
} | null>(null);
// Use aiFixRequest if present, otherwise use example prompt request
const activeRequest = aiFixRequest ?? examplePromptRequest;
const examplePrompts = [
"Show me failed runs by hour for the past 7 days",
"Count of runs by status by hour for the past 48h",
"Top 50 most expensive runs this week",
"Average execution duration by task this week",
"Run counts by tag in the past 7 days",
"CPU utilization over time by task",
"Peak memory usage per run",
];
return (
<div className="space-y-2">
<AIQueryInput
onQueryGenerated={onQueryGenerated}
onTimeFilterChange={onTimeFilterChange}
autoSubmitPrompt={activeRequest?.prompt}
autoSubmitKey={activeRequest?.key}
getCurrentQuery={getCurrentQuery}
/>
<div className="pt-4">
<Header3 className="mb-3 text-text-bright">Example prompts</Header3>
<div className="flex flex-wrap gap-2">
{examplePrompts.map((example) => (
<button
key={example}
type="button"
onClick={() => {
setExamplePromptRequest((prev) => ({
prompt: example,
key: (prev?.key ?? 0) + 1,
}));
}}
className="group flex w-fit items-center gap-2 rounded-full border border-dashed border-border-bright px-4 py-2 transition-colors hover:border-solid hover:border-indigo-500 focus-custom focus-visible:rounded-full!"
>
<SparkleListIcon className="size-4 shrink-0 text-text-dimmed transition group-hover:text-indigo-500" />
<Paragraph
variant="small"
className="text-left transition group-hover:text-text-bright"
>
{example}
</Paragraph>
</button>
))}
</div>
</div>
</div>
);
}
@@ -0,0 +1,250 @@
import { useState } from "react";
import { Header3 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import SegmentedControl from "~/components/primitives/SegmentedControl";
import type { QueryScope } from "~/services/queryService.server";
import { querySchemas } from "~/v3/querySchemas";
import { TryableCodeBlock } from "./TRQLGuideContent";
// Example queries for the Examples tab
export const exampleQueries: Array<{
title: string;
description: string;
query: string;
scope: QueryScope;
table: string;
}> = [
{
title: "Failed runs by task (past 7 days)",
description: "Count of failed runs grouped by task identifier over the last 7 days.",
query: `SELECT
task_identifier,
count() AS failed_count
FROM runs
WHERE status = 'Failed'
AND triggered_at > now() - INTERVAL 7 DAY
GROUP BY task_identifier
ORDER BY failed_count DESC
LIMIT 20`,
scope: "environment",
table: "runs",
},
{
title: "Execution duration p50 by task (past 7d)",
description: "Median (50th percentile) execution duration for each task.",
query: `SELECT
task_identifier,
quantile(0.5)(execution_duration) AS p50_duration_ms
FROM runs
WHERE triggered_at > now() - INTERVAL 7 DAY
AND execution_duration IS NOT NULL
GROUP BY task_identifier
ORDER BY p50_duration_ms DESC
LIMIT 20`,
scope: "environment",
table: "runs",
},
{
title: "Runs over time",
description:
"Count of runs bucketed over time. The bucket size adjusts automatically to the time range.",
query: `SELECT
timeBucket(),
count() AS run_count
FROM runs
GROUP BY timeBucket
ORDER BY timeBucket
LIMIT 1000`,
scope: "environment",
table: "runs",
},
{
title: "Most expensive 100 runs (past 7d)",
description: "Top 100 runs by cost over the last 7 days.",
query: `SELECT
run_id,
task_identifier,
status,
total_cost,
usage_duration,
machine,
triggered_at
FROM runs
WHERE triggered_at > now() - INTERVAL 7 DAY
ORDER BY total_cost DESC
LIMIT 100`,
scope: "environment",
table: "runs",
},
{
title: "CPU utilization over time",
description: "Track process CPU utilization bucketed over time.",
query: `SELECT
timeBucket(),
avg(metric_value) AS avg_cpu
FROM metrics
WHERE metric_name = 'process.cpu.utilization'
GROUP BY timeBucket
ORDER BY timeBucket
LIMIT 1000`,
scope: "environment",
table: "metrics",
},
{
title: "Memory usage by task (past 7d)",
description: "Average memory usage per task identifier over the last 7 days.",
query: `SELECT
task_identifier,
avg(metric_value) AS avg_memory
FROM metrics
WHERE metric_name = 'system.memory.usage'
AND bucket_start > now() - INTERVAL 7 DAY
GROUP BY task_identifier
ORDER BY avg_memory DESC
LIMIT 20`,
scope: "environment",
table: "metrics",
},
{
title: "Available metric names",
description: "List all distinct metric names collected in your environment.",
query: `SELECT
metric_name,
count() AS sample_count
FROM metrics
GROUP BY metric_name
ORDER BY sample_count DESC
LIMIT 100`,
scope: "environment",
table: "metrics",
},
{
title: "LLM cost by model (past 7d)",
description:
"Total cost, input tokens, and output tokens grouped by model over the last 7 days.",
query: `SELECT
response_model,
SUM(total_cost) AS total_cost,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens
FROM llm_metrics
WHERE start_time > now() - INTERVAL 7 DAY
GROUP BY response_model
ORDER BY total_cost DESC`,
scope: "environment",
table: "llm_metrics",
},
{
title: "LLM cost over time",
description: "Total LLM cost bucketed over time. The bucket size adjusts automatically.",
query: `SELECT
timeBucket(),
SUM(total_cost) AS total_cost
FROM llm_metrics
GROUP BY timeBucket
ORDER BY timeBucket
LIMIT 1000`,
scope: "environment",
table: "llm_metrics",
},
{
title: "Most expensive runs by LLM cost (top 50)",
description: "Top 50 runs by total LLM cost with token breakdown.",
query: `SELECT
run_id,
task_identifier,
SUM(total_cost) AS llm_cost,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens
FROM llm_metrics
GROUP BY run_id, task_identifier
ORDER BY llm_cost DESC
LIMIT 50`,
scope: "environment",
table: "llm_metrics",
},
{
title: "LLM calls by provider",
description: "Count and cost of LLM calls grouped by AI provider.",
query: `SELECT
gen_ai_system,
count() AS call_count,
SUM(total_cost) AS total_cost
FROM llm_metrics
GROUP BY gen_ai_system
ORDER BY total_cost DESC`,
scope: "environment",
table: "llm_metrics",
},
{
title: "LLM cost by user",
description:
"Total LLM cost per user from run tags or AI SDK telemetry metadata. Uses metadata.userId which comes from experimental_telemetry metadata or run tags like user:123.",
query: `SELECT
metadata.userId AS user_id,
SUM(total_cost) AS total_cost,
SUM(total_tokens) AS total_tokens,
count() AS call_count
FROM llm_metrics
WHERE metadata.userId != ''
GROUP BY metadata.userId
ORDER BY total_cost DESC
LIMIT 50`,
scope: "environment",
table: "llm_metrics",
},
{
title: "LLM cost by metadata key",
description:
"Browse all metadata keys and their LLM cost. Metadata comes from run tags (key:value) and AI SDK telemetry metadata.",
query: `SELECT
metadata,
response_model,
total_cost,
total_tokens,
run_id
FROM llm_metrics
ORDER BY start_time DESC
LIMIT 20`,
scope: "environment",
table: "llm_metrics",
},
];
const tableOptions = querySchemas.map((s) => ({ label: s.name, value: s.name }));
export function ExamplesContent({
onTryExample,
}: {
onTryExample: (query: string, scope: QueryScope) => void;
}) {
const [selectedTable, setSelectedTable] = useState(querySchemas[0].name);
const filtered = exampleQueries.filter((e) => e.table === selectedTable);
return (
<div className="space-y-6">
<div className="sticky top-0 z-10 bg-background-bright pb-3">
<SegmentedControl
name="examples-table-selector"
value={selectedTable}
options={tableOptions}
variant="secondary/small"
fullWidth
onChange={setSelectedTable}
/>
</div>
{filtered.map((example) => (
<div key={example.title}>
<Header3 className="mb-1 text-text-bright">{example.title}</Header3>
<Paragraph variant="small" className="mb-2 text-text-dimmed">
{example.description}
</Paragraph>
<TryableCodeBlock
code={example.query}
onTry={() => onTryExample(example.query, example.scope)}
/>
</div>
))}
</div>
);
}
@@ -0,0 +1,117 @@
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
import {
ClientTabs,
ClientTabsContent,
ClientTabsList,
ClientTabsTrigger,
} from "~/components/primitives/ClientTabs";
import type { QueryScope } from "~/services/queryService.server";
import { AITabContent } from "./AITabContent";
import { ExamplesContent } from "./ExamplesContent";
import { TableSchemaContent } from "./TableSchemaContent";
import { TRQLGuideContent } from "./TRQLGuideContent";
import type { AITimeFilter } from "./types";
export function QueryHelpSidebar({
onTryExample,
onQueryGenerated,
onTimeFilterChange,
getCurrentQuery,
activeTab,
onTabChange,
aiFixRequest,
}: {
onTryExample: (query: string, scope: QueryScope) => void;
onQueryGenerated: (query: string) => void;
onTimeFilterChange?: (filter: AITimeFilter) => void;
getCurrentQuery: () => string;
activeTab: string;
onTabChange: (tab: string) => void;
aiFixRequest: { prompt: string; key: number } | null;
}) {
return (
<div className="grid h-full max-h-full grid-rows-[auto_1fr] overflow-hidden bg-background-bright">
<ClientTabs
value={activeTab}
onValueChange={onTabChange}
className="flex min-h-0 flex-col overflow-hidden pt-1"
>
<div className="h-fit overflow-x-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
<ClientTabsList variant="underline" className="mx-3 shrink-0">
<ClientTabsTrigger
value="ai"
variant="underline"
layoutId="query-help-tabs"
className="shrink-0"
>
<div className="flex items-center gap-0.5">
<AISparkleIcon className="size-4" /> AI
</div>
</ClientTabsTrigger>
<ClientTabsTrigger
value="guide"
variant="underline"
layoutId="query-help-tabs"
className="shrink-0"
>
Writing TRQL
</ClientTabsTrigger>
<ClientTabsTrigger
value="schema"
variant="underline"
layoutId="query-help-tabs"
className="shrink-0"
>
Table schema
</ClientTabsTrigger>
<ClientTabsTrigger
value="examples"
variant="underline"
layoutId="query-help-tabs"
className="shrink-0"
>
Examples
</ClientTabsTrigger>
</ClientTabsList>
</div>
<ClientTabsContent
value="ai"
className="min-h-0 flex-1 overflow-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
>
<div className="min-w-64 p-3">
<AITabContent
onQueryGenerated={onQueryGenerated}
onTimeFilterChange={onTimeFilterChange}
getCurrentQuery={getCurrentQuery}
aiFixRequest={aiFixRequest}
/>
</div>
</ClientTabsContent>
<ClientTabsContent
value="guide"
className="min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
>
<div className="min-w-64 p-3">
<TRQLGuideContent onTryExample={onTryExample} />
</div>
</ClientTabsContent>
<ClientTabsContent
value="schema"
className="min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
>
<div className="min-w-64 p-3">
<TableSchemaContent />
</div>
</ClientTabsContent>
<ClientTabsContent
value="examples"
className="min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
>
<div className="min-w-64 p-3">
<ExamplesContent onTryExample={onTryExample} />
</div>
</ClientTabsContent>
</ClientTabs>
</div>
);
}
@@ -0,0 +1,163 @@
import { useState } from "react";
import { ClockRotateLeftIcon } from "~/assets/icons/ClockRotateLeftIcon";
import { Button } from "~/components/primitives/Buttons";
import { Popover, PopoverTrigger } from "~/components/primitives/Popover";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import type { QueryHistoryItem } from "~/presenters/v3/QueryPresenter.server";
import { timeFilterRenderValues } from "~/components/runs/v3/SharedFilters";
import { cn } from "~/utils/cn";
const SQL_KEYWORDS = [
"SELECT",
"FROM",
"WHERE",
"ORDER BY",
"LIMIT",
"GROUP BY",
"HAVING",
"JOIN",
"LEFT JOIN",
"RIGHT JOIN",
"INNER JOIN",
"OUTER JOIN",
"AND",
"OR",
"AS",
"ON",
"IN",
"NOT",
"NULL",
"DESC",
"ASC",
"DISTINCT",
"COUNT",
"SUM",
"AVG",
"MIN",
"MAX",
];
function highlightSQL(query: string): React.ReactNode[] {
// Normalize: collapse multiple spaces/tabs to single space, but preserve newlines
// Then trim each line and limit total length
const normalized = query
.split("\n")
.map((line) => line.replace(/[ \t]+/g, " ").trim())
.filter((line) => line.length > 0)
.join("\n")
.slice(0, 500);
// Create a regex pattern that matches keywords as whole words (case insensitive)
const keywordPattern = new RegExp(
`\\b(${SQL_KEYWORDS.map((k) => k.replace(/\s+/g, "\\s+")).join("|")})\\b`,
"gi"
);
const parts: React.ReactNode[] = [];
let lastIndex = 0;
let match;
while ((match = keywordPattern.exec(normalized)) !== null) {
// Add text before the match
if (match.index > lastIndex) {
parts.push(normalized.slice(lastIndex, match.index));
}
// Add the highlighted keyword
parts.push(
<span key={match.index} className="text-[#c678dd]">
{match[0]}
</span>
);
lastIndex = keywordPattern.lastIndex;
}
// Add remaining text
if (lastIndex < normalized.length) {
parts.push(normalized.slice(lastIndex));
}
return parts;
}
export function QueryHistoryPopover({
history,
onQuerySelected,
}: {
history: QueryHistoryItem[];
onQuerySelected: (item: QueryHistoryItem) => void;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
type="button"
variant="secondary/small"
LeadingIcon={ClockRotateLeftIcon}
leadingIconClassName="-mr-1.5"
disabled={history.length === 0}
tooltip="Query history"
shortcut={{ key: "h" }}
>
History
</Button>
</PopoverTrigger>
<PopoverPrimitive.Content
className={cn(
"z-50 w-[400px] min-w-0 overflow-hidden rounded border border-grid-bright bg-background-bright p-0 shadow-md outline-hidden animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
)}
align="start"
sideOffset={6}
style={{ maxHeight: "var(--radix-popover-content-available-height)" }}
>
<div className="max-h-160 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
<div className="p-1">
{history.map((item) => {
// Format time filter display
const { valueLabel } = timeFilterRenderValues({
period: item.filterPeriod ?? undefined,
from: item.filterFrom ?? undefined,
to: item.filterTo ?? undefined,
});
return (
<button
key={item.id}
type="button"
onClick={() => {
onQuerySelected(item);
setIsOpen(false);
}}
className="flex w-full flex-col gap-1 rounded-sm px-2 py-2 outline-hidden transition-colors focus-custom hover:bg-background-hover"
>
<div className="flex w-full flex-col items-start">
{item.title ? (
<p className="mb-1 truncate text-left text-sm font-medium text-text-bright">
{item.title}
</p>
) : (
<p className="mb-1 truncate text-left font-mono text-xs text-text-bright">
{item.query.split("\n")[0].slice(0, 60)}
</p>
)}
<div className="flex items-center gap-1.5 text-xs text-text-dimmed">
<span className="capitalize">{item.scope}</span>
{valueLabel && <span>· {valueLabel}</span>}
{item.userName && <span>· {item.userName}</span>}
</div>
</div>
<div className="w-full border-l-2 border-border-bright pl-2.5">
<p className="line-clamp-4 w-full whitespace-pre-wrap text-left font-mono text-xs text-text-dimmed">
{highlightSQL(item.query)}
</p>
</div>
</button>
);
})}
</div>
</div>
</PopoverPrimitive.Content>
</Popover>
);
}
@@ -0,0 +1,78 @@
import type { ColumnSchema } from "@internal/tsql";
import { useState } from "react";
import { Badge } from "~/components/primitives/Badge";
import { CopyableText } from "~/components/primitives/CopyableText";
import { Paragraph } from "~/components/primitives/Paragraph";
import SegmentedControl from "~/components/primitives/SegmentedControl";
import { querySchemas } from "~/v3/querySchemas";
function ColumnHelpItem({ col }: { col: ColumnSchema }) {
return (
<div className="pt-1">
<div className="flex items-center gap-2">
<CopyableText value={col.name} className="text-sm text-indigo-400" />
<Badge className="font-mono text-xxs">{col.type}</Badge>
</div>
{col.description && (
<Paragraph variant="extra-small" className="mt-1 text-text-dimmed">
{col.description}
</Paragraph>
)}
{col.example && (
<div className="mt-1 flex items-baseline gap-0.5">
<span className="text-xs text-text-dimmed">Example:</span>
<CopyableText
value={col.example}
className="rounded-sm bg-background-hover px-1.5 py-0.5 font-mono text-xxs"
/>
</div>
)}
{col.allowedValues && col.allowedValues.length > 0 && (
<div className="mt-0.5 flex flex-wrap gap-1">
<span className="text-xs text-text-dimmed">Available options:</span>
{col.allowedValues.map((value) => (
<CopyableText
key={value}
value={col.valueMap?.[value] ?? value}
className="rounded-sm bg-background-hover px-1.5 py-0.5 font-mono text-xxs"
/>
))}
</div>
)}
</div>
);
}
const tableOptions = querySchemas.map((s) => ({ label: s.name, value: s.name }));
export function TableSchemaContent() {
const [selectedTable, setSelectedTable] = useState(querySchemas[0].name);
const table = querySchemas.find((s) => s.name === selectedTable) ?? querySchemas[0];
return (
<div>
<div className="sticky top-0 z-10 bg-background-bright pb-3">
<SegmentedControl
name="table-schema-selector"
value={selectedTable}
options={tableOptions}
variant="secondary/small"
fullWidth
onChange={setSelectedTable}
/>
</div>
<div className="mb-2">
{table.description && (
<Paragraph variant="small" className="text-text-dimmed">
{table.description}
</Paragraph>
)}
</div>
<div className="flex flex-col gap-2 divide-y divide-grid-dimmed">
{Object.values(table.columns).map((col) => (
<ColumnHelpItem key={col.name} col={col} />
))}
</div>
</div>
);
}
@@ -0,0 +1,295 @@
import {
redirect,
type ActionFunctionArgs,
type LoaderFunctionArgs,
} from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { QueryEditor } from "~/components/query/QueryEditor";
import { env } from "~/env.server";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { QueryPresenter } from "~/presenters/v3/QueryPresenter.server";
import { executeQuery, getDefaultPeriod } from "~/services/queryService.server";
import { requireUser } from "~/services/session.server";
import { EnvironmentParamSchema, queryPath } from "~/utils/pathBuilder";
import { canAccessQuery } from "~/v3/canAccessQuery.server";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useEnvironment } from "~/hooks/useEnvironment";
/** Convert a Date or ISO string to ISO string format */
function toISOString(value: Date | string): string {
if (typeof value === "string") {
return value;
}
return value.toISOString();
}
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const user = await requireUser(request);
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
const canAccess = await canAccessQuery({
userId: user.id,
isAdmin: user.admin,
isImpersonating: user.isImpersonating,
organizationSlug,
});
if (!canAccess) {
throw redirect("/");
}
const project = await findProjectBySlug(organizationSlug, projectParam, user.id);
if (!project) {
throw new Response(undefined, {
status: 404,
statusText: "Project not found",
});
}
const environment = await findEnvironmentBySlug(project.id, envParam, user.id);
if (!environment) {
throw new Response(undefined, {
status: 404,
statusText: "Environment not found",
});
}
const presenter = new QueryPresenter();
const { defaultQuery, history } = await presenter.call({
organizationId: project.organizationId,
});
// Admins and impersonating users can use EXPLAIN
const isAdmin = user.admin || user.isImpersonating;
return typedjson({
defaultQuery,
defaultPeriod: await getDefaultPeriod(project.organizationId),
history,
isAdmin,
maxRows: env.QUERY_CLICKHOUSE_MAX_RETURNED_ROWS,
});
};
const ActionSchema = z.object({
query: z.string().min(1, "Query is required"),
scope: z.enum(["environment", "project", "organization"]),
explain: z.enum(["true", "false"]).nullable().optional(),
period: z.string().nullable().optional(),
from: z.string().nullable().optional(),
to: z.string().nullable().optional(),
});
export const action = async ({ request, params }: ActionFunctionArgs) => {
const user = await requireUser(request);
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
const canAccess = await canAccessQuery({
userId: user.id,
isAdmin: user.admin,
isImpersonating: user.isImpersonating,
organizationSlug,
});
if (!canAccess) {
return typedjson(
{
error: "Unauthorized",
rows: null,
columns: null,
stats: null,
hiddenColumns: null,
reachedMaxRows: null,
explainOutput: null,
generatedSql: null,
periodClipped: null,
},
{ status: 403 }
);
}
const project = await findProjectBySlug(organizationSlug, projectParam, user.id);
if (!project) {
return typedjson(
{
error: "Project not found",
rows: null,
columns: null,
stats: null,
hiddenColumns: null,
reachedMaxRows: null,
explainOutput: null,
generatedSql: null,
periodClipped: null,
},
{ status: 404 }
);
}
const environment = await findEnvironmentBySlug(project.id, envParam, user.id);
if (!environment) {
return typedjson(
{
error: "Environment not found",
rows: null,
columns: null,
stats: null,
hiddenColumns: null,
reachedMaxRows: null,
explainOutput: null,
generatedSql: null,
periodClipped: null,
},
{ status: 404 }
);
}
const formData = await request.formData();
const parsed = ActionSchema.safeParse({
query: formData.get("query"),
scope: formData.get("scope"),
explain: formData.get("explain"),
period: formData.get("period"),
from: formData.get("from"),
to: formData.get("to"),
});
if (!parsed.success) {
return typedjson(
{
error: parsed.error.errors.map((e) => e.message).join(", "),
rows: null,
columns: null,
stats: null,
hiddenColumns: null,
reachedMaxRows: null,
explainOutput: null,
generatedSql: null,
periodClipped: null,
},
{ status: 400 }
);
}
const { query, scope, explain: explainParam, period, from, to } = parsed.data;
// Only allow explain for admins/impersonating users
const isAdmin = user.admin || user.isImpersonating;
const explain = explainParam === "true" && isAdmin;
try {
const queryResult = await executeQuery({
name: "query-page",
query,
scope,
organizationId: project.organizationId,
projectId: project.id,
environmentId: environment.id,
explain,
period,
from,
to,
history: {
source: "DASHBOARD",
userId: user.id,
skip: user.isImpersonating,
},
});
if (!queryResult.success) {
return typedjson(
{
error: queryResult.error.message,
rows: null,
columns: null,
stats: null,
hiddenColumns: null,
reachedMaxRows: null,
explainOutput: null,
generatedSql: null,
queryId: null,
periodClipped: null,
},
{ status: 400 }
);
}
return typedjson({
error: null,
rows: queryResult.result.rows,
columns: queryResult.result.columns,
stats: queryResult.result.stats,
hiddenColumns: queryResult.result.hiddenColumns ?? null,
reachedMaxRows: queryResult.result.reachedMaxRows,
explainOutput: queryResult.result.explainOutput ?? null,
generatedSql: queryResult.result.generatedSql ?? null,
queryId: queryResult.queryId,
periodClipped: queryResult.periodClipped,
maxQueryPeriod: queryResult.maxQueryPeriod,
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error executing query";
return typedjson(
{
error: errorMessage,
rows: null,
columns: null,
stats: null,
hiddenColumns: null,
reachedMaxRows: null,
explainOutput: null,
generatedSql: null,
queryId: null,
periodClipped: null,
},
{ status: 500 }
);
}
};
export default function Page() {
const { defaultPeriod, defaultQuery, history, isAdmin, maxRows } =
useTypedLoaderData<typeof loader>();
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const plan = useCurrentPlan();
const maxPeriodDays = plan?.v3Subscription?.plan?.limits?.queryPeriodDays?.number;
// Use most recent history item if available, otherwise fall back to defaults
const initialQuery = history.length > 0 ? history[0].query : defaultQuery;
const initialScope = history.length > 0 ? history[0].scope : "environment";
const initialTimeFilter =
history.length > 0
? {
period: history[0].filterPeriod ?? undefined,
from: history[0].filterFrom ? toISOString(history[0].filterFrom) : undefined,
to: history[0].filterTo ? toISOString(history[0].filterTo) : undefined,
}
: undefined;
// Build the query action URL for this page
const queryActionUrl = queryPath(
{ slug: organization.slug },
{ slug: project.slug },
{ slug: environment.slug }
);
return (
<QueryEditor
defaultQuery={initialQuery}
defaultScope={initialScope}
defaultPeriod={defaultPeriod}
defaultTimeFilter={initialTimeFilter}
history={history}
isAdmin={isAdmin}
maxRows={maxRows}
queryActionUrl={queryActionUrl}
mode={{ type: "standalone" }}
maxPeriodDays={maxPeriodDays}
/>
);
}
@@ -0,0 +1,9 @@
/**
* Time filter configuration that can be set by the AI.
* Used across both server and client code for AI query generation.
*/
export type AITimeFilter = {
period?: string;
from?: string;
to?: string;
};