Skip to content

Commit ed0fd70

Browse files
committed
Fixed time filtering and page reloading
1 parent 1f5b0f4 commit ed0fd70

File tree

2 files changed

+87
-77
lines changed
  • apps/webapp/app

2 files changed

+87
-77
lines changed

apps/webapp/app/components/runs/v3/SharedFilters.tsx

Lines changed: 60 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ export function timeFilterRenderValues({
265265
rangeType === "range" || rangeType === "period"
266266
? labelName
267267
: rangeType === "from"
268-
? `${labelName} after`
269-
: `${labelName} before`;
268+
? `${labelName} after`
269+
: `${labelName} before`;
270270

271271
return { label, valueLabel, rangeType };
272272
}
@@ -280,56 +280,37 @@ export interface TimeFilterApplyValues {
280280

281281
export interface TimeFilterProps {
282282
defaultPeriod?: string;
283+
period?: string;
284+
from?: string;
285+
to?: string;
283286
/** Label name used in the filter display, defaults to "Created" */
284287
labelName?: string;
285288
applyShortcut?: ShortcutDefinition | undefined;
286289
/** Callback when the user applies a time filter selection, receives the applied values */
287-
onApply?: (values: TimeFilterApplyValues) => void;
290+
onValueChange?: (values: TimeFilterApplyValues) => void;
288291
}
289292

290293
export function TimeFilter({
291294
defaultPeriod,
295+
period,
296+
from,
297+
to,
292298
labelName = "Created",
293299
applyShortcut,
294-
onApply,
300+
onValueChange,
295301
}: TimeFilterProps = {}) {
296302
const { value } = useSearchParams();
297-
const periodValue = value("period");
298-
const fromValue = value("from");
299-
const toValue = value("to");
300-
301-
//non-optimistic location
302-
const location = useLocation();
303-
304-
const { period, from, to, label, valueLabel } = timeFilters({
303+
const periodValue = period ?? value("period");
304+
const fromValue = from ?? value("from");
305+
const toValue = to ?? value("to");
306+
307+
const constrained = timeFilters({
305308
period: periodValue,
306309
from: fromValue,
307310
to: toValue,
308311
defaultPeriod,
309312
labelName,
310313
});
311-
312-
// Track the search string, not individual values
313-
const previousSearch = useRef<string | null>(null);
314-
315-
useEffect(() => {
316-
// Skip first render - set initial value and return
317-
if (previousSearch.current === null) {
318-
previousSearch.current = location.search;
319-
return;
320-
}
321-
322-
// Only call onApply if the URL actually changed
323-
if (previousSearch.current !== location.search) {
324-
const currentSearch = new URLSearchParams(location.search);
325-
onApply?.({
326-
period: currentSearch.get("period") ?? undefined,
327-
from: currentSearch.get("from") ?? undefined,
328-
to: currentSearch.get("to") ?? undefined
329-
});
330-
previousSearch.current = location.search;
331-
}
332-
}, [location.search, onApply]);
333314

334315
return (
335316
<FilterMenuProvider>
@@ -338,21 +319,21 @@ export function TimeFilter({
338319
trigger={
339320
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
340321
<AppliedFilter
341-
label={label}
322+
label={constrained.label}
342323
icon={filterIcon("period")}
343-
value={valueLabel}
324+
value={constrained.valueLabel}
344325
removable={false}
345326
variant="secondary/small"
346327
/>
347328
</Ariakit.Select>
348329
}
349-
period={period}
350-
from={from}
351-
to={to}
330+
period={constrained.period}
331+
from={constrained.from}
332+
to={constrained.to}
352333
defaultPeriod={defaultPeriod}
353334
labelName={labelName}
354335
applyShortcut={applyShortcut}
355-
onApply={onApply}
336+
onValueChange={onValueChange}
356337
/>
357338
)}
358339
</FilterMenuProvider>
@@ -379,6 +360,7 @@ export function TimeDropdown({
379360
labelName = "Created",
380361
applyShortcut,
381362
onApply,
363+
onValueChange,
382364
}: {
383365
trigger: ReactNode;
384366
period?: string;
@@ -388,6 +370,8 @@ export function TimeDropdown({
388370
labelName?: string;
389371
applyShortcut?: ShortcutDefinition | undefined;
390372
onApply?: (values: TimeFilterApplyValues) => void;
373+
/** When provided, the component operates in controlled mode and skips URL navigation */
374+
onValueChange?: (values: TimeFilterApplyValues) => void;
391375
}) {
392376
const [open, setOpen] = useState<boolean | undefined>();
393377
const { replace } = useSearchParams();
@@ -444,18 +428,26 @@ export function TimeDropdown({
444428
periodToApply = `${customValue}${customUnit}`;
445429
}
446430

447-
replace({
448-
period: periodToApply,
449-
cursor: undefined,
450-
direction: undefined,
451-
from: undefined,
452-
to: undefined,
453-
});
431+
const values: TimeFilterApplyValues = { period: periodToApply, from: undefined, to: undefined };
432+
433+
if (onValueChange) {
434+
// Controlled mode - just call the handler
435+
onValueChange(values);
436+
} else {
437+
// URL mode - navigate
438+
replace({
439+
period: periodToApply,
440+
cursor: undefined,
441+
direction: undefined,
442+
from: undefined,
443+
to: undefined,
444+
});
445+
}
454446

455447
setFromValue(undefined);
456448
setToValue(undefined);
457449
setOpen(false);
458-
onApply?.({ period: periodToApply, from: undefined, to: undefined });
450+
onApply?.(values);
459451
} else {
460452
// Validate date range
461453
if (!fromValue && !toValue) {
@@ -471,16 +463,24 @@ export function TimeDropdown({
471463
const fromStr = fromValue?.getTime().toString();
472464
const toStr = toValue?.getTime().toString();
473465

474-
replace({
475-
period: undefined,
476-
cursor: undefined,
477-
direction: undefined,
478-
from: fromStr,
479-
to: toStr,
480-
});
466+
const values: TimeFilterApplyValues = { period: undefined, from: fromStr, to: toStr };
467+
468+
if (onValueChange) {
469+
// Controlled mode - just call the handler
470+
onValueChange(values);
471+
} else {
472+
// URL mode - navigate
473+
replace({
474+
period: undefined,
475+
cursor: undefined,
476+
direction: undefined,
477+
from: fromStr,
478+
to: toStr,
479+
});
480+
}
481481

482482
setOpen(false);
483-
onApply?.({ period: undefined, from: fromStr, to: toStr });
483+
onApply?.(values);
484484
}
485485
}, [
486486
activeSection,
@@ -492,6 +492,7 @@ export function TimeDropdown({
492492
toValue,
493493
replace,
494494
onApply,
495+
onValueChange,
495496
]);
496497

497498
return (
@@ -532,9 +533,9 @@ export function TimeDropdown({
532533
? "border-indigo-500 "
533534
: "border-charcoal-650 hover:border-charcoal-600",
534535
validationError &&
535-
activeSection === "duration" &&
536-
selectedPeriod === "custom" &&
537-
"border-error"
536+
activeSection === "duration" &&
537+
selectedPeriod === "custom" &&
538+
"border-error"
538539
)}
539540
onClick={(e) => e.stopPropagation()}
540541
>

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

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ArrowDownTrayIcon, ClipboardIcon } from "@heroicons/react/20/solid";
22
import type { OutputColumnMetadata, WhereClauseFallback } from "@internal/clickhouse";
3-
import { Form, useNavigation, useSubmit } from "@remix-run/react";
43
import {
54
redirect,
65
type ActionFunctionArgs,
76
type LoaderFunctionArgs,
87
} from "@remix-run/server-runtime";
98
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
10-
import { typedjson, useTypedActionData, useTypedLoaderData } from "remix-typedjson";
9+
import { flushSync } from "react-dom";
10+
import { typedjson, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
1111
import { z } from "zod";
1212
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
1313
import { AlphaTitle } from "~/components/AlphaBadge";
@@ -353,26 +353,24 @@ const QueryEditorForm = forwardRef<
353353
defaultQuery: string;
354354
defaultScope: QueryScope;
355355
history: QueryHistoryItem[];
356-
isLoading: boolean;
356+
fetcher: ReturnType<typeof useTypedFetcher<typeof action>>;
357357
isAdmin: boolean;
358358
}
359-
>(function QueryEditorForm({ defaultQuery, defaultScope, history, isLoading, isAdmin }, ref) {
359+
>(function QueryEditorForm({ defaultQuery, defaultScope, history, fetcher, isAdmin }, ref) {
360+
const isLoading = fetcher.state === "submitting" || fetcher.state === "loading";
360361
const [query, setQuery] = useState(defaultQuery);
361362
const [scope, setScope] = useState<QueryScope>(defaultScope);
362-
const { value: searchParamValue } = useSearchParams();
363363
const formRef = useRef<HTMLFormElement>(null);
364364

365365
// Get time filter values from URL search params
366-
const period = searchParamValue("period");
367-
const from = searchParamValue("from");
368-
const to = searchParamValue("to");
366+
const [period, setPeriod] = useState<string | undefined>();
367+
const [from, setFrom] = useState<string | undefined>();
368+
const [to, setTo] = useState<string | undefined>();
369369

370370
// Check if the query contains triggered_at in a WHERE clause
371371
// This disables the time filter UI since the user is filtering in their query
372372
const queryHasTriggeredAt = /\bWHERE\b[\s\S]*\btriggered_at\b/i.test(query);
373373

374-
375-
376374
// Expose methods to parent for external query setting (history, AI, examples)
377375
useImperativeHandle(
378376
ref,
@@ -401,7 +399,7 @@ const QueryEditorForm = forwardRef<
401399
minHeight="200px"
402400
className="min-h-[200px]"
403401
/>
404-
<Form ref={formRef} method="post" className="flex items-center justify-between gap-2 px-2">
402+
<fetcher.Form ref={formRef} method="post" className="flex items-center justify-between gap-2 px-2">
405403
<input type="hidden" name="query" value={query} />
406404
<input type="hidden" name="scope" value={scope} />
407405
{/* Pass time filter values to action */}
@@ -450,8 +448,20 @@ const QueryEditorForm = forwardRef<
450448
<TimeFilter
451449
defaultPeriod={DEFAULT_PERIOD}
452450
labelName="Triggered"
451+
period={period}
452+
from={from}
453+
to={to}
453454
applyShortcut={{ key: "enter", enabledOnInputElements: true }}
454-
onApply={() => formRef.current?.submit()}
455+
onValueChange={(values) => {
456+
flushSync(() => {
457+
setPeriod(values.period);
458+
setFrom(values.from);
459+
setTo(values.to);
460+
});
461+
if (formRef.current) {
462+
fetcher.submit(formRef.current);
463+
}
464+
}}
455465
/>
456466
)}
457467
<Button
@@ -464,15 +474,15 @@ const QueryEditorForm = forwardRef<
464474
{isLoading ? "Querying..." : "Query"}
465475
</Button>
466476
</div>
467-
</Form>
477+
</fetcher.Form>
468478
</div>
469479
);
470480
});
471481

472482
export default function Page() {
473483
const { defaultQuery, history, isAdmin } = useTypedLoaderData<typeof loader>();
474-
const results = useTypedActionData<typeof action>();
475-
const navigation = useNavigation();
484+
const fetcher = useTypedFetcher<typeof action>();
485+
const results = fetcher.data;
476486
const { replace: replaceSearchParams } = useSearchParams();
477487

478488
// Use most recent history item if available, otherwise fall back to defaults
@@ -509,8 +519,7 @@ export default function Page() {
509519
[replaceSearchParams]
510520
);
511521

512-
const isLoading = (navigation.state === "submitting" || navigation.state === "loading") && navigation.formMethod === "POST";
513-
522+
const isLoading = fetcher.state === "submitting" || fetcher.state === "loading";
514523

515524
// Create a stable key from columns to detect schema changes
516525
const columnsKey = results?.columns
@@ -544,7 +553,7 @@ export default function Page() {
544553
defaultQuery={initialQuery}
545554
defaultScope={initialScope}
546555
history={history}
547-
isLoading={isLoading}
556+
fetcher={fetcher}
548557
isAdmin={isAdmin}
549558
/>
550559
{/* Results */}

0 commit comments

Comments
 (0)