import { CheckIcon, PlusIcon } from "@heroicons/react/20/solid"; import { BookOpenIcon } from "@heroicons/react/24/solid"; import { useSearchParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { useCallback } from "react"; import { SearchInput } from "~/components/primitives/SearchInput"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; import { V4Title } from "~/components/V4Badge"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { CopyableText } from "~/components/primitives/CopyableText"; import { DateTime } from "~/components/primitives/DateTime"; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog"; import { Header3 } from "~/components/primitives/Headers"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { PopoverMenuItem } from "~/components/primitives/Popover"; import * as Property from "~/components/primitives/PropertyTable"; import { Switch } from "~/components/primitives/Switch"; import { Table, TableBlankRow, TableBody, TableCell, TableCellMenu, TableHeader, TableHeaderCell, TableRow, } from "~/components/primitives/Table"; import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { BranchesPresenter } from "~/presenters/v3/BranchesPresenter.server"; import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { branchesDevPath, docsPath, ProjectParamSchema } from "~/utils/pathBuilder"; import { ArchiveButton } from "../resources.branches.archive"; import { NewBranchPanel } from "~/routes/resources.branches.create"; import { BranchesOptions } from "~/utils/branches"; import { IconArrowBearRight2 } from "@tabler/icons-react"; import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { projectParam } = ProjectParamSchema.parse(params); const searchParams = new URL(request.url).searchParams; const parsedSearchParams = BranchesOptions.safeParse(Object.fromEntries(searchParams)); const options = parsedSearchParams.success ? parsedSearchParams.data : {}; try { const presenter = new BranchesPresenter(); const result = await presenter.call({ userId, projectSlug: projectParam, env: "development", ...options, }); return typedjson(result); } catch (error) { logger.error("Error loading dev branches page", { error }); throw new Response(undefined, { status: 400, statusText: "Something went wrong, if this problem persists please contact support.", }); } }; export default function Page() { const { branches, limits, currentPage, totalPages } = useTypedLoaderData(); useAutoRevalidate({ interval: 5000 }); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const atBranchLimit = limits.used >= limits.limit; const usageRatio = limits.limit > 0 ? Math.min(limits.used / limits.limit, 1) : 0; return ( Dev branches} /> {branches.map((branch) => ( {branch.branchName} {branch.id} ))} Dev branches docs {limits.isAtLimit ? ( ) : ( New branch… } env="development" /> )}
Branch Created Last active Archived Actions {branches.length === 0 ? ( There are no matches for your filters ) : ( branches.map((branch) => { const path = branchesDevPath(organization, project, branch); const cellClass = branch.archivedAt ? "opacity-50" : ""; const isSelected = branch.id === environment.id; return (
{isSelected && Current}
{branch.isConnected ? ( <>Online now ) : branch.lastActivity ? ( ) : null} {branch.archivedAt ? ( ) : ( "–" )} Switch to branch ) } popoverContent={ !isSelected || !branch.archivedAt ? ( <> {isSelected ? null : ( )} {!branch.archivedAt ? ( ) : null} ) : null } />
); }) )}
} content={`${Math.round(usageRatio * 100)}%`} />
{atBranchLimit ? ( You've used all {limits.limit} of your branches. Archive one to free up space. ) : (
You've used {limits.used}/{limits.limit} of your branches
)}
); } export function BranchFilters() { const [searchParams, setSearchParams] = useSearchParams(); const { showArchived } = BranchesOptions.parse(Object.fromEntries(searchParams.entries())); const handleArchivedChange = useCallback((checked: boolean) => { setSearchParams((s) => { if (checked) { s.set("showArchived", "true"); } else { s.delete("showArchived"); } s.delete("page"); return s; }); }, []); return (
); } function BranchLimitReachedDialog({ limits, }: { limits: { used: number; limit: number; }; }) { return ( You've exceeded your limit
You've used {limits.used}/{limits.limit} of your branches. You can archive a branch to free up space.
); }