Skip to content

Commit ca95a03

Browse files
committed
refactor(webapp): extract shared TaskRunsList for both task pages
1 parent 4861ad5 commit ca95a03

3 files changed

Lines changed: 126 additions & 253 deletions

File tree

  • apps/webapp/app
    • components/runs/v3
    • routes
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useLocation, useNavigation, useRevalidator } from "@remix-run/react";
2+
import { type MutableRefObject, useEffect } from "react";
3+
import { Button } from "~/components/primitives/Buttons";
4+
import { PulsingDot } from "~/components/primitives/PulsingDot";
5+
import { useEnvironment } from "~/hooks/useEnvironment";
6+
import { useOrganization } from "~/hooks/useOrganizations";
7+
import { useProject } from "~/hooks/useProject";
8+
import { useSearchParams } from "~/hooks/useSearchParam";
9+
import type { NextRunList } from "~/presenters/v3/NextRunListPresenter.server";
10+
import { useRunsLiveReload } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload";
11+
import { TaskRunsTable } from "./TaskRunsTable";
12+
13+
/**
14+
* Compact "N new runs" button, shown in a task page's header to the left of the
15+
* time filter when the live-reload hook has detected newer runs.
16+
*/
17+
export function NewRunsButton({ count, onClick }: { count: number; onClick: () => void }) {
18+
return (
19+
<span className="flex duration-150 animate-in fade-in-0">
20+
<Button
21+
variant="secondary/small"
22+
className="text-text-bright"
23+
onClick={onClick}
24+
LeadingIcon={<PulsingDot className="h-2 w-2" />}
25+
tooltip="Refresh to see new runs"
26+
aria-label="New runs created. Refresh to see new runs."
27+
>
28+
{count >= 100 ? "99+ new runs" : `${count} new ${count === 1 ? "run" : "runs"}`}
29+
</Button>
30+
</span>
31+
);
32+
}
33+
34+
/**
35+
* Runs table with live updating, shared by the standard and scheduled task
36+
* landing pages. Mirrors the Runs list page: active rows are patched in place
37+
* (status/timing/cost). The "N new runs" count is surfaced to the top-bar
38+
* button via `onNewRunsCountChange` (count drives visibility) and
39+
* `showNewRunsRef` (the latest click action), since the button lives outside
40+
* this deferred boundary. The task lives in the route path rather than a
41+
* `tasks` filter, so we pass `taskSlug` to scope new-run detection to this task.
42+
*/
43+
export function TaskRunsList({
44+
list,
45+
taskSlug,
46+
onNewRunsCountChange,
47+
showNewRunsRef,
48+
}: {
49+
list: NextRunList;
50+
taskSlug: string;
51+
onNewRunsCountChange: (count: number) => void;
52+
showNewRunsRef: MutableRefObject<() => void>;
53+
}) {
54+
const organization = useOrganization();
55+
const project = useProject();
56+
const environment = useEnvironment();
57+
const navigation = useNavigation();
58+
const location = useLocation();
59+
const { has, replace } = useSearchParams();
60+
const revalidator = useRevalidator();
61+
62+
// Loading a new version of this same page (time filter / pagination change).
63+
const isLoading =
64+
navigation.state === "loading" &&
65+
navigation.location !== undefined &&
66+
navigation.location.pathname === location.pathname &&
67+
navigation.location.search !== location.search;
68+
69+
const { visibleRuns, newRunsCount, dismissNewRuns, childrenStatusesBasePath } = useRunsLiveReload(
70+
{
71+
runs: list.runs,
72+
hasAnyRuns: list.hasAnyRuns,
73+
isLoading,
74+
organizationSlug: organization.slug,
75+
projectSlug: project.slug,
76+
environmentSlug: environment.slug,
77+
taskSlug,
78+
}
79+
);
80+
81+
const onClickShowNewRuns = () => {
82+
const isPaginated = has("cursor") || has("direction");
83+
dismissNewRuns();
84+
if (isPaginated) {
85+
replace({ cursor: undefined, direction: undefined });
86+
return;
87+
}
88+
revalidator.revalidate();
89+
};
90+
91+
// Surface the banner to the top-bar button rendered by the page: keep the
92+
// ref's action current, mirror the count up, and clear it when this boundary
93+
// unmounts (e.g. the table re-suspends on a filter change).
94+
useEffect(() => {
95+
showNewRunsRef.current = onClickShowNewRuns;
96+
}, [onClickShowNewRuns, showNewRunsRef]);
97+
useEffect(() => {
98+
onNewRunsCountChange(newRunsCount);
99+
}, [newRunsCount, onNewRunsCountChange]);
100+
useEffect(() => () => onNewRunsCountChange(0), [onNewRunsCountChange]);
101+
102+
return (
103+
<div className="h-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
104+
<TaskRunsTable
105+
total={visibleRuns.length}
106+
hasFilters={list.hasFilters}
107+
filters={list.filters}
108+
runs={visibleRuns}
109+
childrenStatusesBasePath={childrenStatusesBasePath}
110+
isLoading={isLoading}
111+
variant="dimmed"
112+
showTopBorder={false}
113+
stickyHeader
114+
/>
115+
</div>
116+
);
117+
}

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

Lines changed: 4 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,8 @@
11
import { BookOpenIcon, PlusIcon } from "@heroicons/react/20/solid";
2-
import {
3-
useFetcher,
4-
useLocation,
5-
useNavigation,
6-
useRevalidator,
7-
type MetaFunction,
8-
} from "@remix-run/react";
2+
import { useFetcher, useRevalidator, type MetaFunction } from "@remix-run/react";
93
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
10-
import {
11-
type MutableRefObject,
12-
Suspense,
13-
useCallback,
14-
useEffect,
15-
useMemo,
16-
useRef,
17-
useState,
18-
} from "react";
19-
import {
20-
TypedAwait,
21-
typeddefer,
22-
type UseDataFunctionReturn,
23-
useTypedFetcher,
24-
useTypedLoaderData,
25-
} from "remix-typedjson";
4+
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
5+
import { TypedAwait, typeddefer, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
266
import { z } from "zod";
277
import { BeakerIcon } from "~/assets/icons/BeakerIcon";
288
import { ClockIcon } from "~/assets/icons/ClockIcon";
@@ -52,7 +32,6 @@ import { InfoPanel } from "~/components/primitives/InfoPanel";
5232
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
5333
import { PaginationControls } from "~/components/primitives/Pagination";
5434
import { Paragraph } from "~/components/primitives/Paragraph";
55-
import { PulsingDot } from "~/components/primitives/PulsingDot";
5635
import * as Property from "~/components/primitives/PropertyTable";
5736
import {
5837
ResizableHandle,
@@ -77,7 +56,6 @@ import { EnabledStatus } from "~/components/runs/v3/EnabledStatus";
7756
import type { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
7857
import { ScheduleTypeIcon, scheduleTypeName } from "~/components/runs/v3/ScheduleType";
7958
import { TimeFilter, timeFilterFromTo } from "~/components/runs/v3/SharedFilters";
80-
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
8159
import { ScheduleInspector } from "~/components/schedules/ScheduleInspector";
8260
import { ScheduleLimitActions } from "~/components/schedules/ScheduleLimitActions";
8361
import { SchedulesUsageBar } from "~/components/schedules/SchedulesUsageBar";
@@ -116,7 +94,7 @@ import type { loader as scheduleEditLoader } from "../_app.orgs.$organizationSlu
11694
import type { loader as scheduleNewLoader } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route";
11795
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
11896
import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route";
119-
import { useRunsLiveReload } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload";
97+
import { NewRunsButton, TaskRunsList } from "~/components/runs/v3/TaskRunsList";
12098

12199
export const meta: MetaFunction<typeof loader> = ({ data }) => {
122100
const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug;
@@ -425,113 +403,6 @@ export default function Page() {
425403
);
426404
}
427405

428-
type TaskRunList = NonNullable<Awaited<UseDataFunctionReturn<typeof loader>["runList"]>>;
429-
430-
/**
431-
* Compact "N new runs" button, shown in the page top bar to the left of the
432-
* time filter when the live-reload hook has detected newer runs.
433-
*/
434-
function NewRunsButton({ count, onClick }: { count: number; onClick: () => void }) {
435-
return (
436-
<span className="flex duration-150 animate-in fade-in-0">
437-
<Button
438-
variant="secondary/small"
439-
className="text-text-bright"
440-
onClick={onClick}
441-
LeadingIcon={<PulsingDot className="h-2 w-2" />}
442-
tooltip="Refresh to see new runs"
443-
aria-label="New runs created. Refresh to see new runs."
444-
>
445-
{count >= 100 ? "99+ new runs" : `${count} new ${count === 1 ? "run" : "runs"}`}
446-
</Button>
447-
</span>
448-
);
449-
}
450-
451-
/**
452-
* Runs table with live updating. Mirrors the Runs list page: active rows are
453-
* patched in place (status/timing/cost). The "N new runs" count is surfaced to
454-
* the top-bar button via `onNewRunsCountChange` (count → visibility) and
455-
* `showNewRunsRef` (the latest click action), since the button lives outside
456-
* this deferred boundary. The task lives in the route path rather than a
457-
* `tasks` filter, so we pass `taskSlug` to scope new-run detection to this task.
458-
*/
459-
function TaskRunsList({
460-
list,
461-
taskSlug,
462-
onNewRunsCountChange,
463-
showNewRunsRef,
464-
}: {
465-
list: TaskRunList;
466-
taskSlug: string;
467-
onNewRunsCountChange: (count: number) => void;
468-
showNewRunsRef: MutableRefObject<() => void>;
469-
}) {
470-
const organization = useOrganization();
471-
const project = useProject();
472-
const environment = useEnvironment();
473-
const navigation = useNavigation();
474-
const location = useLocation();
475-
const { has, replace } = useSearchParams();
476-
const revalidator = useRevalidator();
477-
478-
// Loading a new version of this same page (time filter / pagination change).
479-
const isLoading =
480-
navigation.state === "loading" &&
481-
navigation.location !== undefined &&
482-
navigation.location.pathname === location.pathname &&
483-
navigation.location.search !== location.search;
484-
485-
const { visibleRuns, newRunsCount, dismissNewRuns, childrenStatusesBasePath } = useRunsLiveReload(
486-
{
487-
runs: list.runs,
488-
hasAnyRuns: list.hasAnyRuns,
489-
isLoading,
490-
organizationSlug: organization.slug,
491-
projectSlug: project.slug,
492-
environmentSlug: environment.slug,
493-
taskSlug,
494-
}
495-
);
496-
497-
const onClickShowNewRuns = () => {
498-
const isPaginated = has("cursor") || has("direction");
499-
dismissNewRuns();
500-
if (isPaginated) {
501-
replace({ cursor: undefined, direction: undefined });
502-
return;
503-
}
504-
revalidator.revalidate();
505-
};
506-
507-
// Surface the banner to the top-bar button rendered by Page: keep the ref's
508-
// action current, mirror the count up, and clear it when this boundary
509-
// unmounts (e.g. the table re-suspends on a filter change).
510-
useEffect(() => {
511-
showNewRunsRef.current = onClickShowNewRuns;
512-
}, [onClickShowNewRuns, showNewRunsRef]);
513-
useEffect(() => {
514-
onNewRunsCountChange(newRunsCount);
515-
}, [newRunsCount, onNewRunsCountChange]);
516-
useEffect(() => () => onNewRunsCountChange(0), [onNewRunsCountChange]);
517-
518-
return (
519-
<div className="h-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
520-
<TaskRunsTable
521-
total={visibleRuns.length}
522-
hasFilters={list.hasFilters}
523-
filters={list.filters}
524-
runs={visibleRuns}
525-
childrenStatusesBasePath={childrenStatusesBasePath}
526-
isLoading={isLoading}
527-
variant="dimmed"
528-
showTopBorder={false}
529-
stickyHeader
530-
/>
531-
</div>
532-
);
533-
}
534-
535406
/**
536407
* "Create schedule" button with a limit-exceeded intercept. When the project
537408
* is already at its schedules limit, clicking opens a dialog explaining the

0 commit comments

Comments
 (0)