Skip to content

Commit 07dd634

Browse files
committed
Moved things around and use Context
1 parent c9e6a6d commit 07dd634

File tree

5 files changed

+134
-101
lines changed

5 files changed

+134
-101
lines changed
Lines changed: 40 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
1-
import { useEffect, useState } from "react";
2-
import { ConnectedIcon, DisconnectedIcon } from "~/assets/icons/ConnectionIcons";
1+
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
32
import { useDebounce } from "~/hooks/useDebounce";
43
import { useEnvironment } from "~/hooks/useEnvironment";
54
import { useEventSource } from "~/hooks/useEventSource";
65
import { useOrganization } from "~/hooks/useOrganizations";
76
import { useProject } from "~/hooks/useProject";
8-
import {
9-
Dialog,
10-
DialogContent,
11-
DialogFooter,
12-
DialogHeader,
13-
DialogTrigger,
14-
} from "./primitives/Dialog";
15-
import { Button, LinkButton } from "./primitives/Buttons";
16-
import connectedImage from "../assets/images/cli-connected.png";
17-
import disconnectedImage from "../assets/images/cli-disconnected.png";
18-
import { Paragraph } from "./primitives/Paragraph";
19-
import { PackageManagerProvider, TriggerDevStepV3 } from "./SetupCommands";
20-
import { docsPath } from "~/utils/pathBuilder";
21-
import { BookOpenIcon } from "@heroicons/react/20/solid";
227

23-
export function useDevPresence() {
8+
// Define Context types
9+
type DevPresenceContextType = {
10+
lastSeen: Date | null;
11+
isConnected: boolean;
12+
};
13+
14+
// Create Context with default values
15+
const DevPresenceContext = createContext<DevPresenceContextType>({
16+
lastSeen: null,
17+
isConnected: false,
18+
});
19+
20+
// Provider component with enabled prop
21+
interface DevPresenceProviderProps {
22+
children: ReactNode;
23+
enabled?: boolean;
24+
}
25+
26+
export function DevPresenceProvider({ children, enabled = true }: DevPresenceProviderProps) {
2427
const organization = useOrganization();
2528
const project = useProject();
2629
const environment = useEnvironment();
30+
31+
// Only subscribe to event source if enabled is true
2732
const streamedEvents = useEventSource(
2833
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/dev/presence`,
2934
{
3035
event: "presence",
36+
disabled: !enabled,
3137
}
3238
);
3339

@@ -38,15 +44,15 @@ export function useDevPresence() {
3844
}, 3_000);
3945

4046
useEffect(() => {
41-
if (streamedEvents === null) {
47+
// If disabled or no events, set lastSeen to null
48+
if (!enabled || streamedEvents === null) {
4249
debouncer(null);
4350
return;
4451
}
4552

4653
try {
4754
const data = JSON.parse(streamedEvents) as any;
4855
if ("lastSeen" in data && data.lastSeen) {
49-
// Parse the timestamp string into a Date object
5056
try {
5157
const lastSeenDate = new Date(data.lastSeen);
5258
debouncer(lastSeenDate);
@@ -61,68 +67,22 @@ export function useDevPresence() {
6167
console.log("DevPresence: Failed to parse presence message", { error });
6268
debouncer(null);
6369
}
64-
}, [streamedEvents]);
70+
}, [streamedEvents, enabled]);
6571

66-
return { lastSeen };
67-
}
72+
// Calculate isConnected and memoize the context value
73+
const contextValue = useMemo(() => {
74+
const isConnected = enabled && lastSeen !== null && lastSeen > new Date(Date.now() - 120_000);
75+
return { lastSeen, isConnected };
76+
}, [lastSeen, enabled]);
6877

69-
export function DevPresence() {
70-
const { lastSeen } = useDevPresence();
71-
const isConnected = lastSeen && lastSeen > new Date(Date.now() - 120_000);
78+
return <DevPresenceContext.Provider value={contextValue}>{children}</DevPresenceContext.Provider>;
79+
}
7280

73-
return (
74-
<Dialog>
75-
<DialogTrigger asChild>
76-
<Button
77-
variant="minimal/small"
78-
className="px-1"
79-
LeadingIcon={
80-
isConnected ? (
81-
<ConnectedIcon className="size-5" />
82-
) : (
83-
<DisconnectedIcon className="size-5" />
84-
)
85-
}
86-
/>
87-
</DialogTrigger>
88-
<DialogContent>
89-
<DialogHeader>
90-
{isConnected
91-
? "Your dev server is connected to Trigger.dev"
92-
: "Your dev server is not connected to Trigger.dev"}
93-
</DialogHeader>
94-
<div className="mt-2 flex flex-col gap-3 px-2">
95-
<div className="flex flex-col items-center justify-center gap-6 px-6 py-10">
96-
<img
97-
src={isConnected ? connectedImage : disconnectedImage}
98-
alt={isConnected ? "Connected" : "Disconnected"}
99-
width={282}
100-
height={45}
101-
/>
102-
<Paragraph variant="small" className={isConnected ? "text-success" : "text-error"}>
103-
{isConnected
104-
? "Your local dev server is connected to Trigger.dev"
105-
: "Your local dev server is not connected to Trigger.dev"}
106-
</Paragraph>
107-
</div>
108-
{isConnected ? null : (
109-
<div className="space-y-3">
110-
<PackageManagerProvider>
111-
<TriggerDevStepV3 />
112-
</PackageManagerProvider>
113-
<Paragraph variant="small">
114-
Run this CLI `dev` command to connect to the Trigger.dev servers to start developing
115-
locally. Keep it running while you develop to stay connected.
116-
</Paragraph>
117-
</div>
118-
)}
119-
</div>
120-
<DialogFooter>
121-
<LinkButton variant="tertiary/medium" LeadingIcon={BookOpenIcon} to={docsPath("cli-dev")}>
122-
CLI docs
123-
</LinkButton>
124-
</DialogFooter>
125-
</DialogContent>
126-
</Dialog>
127-
);
81+
// Custom hook to use the context
82+
export function useDevPresence() {
83+
const context = useContext(DevPresenceContext);
84+
if (context === undefined) {
85+
throw new Error("useDevPresence must be used within a DevPresenceProvider");
86+
}
87+
return context;
12888
}

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ArrowRightOnRectangleIcon,
44
BeakerIcon,
55
BellAlertIcon,
6+
BookOpenIcon,
67
ChartBarIcon,
78
ChevronRightIcon,
89
ClockIcon,
@@ -20,6 +21,7 @@ import {
2021
import { useNavigation } from "@remix-run/react";
2122
import { useEffect, useRef, useState, type ReactNode } from "react";
2223
import simplur from "simplur";
24+
import { ConnectedIcon, DisconnectedIcon } from "~/assets/icons/ConnectionIcons";
2325
import { RunsIcon } from "~/assets/icons/RunsIcon";
2426
import { TaskIcon } from "~/assets/icons/TaskIcon";
2527
import { Avatar } from "~/components/primitives/Avatar";
@@ -32,6 +34,7 @@ import { type FeedbackType } from "~/routes/resources.feedback";
3234
import { cn } from "~/utils/cn";
3335
import {
3436
accountPath,
37+
docsPath,
3538
logoutPath,
3639
newOrganizationPath,
3740
newProjectPath,
@@ -53,9 +56,21 @@ import {
5356
v3TestPath,
5457
v3UsagePath,
5558
} from "~/utils/pathBuilder";
59+
import { useDevPresence } from "../DevPresence";
5660
import { ImpersonationBanner } from "../ImpersonationBanner";
61+
import { PackageManagerProvider, TriggerDevStepV3 } from "../SetupCommands";
5762
import { UserProfilePhoto } from "../UserProfilePhoto";
63+
import connectedImage from "../../assets/images/cli-connected.png";
64+
import disconnectedImage from "../../assets/images/cli-disconnected.png";
5865
import { FreePlanUsage } from "../billing/FreePlanUsage";
66+
import { Button, ButtonContent, LinkButton } from "../primitives/Buttons";
67+
import {
68+
Dialog,
69+
DialogContent,
70+
DialogFooter,
71+
DialogHeader,
72+
DialogTrigger,
73+
} from "../primitives/Dialog";
5974
import { Paragraph } from "../primitives/Paragraph";
6075
import {
6176
Popover,
@@ -64,14 +79,12 @@ import {
6479
PopoverMenuItem,
6580
PopoverTrigger,
6681
} from "../primitives/Popover";
82+
import { TextLink } from "../primitives/TextLink";
6783
import { EnvironmentSelector } from "./EnvironmentSelector";
6884
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
6985
import { SideMenuHeader } from "./SideMenuHeader";
7086
import { SideMenuItem } from "./SideMenuItem";
7187
import { SideMenuSection } from "./SideMenuSection";
72-
import { ButtonContent, LinkButton } from "../primitives/Buttons";
73-
import { TextLink } from "../primitives/TextLink";
74-
import { DevPresence } from "../DevPresence";
7588

7689
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
7790
export type SideMenuProject = Pick<
@@ -148,7 +161,7 @@ export function SideMenu({
148161
project={project}
149162
environment={environment}
150163
/>
151-
{environment.type === "DEVELOPMENT" && <DevPresence />}
164+
{environment.type === "DEVELOPMENT" && <DevConnection />}
152165
</div>
153166
</div>
154167

@@ -496,3 +509,63 @@ function SelectorDivider() {
496509
</svg>
497510
);
498511
}
512+
513+
export function DevConnection() {
514+
const { isConnected } = useDevPresence();
515+
516+
return (
517+
<Dialog>
518+
<DialogTrigger asChild>
519+
<Button
520+
variant="minimal/small"
521+
className="px-1"
522+
LeadingIcon={
523+
isConnected ? (
524+
<ConnectedIcon className="size-5" />
525+
) : (
526+
<DisconnectedIcon className="size-5" />
527+
)
528+
}
529+
/>
530+
</DialogTrigger>
531+
<DialogContent>
532+
<DialogHeader>
533+
{isConnected
534+
? "Your dev server is connected to Trigger.dev"
535+
: "Your dev server is not connected to Trigger.dev"}
536+
</DialogHeader>
537+
<div className="mt-2 flex flex-col gap-3 px-2">
538+
<div className="flex flex-col items-center justify-center gap-6 px-6 py-10">
539+
<img
540+
src={isConnected ? connectedImage : disconnectedImage}
541+
alt={isConnected ? "Connected" : "Disconnected"}
542+
width={282}
543+
height={45}
544+
/>
545+
<Paragraph variant="small" className={isConnected ? "text-success" : "text-error"}>
546+
{isConnected
547+
? "Your local dev server is connected to Trigger.dev"
548+
: "Your local dev server is not connected to Trigger.dev"}
549+
</Paragraph>
550+
</div>
551+
{isConnected ? null : (
552+
<div className="space-y-3">
553+
<PackageManagerProvider>
554+
<TriggerDevStepV3 />
555+
</PackageManagerProvider>
556+
<Paragraph variant="small">
557+
Run this CLI `dev` command to connect to the Trigger.dev servers to start developing
558+
locally. Keep it running while you develop to stay connected.
559+
</Paragraph>
560+
</div>
561+
)}
562+
</div>
563+
<DialogFooter>
564+
<LinkButton variant="tertiary/medium" LeadingIcon={BookOpenIcon} to={docsPath("cli-dev")}>
565+
CLI docs
566+
</LinkButton>
567+
</DialogFooter>
568+
</DialogContent>
569+
</Dialog>
570+
);
571+
}

apps/webapp/app/hooks/useEventSource.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ export function useEventSource(
2323
return;
2424
}
2525

26-
const eventSource = new EventSource(url, init);
27-
eventSource.addEventListener(event ?? "message", handler);
28-
2926
// reset data if dependencies change
3027
setData(null);
3128

29+
const eventSource = new EventSource(url, init);
30+
eventSource.addEventListener(event ?? "message", handler);
31+
3232
function handler(event: MessageEvent) {
3333
setData(event.data || "UNKNOWN_EVENT_DATA");
3434
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Outlet } from "@remix-run/react";
2+
import { DevPresenceProvider } from "~/components/DevPresence";
23
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
34
import { MainBody } from "~/components/layout/AppLayout";
45
import { SideMenu } from "~/components/navigation/SideMenu";
@@ -29,16 +30,18 @@ export default function Project() {
2930
return (
3031
<>
3132
<div className="grid grid-cols-[14rem_1fr] overflow-hidden">
32-
<SideMenu
33-
user={{ ...user, isImpersonating }}
34-
project={project}
35-
environment={environment}
36-
organization={organization}
37-
organizations={organizations}
38-
/>
39-
<MainBody>
40-
<Outlet />
41-
</MainBody>
33+
<DevPresenceProvider enabled={environment.type === "DEVELOPMENT"}>
34+
<SideMenu
35+
user={{ ...user, isImpersonating }}
36+
project={project}
37+
environment={environment}
38+
organization={organization}
39+
organizations={organizations}
40+
/>
41+
<MainBody>
42+
<Outlet />
43+
</MainBody>
44+
</DevPresenceProvider>
4245
</div>
4346
</>
4447
);

apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { Outlet, type ShouldRevalidateFunction, type UIMatch } from "@remix-run/react";
22
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
3-
import { typedjson, useTypedLoaderData } from "remix-typedjson";
3+
import { typedjson } from "remix-typedjson";
44
import { z } from "zod";
55
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
6-
import { MainBody } from "~/components/layout/AppLayout";
7-
import { SideMenu } from "~/components/navigation/SideMenu";
86
import { useOptionalOrganization } from "~/hooks/useOrganizations";
97
import { useTypedMatchesData } from "~/hooks/useTypedMatchData";
10-
import { useUser } from "~/hooks/useUser";
118
import { OrganizationsPresenter } from "~/presenters/OrganizationsPresenter.server";
129
import { getImpersonationId } from "~/services/impersonation.server";
1310
import { getCachedUsage, getCurrentPlan } from "~/services/platform.v3.server";

0 commit comments

Comments
 (0)