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 3 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
112 changes: 112 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,100 @@ export function useDevPresence() {
}
return context;
}

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 DevConnection({
children,
}: {
children: (props: { isConnected: boolean | undefined }) => React.ReactNode;
}) {
const { isConnected } = useDevPresence();

return (
<Dialog>
{children({ isConnected })}
<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>
);
}

export function DevPresenceBanner() {
const environment = useEnvironment();
const { isConnected } = useDevPresence();

return (
<AnimatePresence>
{environment.type === "DEVELOPMENT" && !isConnected && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="flex"
>
<DevConnection>
{({ isConnected }) => (
<DialogTrigger asChild>
<Button
variant="minimal/small"
className="py-1 pl-1 pr-2 text-error"
LeadingIcon={<ConnectionIcon isConnected={isConnected} />}
>
Your local dev server is not connected to Trigger.dev
</Button>
</DialogTrigger>
)}
</DevConnection>
</motion.div>
)}
</AnimatePresence>
);
}
127 changes: 33 additions & 94 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,12 @@ 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, DevConnection } from "../DevPresence";
import { ImpersonationBanner } from "../ImpersonationBanner";
import { Button, ButtonContent, LinkButton } from "../primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
import { DialogTrigger } from "../primitives/Dialog";
import { Paragraph } from "../primitives/Paragraph";
import {
Popover,
Expand All @@ -78,15 +71,12 @@ 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<
Expand Down Expand Up @@ -163,7 +153,34 @@ export function SideMenu({
project={project}
environment={environment}
/>
{environment.type === "DEVELOPMENT" && <DevConnection />}
{environment.type === "DEVELOPMENT" && (
<DevConnection>
{({ isConnected }) => (
<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>
)}
</DevConnection>
)}
</div>
</div>

Expand Down Expand Up @@ -522,81 +539,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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import {
tryCatch,
} from "@trigger.dev/core/v3";
import { type RuntimeEnvironmentType } from "@trigger.dev/database";
import { motion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { DisconnectedIcon } from "~/assets/icons/ConnectionIcons";
import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParentIcon";
import tileBgPath from "~/assets/images/error-banner-tile@2x.png";
import { useDevPresence } from "~/components/DevPresence";
import { DevPresenceBanner, useDevPresence } from "~/components/DevPresence";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { PageBody } from "~/components/layout/AppLayout";
import { Badge } from "~/components/primitives/Badge";
Expand Down Expand Up @@ -195,6 +195,19 @@ export default function Page() {
}}
title={`Run #${run.number}`}
/>
<AnimatePresence>
{environment.type === "DEVELOPMENT" && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="flex"
>
<DevPresenceBanner />
</motion.div>
)}
</AnimatePresence>
<PageAccessories>
<AdminDebugTooltip>
<Property.Table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ListChecks, ListX } from "lucide-react";
import { Suspense, useState } from "react";
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { DevPresenceBanner, useDevPresence } from "~/components/DevPresence";
import { StepContentContainer } from "~/components/StepContentContainer";
import { MainCenteredContainer, PageBody } from "~/components/layout/AppLayout";
import { Button, LinkButton } from "~/components/primitives/Buttons";
Expand Down Expand Up @@ -161,11 +162,26 @@ export default function Page() {
const { data, rootOnlyDefault } = useTypedLoaderData<typeof loader>();
const navigation = useNavigation();
const isLoading = navigation.state !== "idle";
const { isConnected } = useDevPresence();
const environment = useEnvironment();

return (
<>
<NavBar>
<PageTitle title="Runs" />
<AnimatePresence>
{environment.type === "DEVELOPMENT" && !isConnected && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="flex"
>
<DevPresenceBanner />
</motion.div>
)}
</AnimatePresence>
<PageAccessories>
<LinkButton
variant={"docs/small"}
Expand Down