Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,50 +1,58 @@
"use client";

import { type Deployment, collection } from "@/lib/collections";
import { type Deployment, collection, collectionManager } from "@/lib/collections";
import { shortenId } from "@/lib/shorten-id";
import { trpc } from "@/lib/trpc/client";
import { CircleInfo, CodeBranch, CodeCommit } from "@unkey/icons";
import { eq, inArray, useLiveQuery } from "@tanstack/react-db";
import { CircleInfo, CodeBranch, CodeCommit, Link4 } from "@unkey/icons";
import { Badge, Button, DialogContainer, toast } from "@unkey/ui";
import { StatusIndicator } from "../../details/active-deployment-card/status-indicator";

type DeploymentSectionProps = {
title: string;
deployment: Deployment;
isActive: boolean;
isLive: boolean;
showSignal?: boolean;
};

const DeploymentSection = ({ title, deployment, isActive, showSignal }: DeploymentSectionProps) => (
const DeploymentSection = ({ title, deployment, isLive, showSignal }: DeploymentSectionProps) => (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="text-[13px] text-grayA-11">{title}</h3>
<CircleInfo size="sm-regular" className="text-gray-9" />
</div>
<DeploymentCard deployment={deployment} isActive={isActive} showSignal={showSignal} />
<DeploymentCard deployment={deployment} isLive={isLive} showSignal={showSignal} />
</div>
);

type RollbackDialogProps = {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
deployment: Deployment;
currentDeployment: Deployment;
hostname?: string;
targetDeployment: Deployment;
liveDeployment: Deployment;
};

export const RollbackDialog = ({
isOpen,
onOpenChange,
deployment,
currentDeployment,
hostname,
targetDeployment,
liveDeployment,
}: RollbackDialogProps) => {
const utils = trpc.useUtils();
const domainCollection = collectionManager.getProjectCollections(
liveDeployment.projectId,
).domains;
const domains = useLiveQuery((q) =>
q
.from({ domain: domainCollection })
.where(({ domain }) => inArray(domain.sticky, ["environment", "live"]))
.where(({ domain }) => eq(domain.deploymentId, liveDeployment.id)),
);
const rollback = trpc.deploy.deployment.rollback.useMutation({
onSuccess: () => {
utils.invalidate();
toast.success("Rollback completed", {
description: `Successfully rolled back to deployment ${deployment.id}`,
description: `Successfully rolled back to deployment ${targetDeployment.id}`,
});
// hack to revalidate
try {
Expand All @@ -64,21 +72,13 @@ export const RollbackDialog = ({
});

const handleRollback = async () => {
if (!hostname) {
toast.error("Missing hostname", {
description: "Cannot perform rollback without hostname information",
});
return;
}

try {
await rollback.mutateAsync({
hostname,
targetDeploymentId: deployment.id,
await rollback
.mutateAsync({
targetDeploymentId: targetDeployment.id,
})
.catch((error) => {
console.error("Rollback error:", error);
});
} catch (error) {
console.error("Rollback error:", error);
}
};

return (
Expand All @@ -88,41 +88,52 @@ export const RollbackDialog = ({
title="Rollback to version"
subTitle="Switch the active deployment to a target stable version"
footer={
<div className="flex flex-col items-center w-full gap-2">
<Button
variant="primary"
size="xlg"
onClick={handleRollback}
disabled={rollback.isLoading}
loading={rollback.isLoading}
className="w-full rounded-lg"
>
Rollback to target version
</Button>
<div className="text-xs text-gray-9">Rollbacks usually complete within seconds</div>
</div>
<Button
variant="primary"
size="xlg"
onClick={handleRollback}
disabled={rollback.isLoading}
loading={rollback.isLoading}
className="w-full rounded-lg"
>
Rollback to target version
</Button>
}
>
<div className="space-y-9">
<DeploymentSection
title="Current active deployment"
deployment={currentDeployment}
isActive={true}
title="Live Deployment"
deployment={liveDeployment}
isLive={true}
showSignal={true}
/>
<DeploymentSection title="Target version" deployment={deployment} isActive={false} />
<div>
{domains.data.map((domain) => (
<div
key={domain.id}
className="border border-gray-4 border-t-0 first:border-t first:rounded-t-[14px] last:rounded-b-[14px] last:border-b w-full px-4 py-3 flex justify-between items-center"
>
<div className="flex items-center">
<Link4 className="text-gray-9" size="sm-medium" />
<div className="text-gray-12 font-medium text-xs ml-3 mr-2">{domain.domain}</div>
<div className="ml-3" />
</div>
</div>
))}
</div>
<DeploymentSection title="Target Deployment" deployment={targetDeployment} isLive={false} />
</div>
</DialogContainer>
);
};

type DeploymentCardProps = {
deployment: Deployment;
isActive: boolean;
isLive: boolean;
showSignal?: boolean;
};

const DeploymentCard = ({ deployment, isActive, showSignal }: DeploymentCardProps) => (
const DeploymentCard = ({ deployment, isLive, showSignal }: DeploymentCardProps) => (
<div className="bg-white dark:bg-black border border-grayA-5 rounded-lg p-4 relative">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
Expand All @@ -133,14 +144,14 @@ const DeploymentCard = ({ deployment, isActive, showSignal }: DeploymentCardProp
{`${deployment.id.slice(0, 3)}...${deployment.id.slice(-4)}`}
</span>
<Badge
variant={isActive ? "success" : "primary"}
className={`px-1.5 ${isActive ? "text-successA-11" : "text-grayA-11"}`}
variant={isLive ? "success" : "primary"}
className={`px-1.5 capitalize ${isLive ? "text-successA-11" : "text-grayA-11"}`}
>
{isActive ? "Active" : "Preview"}
{isLive ? "Live" : deployment.status}
</Badge>
</div>
<div className="text-xs text-grayA-9">
{deployment.gitCommitMessage || `${isActive ? "Current active" : "Target"} deployment`}
{deployment.gitCommitMessage || `${isLive ? "Current active" : "Target"} deployment`}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,69 +1,58 @@
"use client";
import { type MenuItem, TableActionPopover } from "@/components/logs/table-action.popover";
import type { Deployment, Environment } from "@/lib/collections";
import { ArrowDottedRotateAnticlockwise, PenWriting3 } from "@unkey/icons";
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter } from "next/navigation";
import { ArrowDottedRotateAnticlockwise } from "@unkey/icons";
import { useState } from "react";
import { RollbackDialog } from "../../../rollback-dialog";

type DeploymentListTableActionsProps = {
deployment: Deployment;
currentActiveDeployment?: Deployment;
liveDeployment?: Deployment;
selectedDeployment: Deployment;
environment?: Environment;
};

export const DeploymentListTableActions = ({
deployment,
currentActiveDeployment,
liveDeployment,
selectedDeployment,
environment,
}: DeploymentListTableActionsProps) => {
const router = useRouter();
const [isRollbackModalOpen, setIsRollbackModalOpen] = useState(false);
const menuItems = getDeploymentListTableActionItems(
deployment,
selectedDeployment,
liveDeployment,
environment,
router,
setIsRollbackModalOpen,
);

return (
<>
<TableActionPopover items={menuItems} />
{currentActiveDeployment && (
{liveDeployment && selectedDeployment && (
<RollbackDialog
isOpen={isRollbackModalOpen}
onOpenChange={setIsRollbackModalOpen}
deployment={deployment}
currentDeployment={currentActiveDeployment}
hostname="example.com" // TODO: Get actual hostname from deployment/project
liveDeployment={liveDeployment}
targetDeployment={selectedDeployment}
/>
)}
</>
);
};

const getDeploymentListTableActionItems = (
deployment: Deployment,
selectedDeployment: Deployment,
liveDeployment: Deployment | undefined,
environment: Environment | undefined,
router: AppRouterInstance,
setIsRollbackModalOpen: (open: boolean) => void,
): MenuItem[] => {
// Rollback is only enabled for production deployments that are ready and not currently active
const canRollback =
liveDeployment &&
environment?.slug === "production" &&
deployment.status === "ready" &&
deployment.id !== "current_active_deployment_id"; // TODO: Better way to determine if this is the current active deployment
selectedDeployment.status === "ready" &&
selectedDeployment.id !== liveDeployment.id;

return [
{
id: "edit-root-key",
label: "Edit root key...",
icon: <PenWriting3 size="md-regular" />,
onClick: () => {
router.push(`/settings/root-keys/${deployment.id}`);
},
},
{
id: "rollback",
label: "Rollback",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ const statusBadgeVariants = cva(
variant: {
enabled: "text-successA-11 bg-successA-3",
disabled: "text-warningA-11 bg-warningA-3",
current: "text-feature-11 bg-feature-4",
live: "text-feature-11 bg-feature-4",
},
},
defaultVariants: {
variant: "current",
variant: "live",
},
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const DeploymentsList = () => {
} | null>(null);
const isCompactView = useIsMobile({ breakpoint: COMPACT_BREAKPOINT });

const { activeDeployment, deployments } = useDeployments();
const { liveDeployment, deployments } = useDeployments();

const columns: Column<{
deployment: Deployment;
Expand All @@ -58,8 +58,8 @@ export const DeploymentsList = () => {
header: "Deployment ID",
width: "20%",
headerClassName: "pl-[18px]",
render: ({ deployment }) => {
const isProduction = deployment.id === activeDeployment.data.at(0)?.id;
render: ({ deployment, environment }) => {
const isLive = liveDeployment?.id === deployment.id;
const isSelected = deployment.id === selectedDeployment?.deployment.id;
const iconContainer = (
<div
Expand All @@ -86,10 +86,15 @@ export const DeploymentsList = () => {
>
{shortenId(deployment.id)}
</div>
{isProduction ? <EnvStatusBadge variant="current" text="Current" /> : null}
{isLive ? <EnvStatusBadge variant="live" text="Live" /> : null}
</div>
<div className={cn("font-normal font-mono truncate text-xs mt-1", "text-gray-9")}>
{isProduction ? "Production" : "Preview"}
<div
className={cn(
"font-normal font-mono truncate text-xs mt-1 capitalize",
"text-gray-9",
)}
>
{environment?.slug}
</div>
</div>
</div>
Expand Down Expand Up @@ -281,15 +286,15 @@ export const DeploymentsList = () => {
}) => {
return (
<DeploymentListTableActions
deployment={deployment}
currentActiveDeployment={activeDeployment.data.at(0)}
selectedDeployment={deployment}
liveDeployment={liveDeployment}
environment={environment}
/>
);
},
},
];
}, [selectedDeployment?.deployment.id, isCompactView, activeDeployment]);
}, [selectedDeployment?.deployment.id, isCompactView, liveDeployment]);

return (
<VirtualTable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ export const useDeployments = () => {
.where(({ project }) => eq(project.id, projectId))
.orderBy(({ project }) => project.id, "asc")
.limit(1);
});
const liveDeploymentId = project.data.at(0)?.liveDeploymentId;
const activeDeployment = useLiveQuery(
}).data.at(0);
const liveDeploymentId = project?.liveDeploymentId;
const liveDeployment = useLiveQuery(
(q) =>
q
.from({ deployment: collections.deployments })
.where(({ deployment }) => eq(deployment.id, liveDeploymentId))
.orderBy(({ deployment }) => deployment.createdAt, "desc")
.limit(1),
[liveDeploymentId],
);
).data.at(0);
const deployments = useLiveQuery(
(q) => {
// Query filtered environments
Expand Down Expand Up @@ -114,7 +114,6 @@ export const useDeployments = () => {

return {
deployments,
activeDeployment,
activeDeploymentId: liveDeploymentId,
liveDeployment,
};
};
1 change: 1 addition & 0 deletions apps/dashboard/lib/collections/deploy/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function createDeploymentsCollection(projectId: string) {
queryClient,
queryKey: [projectId, "deployments"],
retry: 3,
refetchInterval: 5000,
queryFn: () => trpcClient.deploy.deployment.list.query({ projectId }),
getKey: (item) => item.id,
id: `${projectId}-deployments`,
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/lib/collections/deploy/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const schema = z.object({
domain: z.string(),
type: z.enum(["custom", "wildcard"]),
projectId: z.string().nullable(),
deploymentId: z.string().nullable(),
sticky: z.enum(["branch", "environment", "live"]).nullable(),
});

export type Domain = z.infer<typeof schema>;
Expand Down
Loading