|
| 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 | +} |
0 commit comments