Skip to content

Commit 49d3769

Browse files
committed
fix(webapp): don't reload runs list when toggling bulk action inspector
Prevent runs list revalidation and loading states when toggling the bulk action inspector. Filter, pagination, and explicit refresh behavior are unchanged.
1 parent db4074d commit 49d3769

6 files changed

Lines changed: 260 additions & 55 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Stop reloading the runs list when opening or closing the bulk action inspector

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

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid";
2-
import { type MetaFunction, useNavigation, useRevalidator } from "@remix-run/react";
2+
import { type MetaFunction, useLocation, useNavigation, useRevalidator } from "@remix-run/react";
33
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
4-
import { Suspense } from "react";
4+
import { Suspense, useState } from "react";
55
import {
66
TypedAwait,
77
typeddefer,
@@ -56,7 +56,6 @@ import { cn } from "~/utils/cn";
5656
import {
5757
docsPath,
5858
EnvironmentParamSchema,
59-
v3CreateBulkActionPath,
6059
v3ProjectPath,
6160
v3TestPath,
6261
v3TestTaskPath,
@@ -65,8 +64,11 @@ import { throwNotFound } from "~/utils/httpErrors";
6564
import { ListPagination } from "../../components/ListPagination";
6665
import { CreateBulkActionInspector } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction";
6766
import { Callout } from "~/components/primitives/Callout";
67+
import { isRunsListLoading, shouldRevalidateRunsList } from "./shouldRevalidateRunsList";
6868
import { useRunsLiveReload } from "./useRunsLiveReload";
6969

70+
export { shouldRevalidateRunsList as shouldRevalidate };
71+
7072
export const meta: MetaFunction = () => {
7173
return [
7274
{
@@ -209,8 +211,9 @@ function RunsList({
209211
filters: TaskRunListSearchFilters;
210212
}) {
211213
const revalidator = useRevalidator();
214+
const location = useLocation();
212215
const navigation = useNavigation();
213-
const isLoading = navigation.state !== "idle";
216+
const isLoading = isRunsListLoading(navigation, location.search);
214217
const organization = useOrganization();
215218
const project = useProject();
216219
const environment = useEnvironment();
@@ -244,7 +247,7 @@ function RunsList({
244247
shortcut: { key: "r" },
245248
action: (e) => {
246249
replace({
247-
bulkInspector: "true",
250+
bulkInspector: "show",
248251
action: "replay",
249252
mode: selectedItems.size > 0 ? "selected" : undefined,
250253
});
@@ -254,14 +257,21 @@ function RunsList({
254257
shortcut: { key: "c" },
255258
action: (e) => {
256259
replace({
257-
bulkInspector: "true",
260+
bulkInspector: "show",
258261
action: "cancel",
259262
mode: selectedItems.size > 0 ? "selected" : undefined,
260263
});
261264
},
262265
});
263266

264267
const isShowingBulkActionInspector = has("bulkInspector") && list.hasAnyRuns;
268+
const [isBulkInspectorPanelCollapsed, setIsBulkInspectorPanelCollapsed] = useState(
269+
!isShowingBulkActionInspector
270+
);
271+
// Keep content mounted until onCollapseChange reports the panel is fully collapsed.
272+
const showBulkInspectorContent =
273+
isShowingBulkActionInspector || !isBulkInspectorPanelCollapsed;
274+
265275
return (
266276
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
267277
<ResizablePanel id="runs-main" min={"100px"}>
@@ -310,39 +320,41 @@ function RunsList({
310320
</Button>
311321
</span>
312322
)}
313-
{!isShowingBulkActionInspector && (
314-
<LinkButton
315-
variant="secondary/small"
316-
to={v3CreateBulkActionPath(
317-
organization,
318-
project,
319-
environment,
320-
filters,
321-
selectedItems.size > 0 ? "selected" : undefined
322-
)}
323-
LeadingIcon={ListCheckedIcon}
324-
className={selectedItems.size > 0 ? "pr-1" : undefined}
325-
tooltip={
326-
<div className="-mr-1 flex items-center gap-3 text-xs text-text-dimmed">
327-
<div className="flex items-center gap-0.5">
328-
<span>Replay</span>
329-
<ShortcutKey shortcut={{ key: "r" }} variant={"small"} />
330-
</div>
331-
<div className="flex items-center gap-0.5">
332-
<span>Cancel</span>
333-
<ShortcutKey shortcut={{ key: "c" }} variant={"small"} />
334-
</div>
323+
{/* Stay mounted while the inspector is open to avoid toolbar layout shift. */}
324+
<Button
325+
variant="secondary/small"
326+
disabled={isShowingBulkActionInspector}
327+
onClick={() =>
328+
replace({
329+
bulkInspector: "show",
330+
mode: selectedItems.size > 0 ? "selected" : undefined,
331+
})
332+
}
333+
LeadingIcon={ListCheckedIcon}
334+
className={cn(
335+
selectedItems.size > 0 ? "pr-1" : undefined,
336+
isShowingBulkActionInspector && "pointer-events-none invisible"
337+
)}
338+
tooltip={
339+
<div className="-mr-1 flex items-center gap-3 text-xs text-text-dimmed">
340+
<div className="flex items-center gap-0.5">
341+
<span>Replay</span>
342+
<ShortcutKey shortcut={{ key: "r" }} variant={"small"} />
335343
</div>
336-
}
337-
>
338-
<span className="flex items-center gap-x-1 whitespace-nowrap text-text-bright">
339-
<span>Bulk action</span>
340-
{selectedItems.size > 0 && (
341-
<Badge variant="rounded">{selectedItems.size}</Badge>
342-
)}
343-
</span>
344-
</LinkButton>
345-
)}
344+
<div className="flex items-center gap-0.5">
345+
<span>Cancel</span>
346+
<ShortcutKey shortcut={{ key: "c" }} variant={"small"} />
347+
</div>
348+
</div>
349+
}
350+
>
351+
<span className="flex items-center gap-x-1 whitespace-nowrap text-text-bright">
352+
<span>Bulk action</span>
353+
{selectedItems.size > 0 && (
354+
<Badge variant="rounded">{selectedItems.size}</Badge>
355+
)}
356+
</span>
357+
</Button>
346358
<ListPagination list={list} />
347359
</div>
348360
</div>
@@ -374,12 +386,12 @@ function RunsList({
374386
className="overflow-hidden"
375387
collapsible
376388
collapsed={!isShowingBulkActionInspector}
377-
onCollapseChange={() => {}}
389+
onCollapseChange={setIsBulkInspectorPanelCollapsed}
378390
collapsedSize="0px"
379391
collapseAnimation={RESIZABLE_PANEL_ANIMATION}
380392
>
381393
<div className="h-full" style={{ minWidth: 400 }}>
382-
{isShowingBulkActionInspector && (
394+
{showBulkInspectorContent && (
383395
<CreateBulkActionInspector
384396
filters={filters}
385397
selectedItems={selectedItems}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { Navigation, ShouldRevalidateFunction } from "@remix-run/react";
2+
3+
/** Search params that only control the bulk-action inspector UI, not list data. */
4+
export const RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS = ["bulkInspector", "action", "mode"] as const;
5+
6+
/** Returns a copy with bulk-inspector UI params removed. */
7+
export function stripBulkInspectorUiParams(params: URLSearchParams): URLSearchParams {
8+
const stripped = new URLSearchParams(params);
9+
for (const key of RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS) {
10+
stripped.delete(key);
11+
}
12+
return stripped;
13+
}
14+
15+
export function searchParamsEqualIgnoringBulkInspectorUiState(
16+
current: URLSearchParams,
17+
next: URLSearchParams
18+
) {
19+
return (
20+
stripBulkInspectorUiParams(current).toString() === stripBulkInspectorUiParams(next).toString()
21+
);
22+
}
23+
24+
/** True when navigation should show the runs table loading state (excludes bulk-inspector UI toggles). */
25+
export function isRunsListLoading(navigation: Navigation, currentSearch: string): boolean {
26+
if (navigation.state === "idle" || !navigation.location) {
27+
return false;
28+
}
29+
30+
const currentParams = new URLSearchParams(currentSearch);
31+
const nextParams = new URLSearchParams(navigation.location.search);
32+
33+
if (searchParamsEqualIgnoringBulkInspectorUiState(currentParams, nextParams)) {
34+
return false;
35+
}
36+
37+
return true;
38+
}
39+
40+
/**
41+
* Skip runs list loader revalidation when only bulk-inspector UI params change.
42+
* Explicit revalidate() (unchanged URL) and filter/pagination changes still revalidate.
43+
*/
44+
export const shouldRevalidateRunsList: ShouldRevalidateFunction = ({
45+
currentUrl,
46+
nextUrl,
47+
defaultShouldRevalidate,
48+
}) => {
49+
if (currentUrl.pathname !== nextUrl.pathname) {
50+
return defaultShouldRevalidate;
51+
}
52+
53+
const currentParams = new URLSearchParams(currentUrl.search);
54+
const nextParams = new URLSearchParams(nextUrl.search);
55+
56+
if (currentParams.toString() === nextParams.toString()) {
57+
return defaultShouldRevalidate;
58+
}
59+
60+
if (searchParamsEqualIgnoringBulkInspectorUiState(currentParams, nextParams)) {
61+
return false;
62+
}
63+
64+
return defaultShouldRevalidate;
65+
};

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { useTypedFetcher } from "remix-typedjson";
44
import { useInterval } from "~/hooks/useInterval";
55
import type { NextRunListItem } from "~/presenters/v3/NextRunListPresenter.server";
66
import type { loader as liveRunsLoader } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.live";
7+
import { stripBulkInspectorUiParams } from "./shouldRevalidateRunsList";
78

8-
const RUNS_SEARCH_PARAMS_TO_REMOVE = ["cursor", "direction", "bulkInspector", "action", "mode"];
9+
const RUNS_PAGINATION_SEARCH_PARAMS = ["cursor", "direction"] as const;
910
const RUNS_POLL_INTERVAL_MS = 3000;
1011
/** Check for new runs every N poll ticks (~6s at 3s interval). */
1112
const NEW_RUNS_EVERY_N_POLL_TICKS = 2;
@@ -30,8 +31,8 @@ function maxCreatedAtMs(runs: ListedRun[]): number | undefined {
3031
}
3132

3233
function filterParamsWithoutPagination(search: string) {
33-
const params = new URLSearchParams(search);
34-
for (const key of RUNS_SEARCH_PARAMS_TO_REMOVE) {
34+
const params = stripBulkInspectorUiParams(new URLSearchParams(search));
35+
for (const key of RUNS_PAGINATION_SEARCH_PARAMS) {
3536
params.delete(key);
3637
}
3738
return params;

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
AccordionItem,
2424
AccordionTrigger,
2525
} from "~/components/primitives/Accordion";
26-
import { Button, LinkButton } from "~/components/primitives/Buttons";
26+
import { Button } from "~/components/primitives/Buttons";
2727
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
2828
import {
2929
Dialog,
@@ -51,10 +51,11 @@ import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/m
5151
import { findProjectBySlug } from "~/models/project.server";
5252
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
5353
import { CreateBulkActionPresenter } from "~/presenters/v3/CreateBulkActionPresenter.server";
54+
import { RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/shouldRevalidateRunsList";
5455
import { logger } from "~/services/logger.server";
5556
import { requireUserId } from "~/services/session.server";
5657
import { cn } from "~/utils/cn";
57-
import { EnvironmentParamSchema, v3BulkActionPath, v3RunsPath } from "~/utils/pathBuilder";
58+
import { EnvironmentParamSchema, v3BulkActionPath } from "~/utils/pathBuilder";
5859
import { BulkActionService } from "~/v3/services/bulk/BulkActionV2.server";
5960

6061
export async function loader({ request, params }: LoaderFunctionArgs) {
@@ -187,7 +188,7 @@ export function CreateBulkActionInspector({
187188
const project = useProject();
188189
const environment = useEnvironment();
189190
const fetcher = useTypedFetcher<typeof loader>();
190-
const { value, replace } = useSearchParams();
191+
const { value, replace, del } = useSearchParams();
191192
const [action, setAction] = useState<BulkActionAction>(
192193
bulkActionActionFromString(value("action"))
193194
);
@@ -208,9 +209,6 @@ export function CreateBulkActionInspector({
208209

209210
const data = fetcher.data != null ? fetcher.data : undefined;
210211

211-
const closedSearchParams = new URLSearchParams(location.search);
212-
closedSearchParams.delete("bulkInspector");
213-
214212
const impactedCountElement =
215213
mode === "selected" ? selectedItems.size : <EstimatedCount count={data?.count} />;
216214

@@ -225,13 +223,10 @@ export function CreateBulkActionInspector({
225223
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
226224
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
227225
<Header2 className="whitespace-nowrap">Create a bulk action</Header2>
228-
<LinkButton
229-
to={`${v3RunsPath(
230-
organization,
231-
project,
232-
environment
233-
)}?${closedSearchParams.toString()}`}
226+
<Button
227+
type="button"
234228
variant="minimal/small"
229+
onClick={() => del([...RUNS_BULK_INSPECTOR_UI_SEARCH_PARAMS])}
235230
TrailingIcon={ExitIcon}
236231
shortcut={{ key: "esc" }}
237232
shortcutPosition="before-trailing-icon"

0 commit comments

Comments
 (0)