WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit e075fa7

Browse files
feat: promote a deployment (#4015)
* fix: multiple rolled back states * fix: opacity * revert: color changes * [autofix.ci] apply automated fixes * revert: color * [autofix.ci] apply automated fixes * feat: promote a deployment * fix: rollbacks actually work now * fix: add missing query --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4d6fcc6 commit e075fa7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1183
-235
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"use client";
2+
3+
import { type Deployment, collection, collectionManager } from "@/lib/collections";
4+
import { shortenId } from "@/lib/shorten-id";
5+
import { trpc } from "@/lib/trpc/client";
6+
import { eq, inArray, useLiveQuery } from "@tanstack/react-db";
7+
import { CircleInfo, CodeBranch, CodeCommit, Link4 } from "@unkey/icons";
8+
import { Badge, Button, DialogContainer, toast } from "@unkey/ui";
9+
import { StatusIndicator } from "../../details/active-deployment-card/status-indicator";
10+
11+
type DeploymentSectionProps = {
12+
title: string;
13+
deployment: Deployment;
14+
isLive: boolean;
15+
showSignal?: boolean;
16+
};
17+
18+
const DeploymentSection = ({ title, deployment, isLive, showSignal }: DeploymentSectionProps) => (
19+
<div className="space-y-2">
20+
<div className="flex items-center gap-2">
21+
<h3 className="text-[13px] text-grayA-11">{title}</h3>
22+
<CircleInfo size="sm-regular" className="text-gray-9" />
23+
</div>
24+
<DeploymentCard deployment={deployment} isLive={isLive} showSignal={showSignal} />
25+
</div>
26+
);
27+
28+
type PromotionDialogProps = {
29+
isOpen: boolean;
30+
onOpenChange: (open: boolean) => void;
31+
targetDeployment: Deployment;
32+
liveDeployment: Deployment;
33+
};
34+
35+
export const PromotionDialog = ({
36+
isOpen,
37+
onOpenChange,
38+
targetDeployment,
39+
liveDeployment,
40+
}: PromotionDialogProps) => {
41+
const utils = trpc.useUtils();
42+
const domainCollection = collectionManager.getProjectCollections(
43+
liveDeployment.projectId,
44+
).domains;
45+
const domains = useLiveQuery((q) =>
46+
q
47+
.from({ domain: domainCollection })
48+
.where(({ domain }) => inArray(domain.sticky, ["environment", "live"]))
49+
.where(({ domain }) => eq(domain.deploymentId, liveDeployment.id)),
50+
);
51+
const promote = trpc.deploy.deployment.promote.useMutation({
52+
onSuccess: () => {
53+
utils.invalidate();
54+
toast.success("Promotion completed", {
55+
description: `Successfully promoted to deployment ${targetDeployment.id}`,
56+
});
57+
// hack to revalidate
58+
try {
59+
// @ts-expect-error Their docs say it's here
60+
collection.projects.utils.refetch();
61+
// @ts-expect-error Their docs say it's here
62+
collection.deployments.utils.refetch();
63+
// @ts-expect-error Their docs say it's here
64+
collection.domains.utils.refetch();
65+
} catch (error) {
66+
console.error("Refetch error:", error);
67+
}
68+
69+
onOpenChange(false);
70+
},
71+
onError: (error) => {
72+
toast.error("Promotion failed", {
73+
description: error.message,
74+
});
75+
},
76+
});
77+
78+
const handlePromotion = async () => {
79+
await promote
80+
.mutateAsync({
81+
targetDeploymentId: targetDeployment.id,
82+
})
83+
.catch((error) => {
84+
console.error("Promotion error:", error);
85+
});
86+
};
87+
88+
return (
89+
<DialogContainer
90+
isOpen={isOpen}
91+
onOpenChange={onOpenChange}
92+
title="Promotion to version"
93+
subTitle="Switch the active deployment to a target stable version"
94+
footer={
95+
<Button
96+
variant="primary"
97+
size="xlg"
98+
onClick={handlePromotion}
99+
disabled={promote.isLoading}
100+
loading={promote.isLoading}
101+
className="w-full rounded-lg"
102+
>
103+
Promote to
104+
{targetDeployment.gitCommitSha
105+
? shortenId(targetDeployment.gitCommitSha)
106+
: targetDeployment.id}
107+
</Button>
108+
}
109+
>
110+
<div className="space-y-9">
111+
<DeploymentSection
112+
title="Live Deployment"
113+
deployment={liveDeployment}
114+
isLive={true}
115+
showSignal={true}
116+
/>
117+
<div>
118+
{domains.data.map((domain) => (
119+
<div
120+
key={domain.id}
121+
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"
122+
>
123+
<div className="flex items-center">
124+
<Link4 className="text-gray-9" size="sm-medium" />
125+
<div className="text-gray-12 font-medium text-xs ml-3 mr-2">{domain.domain}</div>
126+
<div className="ml-3" />
127+
</div>
128+
</div>
129+
))}
130+
</div>
131+
<DeploymentSection title="Target Deployment" deployment={targetDeployment} isLive={false} />
132+
</div>
133+
</DialogContainer>
134+
);
135+
};
136+
137+
type DeploymentCardProps = {
138+
deployment: Deployment;
139+
isLive: boolean;
140+
showSignal?: boolean;
141+
};
142+
143+
const DeploymentCard = ({ deployment, isLive, showSignal }: DeploymentCardProps) => (
144+
<div className="bg-white dark:bg-black border border-grayA-5 rounded-lg p-4 relative">
145+
<div className="flex items-center justify-between">
146+
<div className="flex items-center gap-3">
147+
<StatusIndicator withSignal={showSignal} />
148+
<div>
149+
<div className="flex items-center gap-2">
150+
<span className="text-xs text-accent-12 font-mono">
151+
{`${deployment.id.slice(0, 3)}...${deployment.id.slice(-4)}`}
152+
</span>
153+
<Badge
154+
variant={isLive ? "success" : "primary"}
155+
className={`px-1.5 capitalize ${isLive ? "text-successA-11" : "text-grayA-11"}`}
156+
>
157+
{isLive ? "Live" : deployment.status}
158+
</Badge>
159+
</div>
160+
<div className="text-xs text-grayA-9">
161+
{deployment.gitCommitMessage || `${isLive ? "Current active" : "Target"} deployment`}
162+
</div>
163+
</div>
164+
</div>
165+
<div className="flex gap-1.5">
166+
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-3 rounded-md text-xs text-grayA-11 max-w-[100px]">
167+
<CodeBranch size="sm-regular" className="shrink-0 text-gray-12" />
168+
<span className="truncate">{deployment.gitBranch}</span>
169+
</div>
170+
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-3 rounded-md text-xs text-grayA-11">
171+
<CodeCommit size="sm-regular" className="shrink-0 text-gray-12" />
172+
<span>{shortenId(deployment.gitCommitSha ?? "")}</span>
173+
</div>
174+
</div>
175+
</div>
176+
</div>
177+
);

apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { type Deployment, collection, collectionManager } from "@/lib/collections";
44
import { shortenId } from "@/lib/shorten-id";
55
import { trpc } from "@/lib/trpc/client";
6-
import { eq, inArray, useLiveQuery } from "@tanstack/react-db";
6+
import { inArray, useLiveQuery } from "@tanstack/react-db";
77
import { CircleInfo, CodeBranch, CodeCommit, Link4 } from "@unkey/icons";
88
import { Badge, Button, DialogContainer, toast } from "@unkey/ui";
99
import { StatusIndicator } from "../../details/active-deployment-card/status-indicator";
@@ -45,8 +45,7 @@ export const RollbackDialog = ({
4545
const domains = useLiveQuery((q) =>
4646
q
4747
.from({ domain: domainCollection })
48-
.where(({ domain }) => inArray(domain.sticky, ["environment", "live"]))
49-
.where(({ domain }) => eq(domain.deploymentId, liveDeployment.id)),
48+
.where(({ domain }) => inArray(domain.sticky, ["environment", "live"])),
5049
);
5150
const rollback = trpc.deploy.deployment.rollback.useMutation({
5251
onSuccess: () => {
@@ -58,6 +57,10 @@ export const RollbackDialog = ({
5857
try {
5958
// @ts-expect-error Their docs say it's here
6059
collection.projects.utils.refetch();
60+
// @ts-expect-error Their docs say it's here
61+
collection.deployments.utils.refetch();
62+
// @ts-expect-error Their docs say it's here
63+
collection.domains.utils.refetch();
6164
} catch (error) {
6265
console.error("Refetch error:", error);
6366
}

apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use client";
2-
import { type MenuItem, TableActionPopover } from "@/components/logs/table-action.popover";
2+
import { TableActionPopover } from "@/components/logs/table-action.popover";
33
import type { Deployment, Environment } from "@/lib/collections";
4-
import { ArrowDottedRotateAnticlockwise } from "@unkey/icons";
4+
import { ArrowDottedRotateAnticlockwise, ChevronUp } from "@unkey/icons";
55
import { useState } from "react";
6+
import { PromotionDialog } from "../../../promotion-dialog";
67
import { RollbackDialog } from "../../../rollback-dialog";
78

89
type DeploymentListTableActionsProps = {
@@ -17,16 +18,51 @@ export const DeploymentListTableActions = ({
1718
environment,
1819
}: DeploymentListTableActionsProps) => {
1920
const [isRollbackModalOpen, setIsRollbackModalOpen] = useState(false);
20-
const menuItems = getDeploymentListTableActionItems(
21-
selectedDeployment,
22-
liveDeployment,
23-
environment,
24-
setIsRollbackModalOpen,
25-
);
21+
const [isPromotionModalOpen, setIsPromotionModalOpen] = useState(false);
22+
23+
const canRollback =
24+
liveDeployment &&
25+
environment?.slug === "production" &&
26+
selectedDeployment.status === "ready" &&
27+
selectedDeployment.id !== liveDeployment.id;
28+
29+
// TODO
30+
// This logic is slightly flawed as it does not allow you to promote a deployment that
31+
// is currently live due to a rollback.
32+
const canPromote =
33+
liveDeployment &&
34+
environment?.slug === "production" &&
35+
selectedDeployment.status === "ready" &&
36+
selectedDeployment.id !== liveDeployment.id;
2637

2738
return (
2839
<>
29-
<TableActionPopover items={menuItems} />
40+
<TableActionPopover
41+
items={[
42+
{
43+
id: "rollback",
44+
label: "Rollback",
45+
icon: <ArrowDottedRotateAnticlockwise size="md-regular" />,
46+
disabled: !canRollback,
47+
onClick: () => {
48+
if (canRollback) {
49+
setIsRollbackModalOpen(true);
50+
}
51+
},
52+
},
53+
{
54+
id: "Promote",
55+
label: "Promote",
56+
icon: <ChevronUp size="md-regular" />,
57+
disabled: !canPromote,
58+
onClick: () => {
59+
if (canPromote) {
60+
setIsPromotionModalOpen(true);
61+
}
62+
},
63+
},
64+
]}
65+
/>
3066
{liveDeployment && selectedDeployment && (
3167
<RollbackDialog
3268
isOpen={isRollbackModalOpen}
@@ -35,34 +71,14 @@ export const DeploymentListTableActions = ({
3571
targetDeployment={selectedDeployment}
3672
/>
3773
)}
74+
{liveDeployment && selectedDeployment && (
75+
<PromotionDialog
76+
isOpen={isPromotionModalOpen}
77+
onOpenChange={setIsPromotionModalOpen}
78+
liveDeployment={liveDeployment}
79+
targetDeployment={selectedDeployment}
80+
/>
81+
)}
3882
</>
3983
);
4084
};
41-
42-
const getDeploymentListTableActionItems = (
43-
selectedDeployment: Deployment,
44-
liveDeployment: Deployment | undefined,
45-
environment: Environment | undefined,
46-
setIsRollbackModalOpen: (open: boolean) => void,
47-
): MenuItem[] => {
48-
// Rollback is only enabled for production deployments that are ready and not currently active
49-
const canRollback =
50-
liveDeployment &&
51-
environment?.slug === "production" &&
52-
selectedDeployment.status === "ready" &&
53-
selectedDeployment.id !== liveDeployment.id;
54-
55-
return [
56-
{
57-
id: "rollback",
58-
label: "Rollback",
59-
icon: <ArrowDottedRotateAnticlockwise size="md-regular" />,
60-
disabled: !canRollback,
61-
onClick: () => {
62-
if (canRollback) {
63-
setIsRollbackModalOpen(true);
64-
}
65-
},
66-
},
67-
];
68-
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { eq, useLiveQuery } from "@tanstack/react-db";
2+
import { useProjectLayout } from "../../../../layout-provider";
3+
4+
type Props = {
5+
deploymentId: string;
6+
// I couldn't figure out how to make the domains revalidate on a rollback
7+
// From my understanding it should already work, because we're using the
8+
// .util.refetch() in the trpc mutation, but it doesn't.
9+
// We need to investigate this later
10+
hackyRevalidateDependency?: unknown;
11+
};
12+
13+
export const DomainList = ({ deploymentId, hackyRevalidateDependency }: Props) => {
14+
const { collections } = useProjectLayout();
15+
const domains = useLiveQuery(
16+
(q) =>
17+
q
18+
.from({ domain: collections.domains })
19+
.where(({ domain }) => eq(domain.deploymentId, deploymentId))
20+
.orderBy(({ domain }) => domain.domain, "asc"),
21+
[hackyRevalidateDependency],
22+
);
23+
24+
return (
25+
<ul className="flex flex-col list-none py-2">
26+
{domains.data.map((domain) => (
27+
<li key={domain.id}>https://{domain.domain}</li>
28+
))}
29+
</ul>
30+
);
31+
};

0 commit comments

Comments
 (0)