import { ChevronRightIcon, Cog8ToothIcon } from "@heroicons/react/20/solid"; import { DEFAULT_DEV_BRANCH } from "@trigger.dev/core/v3/utils/gitBranch"; import { isBranchableEnvironment } from "~/utils/branchableEnvironment"; import { DropdownIcon } from "~/assets/icons/DropdownIcon"; import { useNavigation, useRevalidator } from "@remix-run/react"; import { useEffect, useRef, useState } from "react"; import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useEnvironmentSwitcher } from "~/hooks/useEnvironmentSwitcher"; import { useFeatures } from "~/hooks/useFeatures"; import { useOrganization, type MatchedOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { cn } from "~/utils/cn"; import { branchesPath, branchesDevPath, docsPath, v3BillingPath } from "~/utils/pathBuilder"; import { EnvironmentCombo, EnvironmentIcon, EnvironmentLabel, environmentFullTitle, environmentTextClassName, } from "../environments/EnvironmentLabel"; import { ButtonContent } from "../primitives/Buttons"; import { Header2 } from "../primitives/Headers"; import { Paragraph } from "../primitives/Paragraph"; import { Popover, PopoverContent, PopoverMenuItem, PopoverSectionHeader, PopoverTrigger, } from "../primitives/Popover"; import { TextLink } from "../primitives/TextLink"; import { SimpleTooltip } from "../primitives/Tooltip"; import { V4Badge } from "../V4Badge"; import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu"; import { Badge } from "../primitives/Badge"; export function EnvironmentSelector({ organization, project, environment, className, isCollapsed = false, }: { organization: MatchedOrganization; project: SideMenuProject; environment: SideMenuEnvironment; className?: string; isCollapsed?: boolean; }) { const { isManagedCloud } = useFeatures(); const [isMenuOpen, setIsMenuOpen] = useState(false); const navigation = useNavigation(); const { urlForEnvironment } = useEnvironmentSwitcher(); const revalidator = useRevalidator(); useEffect(() => { setIsMenuOpen(false); }, [navigation.location?.pathname]); // Fetch immediately on open so the list is fresh right away useEffect(() => { if (isMenuOpen && revalidator.state !== "loading") { revalidator.revalidate(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMenuOpen]); const hasStaging = project.environments.some((env) => env.type === "STAGING"); return ( setIsMenuOpen(open)} open={isMenuOpen}> } content={environmentFullTitle(environment)} side="right" sideOffset={8} hidden={!isCollapsed} buttonClassName="h-8!" asChild disableHoverableContent />
{project.environments .filter((env) => env.parentEnvironmentId === null) .map((env) => { const renderAsBranchable = isBranchableEnvironment(env); if (renderAsBranchable) { const branchEnvironments = project.environments.filter( (e) => e.parentEnvironmentId === env.id ); const allBranchEnvironments = env.type === "DEVELOPMENT" ? [env, ...branchEnvironments] : branchEnvironments; return ( ); } return ( } isSelected={env.id === environment.id} /> ); })}
{!hasStaging && isManagedCloud && ( <>
Upgrade
} isSelected={false} /> Upgrade } isSelected={false} /> )}
); } function Branches({ parentEnvironment, branchEnvironments, currentEnvironment, }: { parentEnvironment: SideMenuEnvironment; branchEnvironments: SideMenuEnvironment[]; currentEnvironment: SideMenuEnvironment; }) { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const { urlForEnvironment } = useEnvironmentSwitcher(); const navigation = useNavigation(); const [isMenuOpen, setMenuOpen] = useState(false); const timeoutRef = useRef(null); // Clear timeout on unmount useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); useEffect(() => { setMenuOpen(false); }, [navigation.location?.pathname]); const handleMouseEnter = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } setMenuOpen(true); }; const handleMouseLeave = () => { // Small delay before closing to allow moving to the content timeoutRef.current = setTimeout(() => { setMenuOpen(false); }, 150); }; const activeBranches = branchEnvironments.filter((env) => env.archivedAt === null); const state = branchEnvironments.length === 0 ? "no-branches" : activeBranches.length === 0 ? "no-active-branches" : "has-branches"; // Only surface the active environment's archived-branch item in the submenu it // actually belongs to. Both Development and Preview render this component, so // without the parent check an archived dev branch would leak into the Preview // submenu (and vice-versa). const currentBranchIsArchived = environment.archivedAt !== null && environment.parentEnvironmentId === parentEnvironment.id; const envTextClassName = environmentTextClassName(parentEnvironment); return ( setMenuOpen(open)} open={isMenuOpen}>
{currentBranchIsArchived && ( {environment.branchName} Archived } icon={ } isSelected={environment.id === currentEnvironment.id} /> )} {state === "has-branches" ? ( <> {branchEnvironments .filter((env) => env.archivedAt === null) .map((env) => ( {env.branchName ?? DEFAULT_DEV_BRANCH} } icon={ } isSelected={env.id === currentEnvironment.id} /> ))} ) : state === "no-branches" ? (
Create your first branch
Branches are a way to test new features in isolation before merging them into the main environment. Branches are only available when using or above. Read our{" "} v4 upgrade guide to learn more.
) : (
All branches are archived.
)}
{parentEnvironment.type === "DEVELOPMENT" ? ( } leadingIconClassName="text-text-dimmed" /> ) : ( } leadingIconClassName="text-text-dimmed" /> )}
); }