1import type { EnvironmentId, MergebotDeployments } from "@t3tools/contracts";
2import { useNavigate, useParams } from "@tanstack/react-router";
3import { RocketIcon } from "lucide-react";
4
5import { cn } from "../../lib/utils";
6import { useRightPanelStore } from "../../rightPanelStore";
7import { usePrimaryEnvironmentId } from "../../state/environments";
8import { resolveThreadRouteRef } from "../../threadRoutes";
9import { ConfiguredDeployments } from "../deployments/ConfiguredDeployments";
10import { deploymentActivity, stagingOnlyCount } from "../deployments/deployments.logic";
11import { Badge } from "../ui/badge";
12import { SidebarMenuButton, SidebarMenuItem, useSidebar } from "../ui/sidebar";
13import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
14
15const ACTIVITY_DOT = {
16 healthy: "bg-success",
17 deploying: "bg-warning",
18 failed: "bg-destructive",
19} as const;
20
21/** Opens Deployments in the current thread's right panel, or as a page without a thread. */
22export function SidebarDeploymentsButton() {
23 const threadRef = useParams({ strict: false, select: resolveThreadRouteRef });
24 const primaryEnvironmentId = usePrimaryEnvironmentId();
25 const environmentId = threadRef?.environmentId ?? primaryEnvironmentId;
26
27 if (!environmentId) return null;
28
29 return (
30 <ConfiguredDeployments environmentId={environmentId}>
31 {(found) =>
32 found.length > 0 ? <DeploymentsButton environmentId={environmentId} found={found} /> : null
33 }
34 </ConfiguredDeployments>
35 );
36}
37
38function DeploymentsButton(props: {
39 environmentId: EnvironmentId;
40 found: readonly MergebotDeployments[];
41}) {
42 const navigate = useNavigate();
43 const threadRef = useParams({ strict: false, select: resolveThreadRouteRef });
44 const { isMobile, setOpenMobile } = useSidebar();
45
46 const pending = props.found.reduce((total, entry) => total + stagingOnlyCount(entry), 0);
47 const activity = deploymentActivity(props.found);
48 const label = pending > 0 ? `Deployments, ${pending} on staging only` : "Deployments";
49
50 const open = () => {
51 if (threadRef) useRightPanelStore.getState().open(threadRef, "deployments");
52 else void navigate({ to: "/deployments" });
53 if (isMobile) setOpenMobile(false);
54 };
55
56 return (
57 <SidebarMenuItem className="shrink-0">
58 <Tooltip>
59 <TooltipTrigger
60 render={
61 <SidebarMenuButton aria-label={label} onClick={open} size="icon">
62 <RocketIcon aria-hidden="true" />
63 <span
64 aria-hidden
65 className={cn(
66 "absolute right-1 bottom-1 size-1.5 rounded-full",
67 ACTIVITY_DOT[activity],
68 )}
69 />
70 {pending > 0 ? (
71 <Badge aria-hidden className="absolute -top-0.5 -right-0.5" size="sm">
72 {pending}
73 </Badge>
74 ) : null}
75 </SidebarMenuButton>
76 }
77 />
78 <TooltipPopup side="top">{label}</TooltipPopup>
79 </Tooltip>
80 </SidebarMenuItem>
81 );
82}