Skip to content

Adds a "dev connected" banner to the top of the Run and Runs list pages #1855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions apps/webapp/app/components/DevPresence.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { AnimatePresence, motion } from "framer-motion";
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
import {
CheckingConnectionIcon,
ConnectedIcon,
DisconnectedIcon,
} from "~/assets/icons/ConnectionIcons";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useEventSource } from "~/hooks/useEventSource";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { docsPath } from "~/utils/pathBuilder";
import connectedImage from "../assets/images/cli-connected.png";
import disconnectedImage from "../assets/images/cli-disconnected.png";
import { InlineCode } from "./code/InlineCode";
import { Button } from "./primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog";
import { Paragraph } from "./primitives/Paragraph";
import { TextLink } from "./primitives/TextLink";
import { PackageManagerProvider, TriggerDevStepV3 } from "./SetupCommands";

// Define Context types
type DevPresenceContextType = {
Expand Down Expand Up @@ -77,3 +92,131 @@ export function useDevPresence() {
}
return context;
}

/**
* We need this for the legacy v1 engine, where we show the banner after a delay if there are no events.
*/
export function useCrossEngineIsConnected({
isCompleted,
logCount,
}: {
isCompleted: boolean;
logCount: number;
}) {
const project = useProject();
const environment = useEnvironment();
const { isConnected } = useDevPresence();
const [crossEngineIsConnected, setCrossEngineIsConnected] = useState<boolean | undefined>(
undefined
);

useEffect(() => {
if (project.engine === "V2") {
setCrossEngineIsConnected(isConnected);
return;
}

if (project.engine === "V1") {
if (isCompleted) {
setCrossEngineIsConnected(true);
return;
}

if (logCount <= 1) {
const timer = setTimeout(() => {
setCrossEngineIsConnected(false);
}, 5000);
return () => clearTimeout(timer);
} else {
setCrossEngineIsConnected(true);
}
}
}, [environment.type, project.engine, logCount, isConnected, isCompleted]);

return crossEngineIsConnected;
}

export function ConnectionIcon({ isConnected }: { isConnected: boolean | undefined }) {
if (isConnected === undefined) {
return <CheckingConnectionIcon className="size-5" />;
}
return isConnected ? (
<ConnectedIcon className="size-5" />
) : (
<DisconnectedIcon className="size-5" />
);
}

export function DevPresencePanel({ isConnected }: { isConnected: boolean | undefined }) {
return (
<DialogContent>
<DialogHeader>
{isConnected === undefined
? "Checking connection..."
: isConnected
? "Your dev server is connected"
: "Your dev server is not connected"}
</DialogHeader>
<div className="mt-2 flex flex-col gap-3 px-2">
<div className="flex flex-col items-center justify-center gap-6 px-6 py-10">
<img
src={isConnected === true ? connectedImage : disconnectedImage}
alt={isConnected === true ? "Connected" : "Disconnected"}
width={282}
height={45}
/>
<Paragraph variant="small" className={isConnected ? "text-success" : "text-error"}>
{isConnected === undefined
? "Checking connection..."
: isConnected
? "Your local dev server is connected to Trigger.dev"
: "Your local dev server is not connected to Trigger.dev"}
</Paragraph>
</div>
{isConnected ? null : (
<div className="space-y-3">
<PackageManagerProvider>
<TriggerDevStepV3 title="Run this command to connect" />
</PackageManagerProvider>
<Paragraph variant="small">
Run this CLI <InlineCode variant="extra-small">dev</InlineCode> command to connect to
the Trigger.dev servers to start developing locally. Keep it running while you develop
to stay connected. Learn more in the{" "}
<TextLink to={docsPath("cli-dev")}>CLI docs</TextLink>.
</Paragraph>
</div>
)}
</div>
</DialogContent>
);
}

export function DevDisconnectedBanner({ isConnected }: { isConnected: boolean | undefined }) {
return (
<Dialog>
<AnimatePresence>
{isConnected === false && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="flex"
>
<DialogTrigger asChild>
<Button
variant="minimal/small"
className="border border-error/20 bg-error/10 py-1 pl-1 pr-2 group-hover/button:border-error/30 group-hover/button:bg-error/20"
iconSpacing="gap-1"
LeadingIcon={<ConnectionIcon isConnected={false} />}
>
Your local dev server is not connected to Trigger.dev
</Button>
</DialogTrigger>
</motion.div>
)}
</AnimatePresence>
<DevPresencePanel isConnected={isConnected} />
</Dialog>
);
}
128 changes: 33 additions & 95 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,9 @@ import {
import { useNavigation } from "@remix-run/react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import simplur from "simplur";
import {
CheckingConnectionIcon,
ConnectedIcon,
DisconnectedIcon,
} from "~/assets/icons/ConnectionIcons";
import { RunsIconExtraSmall, RunsIconSmall } from "~/assets/icons/RunsIcon";
import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon";
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
import { Avatar } from "~/components/primitives/Avatar";
import { type MatchedEnvironment } from "~/hooks/useEnvironment";
import { type MatchedOrganization } from "~/hooks/useOrganizations";
Expand All @@ -37,7 +33,6 @@ import { type FeedbackType } from "~/routes/resources.feedback";
import { cn } from "~/utils/cn";
import {
accountPath,
docsPath,
logoutPath,
newOrganizationPath,
newProjectPath,
Expand All @@ -60,14 +55,11 @@ import {
v3UsagePath,
v3WaitpointTokensPath,
} from "~/utils/pathBuilder";
import connectedImage from "../../assets/images/cli-connected.png";
import disconnectedImage from "../../assets/images/cli-disconnected.png";
import { FreePlanUsage } from "../billing/FreePlanUsage";
import { InlineCode } from "../code/InlineCode";
import { useDevPresence } from "../DevPresence";
import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence";
import { ImpersonationBanner } from "../ImpersonationBanner";
import { Button, ButtonContent, LinkButton } from "../primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
import { Dialog, DialogTrigger } from "../primitives/Dialog";
import { Paragraph } from "../primitives/Paragraph";
import {
Popover,
Expand All @@ -78,20 +70,17 @@ import {
} from "../primitives/Popover";
import { TextLink } from "../primitives/TextLink";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
import { PackageManagerProvider, TriggerDevStepV3 } from "../SetupCommands";
import { UserProfilePhoto } from "../UserProfilePhoto";
import { EnvironmentSelector } from "./EnvironmentSelector";
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
import { SideMenuHeader } from "./SideMenuHeader";
import { SideMenuItem } from "./SideMenuItem";
import { SideMenuSection } from "./SideMenuSection";
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
import { Spinner } from "../primitives/Spinner";

type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
export type SideMenuProject = Pick<
MatchedProject,
"id" | "name" | "slug" | "version" | "environments"
"id" | "name" | "slug" | "version" | "environments" | "engine"
>;
export type SideMenuEnvironment = MatchedEnvironment;

Expand All @@ -115,6 +104,7 @@ export function SideMenu({
const borderRef = useRef<HTMLDivElement>(null);
const [showHeaderDivider, setShowHeaderDivider] = useState(false);
const currentPlan = useCurrentPlan();
const { isConnected } = useDevPresence();
const isFreeUser = currentPlan?.v3Subscription?.isPaying === false;

useEffect(() => {
Expand Down Expand Up @@ -163,7 +153,33 @@ export function SideMenu({
project={project}
environment={environment}
/>
{environment.type === "DEVELOPMENT" && <DevConnection />}
{environment.type === "DEVELOPMENT" && project.engine === "V2" && (
<Dialog>
<TooltipProvider disableHoverableContent={true}>
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex">
<DialogTrigger asChild>
<Button
variant="minimal/small"
className="aspect-square h-7 p-1"
LeadingIcon={<ConnectionIcon isConnected={isConnected} />}
/>
</DialogTrigger>
</div>
</TooltipTrigger>
<TooltipContent side="right" className={"text-xs"}>
{isConnected === undefined
? "Checking connection..."
: isConnected
? "Your dev server is connected"
: "Your dev server is not connected"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DevPresencePanel isConnected={isConnected} />
</Dialog>
)}
</div>
</div>

Expand Down Expand Up @@ -522,81 +538,3 @@ function SelectorDivider() {
</svg>
);
}

export function DevConnection() {
const { isConnected } = useDevPresence();

return (
<Dialog>
<TooltipProvider disableHoverableContent={true}>
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex">
<DialogTrigger asChild>
<Button
variant="minimal/small"
className="aspect-square h-7 p-1"
LeadingIcon={
isConnected === undefined ? (
<CheckingConnectionIcon className="size-5" />
) : isConnected ? (
<ConnectedIcon className="size-5" />
) : (
<DisconnectedIcon className="size-5" />
)
}
/>
</DialogTrigger>
</div>
</TooltipTrigger>
<TooltipContent side="right" className={"text-xs"}>
{isConnected === undefined
? "Checking connection..."
: isConnected
? "Your dev server is connected"
: "Your dev server is not connected"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DialogContent>
<DialogHeader>
{isConnected === undefined
? "Checking connection..."
: isConnected
? "Your dev server is connected"
: "Your dev server is not connected"}
</DialogHeader>
<div className="mt-2 flex flex-col gap-3 px-2">
<div className="flex flex-col items-center justify-center gap-6 px-6 py-10">
<img
src={isConnected === true ? connectedImage : disconnectedImage}
alt={isConnected === true ? "Connected" : "Disconnected"}
width={282}
height={45}
/>
<Paragraph variant="small" className={isConnected ? "text-success" : "text-error"}>
{isConnected === undefined
? "Checking connection..."
: isConnected
? "Your local dev server is connected to Trigger.dev"
: "Your local dev server is not connected to Trigger.dev"}
</Paragraph>
</div>
{isConnected ? null : (
<div className="space-y-3">
<PackageManagerProvider>
<TriggerDevStepV3 title="Run this command to connect" />
</PackageManagerProvider>
<Paragraph variant="small">
Run this CLI <InlineCode variant="extra-small">dev</InlineCode> command to connect
to the Trigger.dev servers to start developing locally. Keep it running while you
develop to stay connected. Learn more in the{" "}
<TextLink to={docsPath("cli-dev")}>CLI docs</TextLink>.
</Paragraph>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}
Loading