Skip to content

Commit eeb476c

Browse files
committed
Query date picker works
1 parent 1259353 commit eeb476c

File tree

5 files changed

+102
-20
lines changed

5 files changed

+102
-20
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ArrowDownTrayIcon, ClipboardIcon } from "@heroicons/react/20/solid";
2-
import type { OutputColumnMetadata } from "@internal/clickhouse";
2+
import type { OutputColumnMetadata, WhereClauseFallback } from "@internal/clickhouse";
33
import { Form, useNavigation } from "@remix-run/react";
44
import {
55
redirect,
@@ -21,6 +21,8 @@ import { autoFormatSQL, TSQLEditor } from "~/components/code/TSQLEditor";
2121
import { TSQLResultsTable } from "~/components/code/TSQLResultsTable";
2222
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
2323
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
24+
import { TimeFilter, timeFilters } from "~/components/runs/v3/SharedFilters";
25+
import { useSearchParams } from "~/hooks/useSearchParam";
2426
import { Button } from "~/components/primitives/Buttons";
2527
import { Callout } from "~/components/primitives/Callout";
2628
import { Card } from "~/components/primitives/charts/Card";
@@ -62,6 +64,8 @@ import { querySchemas } from "~/v3/querySchemas";
6264
import { QueryHelpSidebar } from "./QueryHelpSidebar";
6365
import { QueryHistoryPopover } from "./QueryHistoryPopover";
6466
import { formatQueryStats } from "./utils";
67+
import { requireUser } from "~/services/session.server";
68+
import parse from "parse-duration";
6569

6670
async function hasQueryAccess(
6771
userId: string,
@@ -148,12 +152,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
148152
});
149153
};
150154

151-
import { requireUser } from "~/services/session.server";
155+
const DEFAULT_PERIOD = "7d";
152156

153157
const ActionSchema = z.object({
154158
query: z.string().min(1, "Query is required"),
155159
scope: z.enum(["environment", "project", "organization"]),
156160
explain: z.enum(["true", "false"]).nullable().optional(),
161+
period: z.string().nullable().optional(),
162+
from: z.string().nullable().optional(),
163+
to: z.string().nullable().optional(),
157164
});
158165

159166
export const action = async ({ request, params }: ActionFunctionArgs) => {
@@ -218,6 +225,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
218225
query: formData.get("query"),
219226
scope: formData.get("scope"),
220227
explain: formData.get("explain"),
228+
period: formData.get("period"),
229+
from: formData.get("from"),
230+
to: formData.get("to"),
221231
});
222232

223233
if (!parsed.success) {
@@ -235,11 +245,35 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
235245
);
236246
}
237247

238-
const { query, scope, explain: explainParam } = parsed.data;
248+
const { query, scope, explain: explainParam, period, from, to } = parsed.data;
239249
// Only allow explain for admins/impersonating users
240250
const isAdmin = user.admin || user.isImpersonating;
241251
const explain = explainParam === "true" && isAdmin;
242252

253+
// Build time filter fallback for triggered_at column
254+
const timeFilter = timeFilters({
255+
period: period ?? undefined,
256+
from: from ?? undefined,
257+
to: to ?? undefined,
258+
defaultPeriod: DEFAULT_PERIOD,
259+
});
260+
261+
let triggeredAtFallback: WhereClauseFallback;
262+
if (timeFilter.from && timeFilter.to) {
263+
// Both from and to specified - use BETWEEN
264+
triggeredAtFallback = { op: "between", low: timeFilter.from, high: timeFilter.to };
265+
} else if (timeFilter.from) {
266+
// Only from specified
267+
triggeredAtFallback = { op: "gte", value: timeFilter.from };
268+
} else if (timeFilter.to) {
269+
// Only to specified
270+
triggeredAtFallback = { op: "lte", value: timeFilter.to };
271+
} else {
272+
// Period specified (or default) - calculate from now
273+
const periodMs = parse(timeFilter.period ?? DEFAULT_PERIOD) ?? 7 * 24 * 60 * 60 * 1000;
274+
triggeredAtFallback = { op: "gte", value: new Date(Date.now() - periodMs) };
275+
}
276+
243277
try {
244278
const [error, result] = await executeQuery({
245279
name: "query-page",
@@ -252,6 +286,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
252286
projectId: project.id,
253287
environmentId: environment.id,
254288
explain,
289+
whereClauseFallback: {
290+
triggered_at: triggeredAtFallback,
291+
},
255292
history: {
256293
source: "DASHBOARD",
257294
userId: user.id,
@@ -320,6 +357,12 @@ const QueryEditorForm = forwardRef<
320357
>(function QueryEditorForm({ defaultQuery, defaultScope, history, isLoading, isAdmin }, ref) {
321358
const [query, setQuery] = useState(defaultQuery);
322359
const [scope, setScope] = useState<QueryScope>(defaultScope);
360+
const { value: searchParamValue } = useSearchParams();
361+
362+
// Get time filter values from URL search params
363+
const period = searchParamValue("period");
364+
const from = searchParamValue("from");
365+
const to = searchParamValue("to");
323366

324367
// Expose methods to parent for external query setting (history, AI, examples)
325368
useImperativeHandle(
@@ -352,6 +395,10 @@ const QueryEditorForm = forwardRef<
352395
<Form method="post" className="flex items-center justify-between gap-2 px-2">
353396
<input type="hidden" name="query" value={query} />
354397
<input type="hidden" name="scope" value={scope} />
398+
{/* Pass time filter values to action */}
399+
<input type="hidden" name="period" value={period ?? ""} />
400+
<input type="hidden" name="from" value={from ?? ""} />
401+
<input type="hidden" name="to" value={to ?? ""} />
355402
<QueryHistoryPopover history={history} onQuerySelected={handleHistorySelected} />
356403
<div className="flex items-center gap-1">
357404
<Select
@@ -372,6 +419,7 @@ const QueryEditorForm = forwardRef<
372419
))
373420
}
374421
</Select>
422+
<TimeFilter defaultPeriod={DEFAULT_PERIOD} />
375423
{isAdmin && (
376424
<Button
377425
type="submit"

apps/webapp/app/services/queryService.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export async function executeQuery<TOut extends z.ZodSchema>(
9494
environmentId,
9595
history,
9696
customOrgConcurrencyLimit,
97+
whereClauseFallback,
9798
...baseOptions
9899
} = options;
99100

@@ -155,6 +156,7 @@ export async function executeQuery<TOut extends z.ZodSchema>(
155156
...baseOptions,
156157
...tenantOptions,
157158
fieldMappings,
159+
whereClauseFallback,
158160
clickhouseSettings: {
159161
...getDefaultClickhouseSettings(),
160162
...baseOptions.clickhouseSettings, // Allow caller overrides if needed

internal-packages/clickhouse/src/client/tsql.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type TableSchema,
1515
type QuerySettings,
1616
type FieldMappings,
17+
type WhereClauseFallback,
1718
} from "@internal/tsql";
1819
import type { ClickhouseReader, QueryStats } from "./types.js";
1920
import { QueryError } from "./errors.js";
@@ -24,7 +25,7 @@ const logger = new Logger("tsql", "info");
2425

2526
export type { QueryStats };
2627

27-
export type { TableSchema, QuerySettings, FieldMappings };
28+
export type { TableSchema, QuerySettings, FieldMappings, WhereClauseFallback };
2829

2930
/**
3031
* Options for executing a TSQL query
@@ -74,6 +75,19 @@ export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
7475
* @default false
7576
*/
7677
explain?: boolean;
78+
/**
79+
* Fallback WHERE conditions to apply when the user hasn't filtered on a column.
80+
* Key is the column name, value is the fallback condition.
81+
*
82+
* @example
83+
* ```typescript
84+
* // Apply triggered_at >= 7 days ago if user doesn't filter on triggered_at
85+
* whereClauseFallback: {
86+
* triggered_at: { op: 'gte', value: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
87+
* }
88+
* ```
89+
*/
90+
whereClauseFallback?: Record<string, WhereClauseFallback>;
7791
}
7892

7993
/**
@@ -144,6 +158,7 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
144158
tableSchema: options.tableSchema,
145159
settings: options.querySettings,
146160
fieldMappings: options.fieldMappings,
161+
whereClauseFallback: options.whereClauseFallback,
147162
});
148163

149164
generatedSql = sql;

internal-packages/clickhouse/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export {
5656
type TSQLQuerySuccess,
5757
type QueryStats,
5858
type FieldMappings,
59+
type WhereClauseFallback,
5960
} from "./client/tsql.js";
6061
export type { OutputColumnMetadata } from "@internal/tsql";
6162

internal-packages/tsql/src/index.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TSQLParser } from "./grammar/TSQLParser.js";
99
import type {
1010
And,
1111
BetweenExpr,
12+
Call,
1213
CompareOperation,
1314
Constant,
1415
Expression,
@@ -267,13 +268,37 @@ export function isColumnReferencedInExpression(
267268
}
268269

269270
/**
270-
* Convert a fallback value to the appropriate type for AST constants
271+
* Format a Date as a ClickHouse-compatible DateTime64 string.
272+
* ClickHouse expects format: 'YYYY-MM-DD HH:MM:SS.mmm' (in UTC)
271273
*/
272-
function convertFallbackValue(value: Date | string | number): string | number {
274+
function formatDateForClickHouse(date: Date): string {
275+
const year = date.getUTCFullYear();
276+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
277+
const day = String(date.getUTCDate()).padStart(2, "0");
278+
const hours = String(date.getUTCHours()).padStart(2, "0");
279+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
280+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
281+
const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
282+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
283+
}
284+
285+
/**
286+
* Create an AST expression for a fallback value.
287+
* Date values are wrapped in toDateTime64() for ClickHouse compatibility.
288+
*/
289+
function createValueExpression(value: Date | string | number): Expression {
273290
if (value instanceof Date) {
274-
return value.toISOString();
291+
// Wrap Date in toDateTime64(formatted_string, 3) for ClickHouse DateTime64(3) columns
292+
return {
293+
expression_type: "call",
294+
name: "toDateTime64",
295+
args: [
296+
{ expression_type: "constant", value: formatDateForClickHouse(value) } as Constant,
297+
{ expression_type: "constant", value: 3 } as Constant,
298+
],
299+
} as Call;
275300
}
276-
return value;
301+
return { expression_type: "constant", value } as Constant;
277302
}
278303

279304
/**
@@ -316,14 +341,8 @@ export function createFallbackExpression(
316341
const betweenExpr: BetweenExpr = {
317342
expression_type: "between_expr",
318343
expr: fieldExpr,
319-
low: {
320-
expression_type: "constant",
321-
value: convertFallbackValue(fallback.low),
322-
} as Constant,
323-
high: {
324-
expression_type: "constant",
325-
value: convertFallbackValue(fallback.high),
326-
} as Constant,
344+
low: createValueExpression(fallback.low),
345+
high: createValueExpression(fallback.high),
327346
};
328347
return betweenExpr;
329348
}
@@ -332,10 +351,7 @@ export function createFallbackExpression(
332351
const compareExpr: CompareOperation = {
333352
expression_type: "compare_operation",
334353
left: fieldExpr,
335-
right: {
336-
expression_type: "constant",
337-
value: convertFallbackValue(fallback.value),
338-
} as Constant,
354+
right: createValueExpression(fallback.value),
339355
op: mapFallbackOpToCompareOp(fallback.op),
340356
};
341357
return compareExpr;

0 commit comments

Comments
 (0)