Skip to content

Commit e4982bf

Browse files
authored
feat(webapp): deployments page live reloading (#2524)
* Fix `current` badge inconsistency in the deployment details page * Add custom hook for auto revalidation based on an interval and/or focus change * Use the autoRevalidate hook for live reloading of the deployments page * Extract autoReloadPollIntervalMs to an env var * Replace the sse-based autoreload in bulk actions and queues page with the simpler autoRevalidate hook
1 parent a03783d commit e4982bf

File tree

8 files changed

+94
-222
lines changed

8 files changed

+94
-222
lines changed

apps/webapp/app/env.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,8 +1028,9 @@ const EnvironmentSchema = z
10281028
TASK_EVENT_PARTITIONING_ENABLED: z.string().default("0"),
10291029
TASK_EVENT_PARTITIONED_WINDOW_IN_SECONDS: z.coerce.number().int().default(60), // 1 minute
10301030

1031-
QUEUE_SSE_AUTORELOAD_INTERVAL_MS: z.coerce.number().int().default(5_000),
1032-
QUEUE_SSE_AUTORELOAD_TIMEOUT_MS: z.coerce.number().int().default(60_000),
1031+
DEPLOYMENTS_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(5_000),
1032+
BULK_ACTION_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(1_000),
1033+
QUEUES_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(5_000),
10331034

10341035
SLACK_BOT_TOKEN: z.string().optional(),
10351036
SLACK_SIGNUP_REASON_CHANNEL_ID: z.string().optional(),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useRevalidator } from "@remix-run/react";
2+
import { useEffect } from "react";
3+
4+
type UseAutoRevalidateOptions = {
5+
interval?: number; // in milliseconds
6+
onFocus?: boolean;
7+
disabled?: boolean;
8+
};
9+
10+
export function useAutoRevalidate(options: UseAutoRevalidateOptions = {}) {
11+
const { interval = 5000, onFocus = true, disabled = false } = options;
12+
const revalidator = useRevalidator();
13+
14+
useEffect(() => {
15+
if (!interval || interval <= 0 || disabled) return;
16+
17+
const intervalId = setInterval(() => {
18+
if (revalidator.state === "loading") {
19+
return;
20+
}
21+
revalidator.revalidate();
22+
}, interval);
23+
24+
return () => clearInterval(intervalId);
25+
}, [interval, disabled]);
26+
27+
useEffect(() => {
28+
if (!onFocus || disabled) return;
29+
30+
const handleFocus = () => {
31+
if (document.visibilityState === "visible" && revalidator.state !== "loading") {
32+
revalidator.revalidate();
33+
}
34+
};
35+
36+
// Revalidate when the page becomes visible
37+
document.addEventListener("visibilitychange", handleFocus);
38+
// Revalidate when the window gains focus
39+
window.addEventListener("focus", handleFocus);
40+
41+
return () => {
42+
document.removeEventListener("visibilitychange", handleFocus);
43+
window.removeEventListener("focus", handleFocus);
44+
};
45+
}, [onFocus, disabled]);
46+
47+
return revalidator;
48+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { ArrowPathIcon } from "@heroicons/react/20/solid";
2-
import { Form, useRevalidator } from "@remix-run/react";
2+
import { Form } from "@remix-run/react";
33
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
44
import { tryCatch } from "@trigger.dev/core";
55
import type { BulkActionType } from "@trigger.dev/database";
66
import { motion } from "framer-motion";
7-
import { useEffect } from "react";
87
import { typedjson, useTypedLoaderData } from "remix-typedjson";
98
import { z } from "zod";
109
import { ExitIcon } from "~/assets/icons/ExitIcon";
@@ -18,8 +17,9 @@ import { Paragraph } from "~/components/primitives/Paragraph";
1817
import * as Property from "~/components/primitives/PropertyTable";
1918
import { BulkActionStatusCombo, BulkActionTypeCombo } from "~/components/runs/v3/BulkAction";
2019
import { UserAvatar } from "~/components/UserProfilePhoto";
20+
import { env } from "~/env.server";
21+
import { useAutoRevalidate } from "~/hooks/useAutoRevalidate";
2122
import { useEnvironment } from "~/hooks/useEnvironment";
22-
import { useEventSource } from "~/hooks/useEventSource";
2323
import { useOrganization } from "~/hooks/useOrganizations";
2424
import { useProject } from "~/hooks/useProject";
2525
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
@@ -72,7 +72,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7272
throw new Error(error.message);
7373
}
7474

75-
return typedjson({ bulkAction: data });
75+
const autoReloadPollIntervalMs = env.BULK_ACTION_AUTORELOAD_POLL_INTERVAL_MS;
76+
77+
return typedjson({ bulkAction: data, autoReloadPollIntervalMs });
7678
} catch (error) {
7779
console.error(error);
7880
throw new Response(undefined, {
@@ -130,30 +132,16 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
130132
};
131133

132134
export default function Page() {
133-
const { bulkAction } = useTypedLoaderData<typeof loader>();
135+
const { bulkAction, autoReloadPollIntervalMs } = useTypedLoaderData<typeof loader>();
134136
const organization = useOrganization();
135137
const project = useProject();
136138
const environment = useEnvironment();
137139

138-
const disabled = bulkAction.status !== "PENDING";
139-
140-
const streamedEvents = useEventSource(
141-
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.id}/runs/bulkaction/${bulkAction.friendlyId}/stream`,
142-
{
143-
event: "progress",
144-
disabled,
145-
}
146-
);
147-
148-
const revalidation = useRevalidator();
149-
150-
useEffect(() => {
151-
if (disabled || streamedEvents === null) {
152-
return;
153-
}
154-
155-
revalidation.revalidate();
156-
}, [streamedEvents, disabled]);
140+
useAutoRevalidate({
141+
interval: autoReloadPollIntervalMs,
142+
onFocus: true,
143+
disabled: bulkAction.status !== "PENDING",
144+
});
157145

158146
return (
159147
<div className="grid h-full max-h-full grid-rows-[2.5rem_2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
44
import { ExitIcon } from "~/assets/icons/ExitIcon";
55
import { GitMetadata } from "~/components/GitMetadata";
66
import { RuntimeIcon } from "~/components/RuntimeIcon";
7-
import { UserAvatar } from "~/components/UserProfilePhoto";
87
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
98
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
109
import { Badge } from "~/components/primitives/Badge";
@@ -132,7 +131,11 @@ export default function Page() {
132131
<Property.Label>Deploy</Property.Label>
133132
<Property.Value className="flex items-center gap-2">
134133
<span>{deployment.shortCode}</span>
135-
{deployment.label && <Badge variant="outline-rounded">{deployment.label}</Badge>}
134+
{deployment.label && (
135+
<Badge variant="extra-small" className="capitalize">
136+
{deployment.label}
137+
</Badge>
138+
)}
136139
</Property.Value>
137140
</Property.Item>
138141
<Property.Item>

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import {
6161
} from "~/utils/pathBuilder";
6262
import { createSearchParams } from "~/utils/searchParams";
6363
import { compareDeploymentVersions } from "~/v3/utils/deploymentVersions";
64+
import { useAutoRevalidate } from "~/hooks/useAutoRevalidate";
65+
import { env } from "~/env.server";
6466

6567
export const meta: MetaFunction = () => {
6668
return [
@@ -116,7 +118,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
116118
? result.deployments.find((d) => d.version === version)
117119
: undefined;
118120

119-
return typedjson({ ...result, selectedDeployment });
121+
const autoReloadPollIntervalMs = env.DEPLOYMENTS_AUTORELOAD_POLL_INTERVAL_MS;
122+
123+
return typedjson({ ...result, selectedDeployment, autoReloadPollIntervalMs });
120124
} catch (error) {
121125
console.error(error);
122126
throw new Response(undefined, {
@@ -137,13 +141,16 @@ export default function Page() {
137141
selectedDeployment,
138142
connectedGithubRepository,
139143
environmentGitHubBranch,
144+
autoReloadPollIntervalMs,
140145
} = useTypedLoaderData<typeof loader>();
141146
const hasDeployments = totalPages > 0;
142147

143148
const { deploymentParam } = useParams();
144149
const location = useLocation();
145150
const navigate = useNavigate();
146151

152+
useAutoRevalidate({ interval: autoReloadPollIntervalMs, onFocus: true });
153+
147154
// If we have a selected deployment from the version param, show it
148155
useEffect(() => {
149156
if (selectedDeployment && !deploymentParam) {

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

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,7 @@ import {
88
RectangleStackIcon,
99
} from "@heroicons/react/20/solid";
1010
import { DialogClose } from "@radix-ui/react-dialog";
11-
import {
12-
Form,
13-
useNavigate,
14-
useNavigation,
15-
useRevalidator,
16-
useSearchParams,
17-
type MetaFunction,
18-
} from "@remix-run/react";
11+
import { Form, useNavigation, useSearchParams, type MetaFunction } from "@remix-run/react";
1912
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
2013
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
2114
import { useEffect, useState } from "react";
@@ -30,7 +23,7 @@ import { Feedback } from "~/components/Feedback";
3023
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
3124
import { BigNumber } from "~/components/metrics/BigNumber";
3225
import { Badge } from "~/components/primitives/Badge";
33-
import { Button, ButtonVariant, LinkButton } from "~/components/primitives/Buttons";
26+
import { Button, type ButtonVariant, LinkButton } from "~/components/primitives/Buttons";
3427
import { Callout } from "~/components/primitives/Callout";
3528
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
3629
import { FormButtons } from "~/components/primitives/FormButtons";
@@ -56,7 +49,6 @@ import {
5649
TooltipTrigger,
5750
} from "~/components/primitives/Tooltip";
5851
import { useEnvironment } from "~/hooks/useEnvironment";
59-
import { useEventSource } from "~/hooks/useEventSource";
6052
import { useOrganization } from "~/hooks/useOrganizations";
6153
import { useProject } from "~/hooks/useProject";
6254
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
@@ -74,6 +66,8 @@ import { Header3 } from "~/components/primitives/Headers";
7466
import { Input } from "~/components/primitives/Input";
7567
import { useThrottle } from "~/hooks/useThrottle";
7668
import { RunsIcon } from "~/assets/icons/RunsIcon";
69+
import { useAutoRevalidate } from "~/hooks/useAutoRevalidate";
70+
import { env } from "~/env.server";
7771

7872
const SearchParamsSchema = z.object({
7973
query: z.string().optional(),
@@ -121,9 +115,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
121115

122116
const environmentQueuePresenter = new EnvironmentQueuePresenter();
123117

118+
const autoReloadPollIntervalMs = env.QUEUES_AUTORELOAD_POLL_INTERVAL_MS;
119+
124120
return typedjson({
125121
...queues,
126122
environment: await environmentQueuePresenter.call(environment),
123+
autoReloadPollIntervalMs,
127124
});
128125
} catch (error) {
129126
console.error(error);
@@ -217,28 +214,23 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
217214
};
218215

219216
export default function Page() {
220-
const { environment, queues, success, pagination, code, totalQueues, hasFilters } =
221-
useTypedLoaderData<typeof loader>();
217+
const {
218+
environment,
219+
queues,
220+
success,
221+
pagination,
222+
code,
223+
totalQueues,
224+
hasFilters,
225+
autoReloadPollIntervalMs,
226+
} = useTypedLoaderData<typeof loader>();
222227

223228
const organization = useOrganization();
224229
const project = useProject();
225230
const env = useEnvironment();
226231
const plan = useCurrentPlan();
227232

228-
// Reload the page periodically
229-
const streamedEvents = useEventSource(
230-
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${env.slug}/queues/stream`,
231-
{
232-
event: "update",
233-
}
234-
);
235-
236-
const revalidation = useRevalidator();
237-
useEffect(() => {
238-
if (streamedEvents) {
239-
revalidation.revalidate();
240-
}
241-
}, [streamedEvents]);
233+
useAutoRevalidate({ interval: autoReloadPollIntervalMs, onFocus: true });
242234

243235
const limitStatus =
244236
environment.running === environment.concurrencyLimit * environment.burstFactor

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.stream.tsx

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)