chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
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 = {
|
||||
isConnected: boolean | undefined;
|
||||
};
|
||||
|
||||
// Create Context with default values
|
||||
const DevPresenceContext = createContext<DevPresenceContextType>({
|
||||
isConnected: undefined,
|
||||
});
|
||||
|
||||
// Provider component with enabled prop
|
||||
interface DevPresenceProviderProps {
|
||||
children: ReactNode;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function DevPresenceProvider({ children, enabled = true }: DevPresenceProviderProps) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
|
||||
// Only subscribe to event source if enabled is true
|
||||
const streamedEvents = useEventSource(
|
||||
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/presence`,
|
||||
{
|
||||
event: "presence",
|
||||
disabled: !enabled,
|
||||
}
|
||||
);
|
||||
|
||||
const [isConnected, setIsConnected] = useState<boolean | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
// If disabled or no events
|
||||
if (!enabled || streamedEvents === null) {
|
||||
setIsConnected(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(streamedEvents) as any;
|
||||
if ("isConnected" in data && data.isConnected) {
|
||||
try {
|
||||
setIsConnected(true);
|
||||
} catch (error) {
|
||||
console.log("DevPresence: Failed to parse lastSeen timestamp", { error });
|
||||
setIsConnected(false);
|
||||
}
|
||||
} else {
|
||||
setIsConnected(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("DevPresence: Failed to parse presence message", { error });
|
||||
setIsConnected(false);
|
||||
}
|
||||
}, [streamedEvents, enabled]);
|
||||
|
||||
// Calculate isConnected and memoize the context value
|
||||
const contextValue = useMemo(() => {
|
||||
return { isConnected };
|
||||
}, [isConnected, enabled]);
|
||||
|
||||
return <DevPresenceContext.Provider value={contextValue}>{children}</DevPresenceContext.Provider>;
|
||||
}
|
||||
|
||||
// Custom hook to use the context
|
||||
export function useDevPresence() {
|
||||
const context = useContext(DevPresenceContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useDevPresence must be used within a DevPresenceProvider");
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user