Skip to content

Commit 9486286

Browse files
committed
feat: move approval UI inline on deployment detail page
Replace the separate /authorize page with an inline DeploymentApproval component shown when deployment status is awaiting_approval. Delete the standalone authorize page. Add awaiting_approval to deriveStatusFromSteps valid statuses so it doesn't fall back to pending.
1 parent 06a1cd0 commit 9486286

File tree

4 files changed

+146
-238
lines changed

4 files changed

+146
-238
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"use client";
2+
3+
import { trpc } from "@/lib/trpc/client";
4+
import type { Deployment } from "@/lib/collections/deploy/deployments";
5+
import {
6+
CircleXMark,
7+
CodeBranch,
8+
CodeCommit,
9+
Github,
10+
ShieldAlert,
11+
User,
12+
XMark,
13+
} from "@unkey/icons";
14+
import { Button } from "@unkey/ui";
15+
import { useProjectData } from "../../../data-provider";
16+
17+
export function DeploymentApproval({ deployment }: { deployment: Deployment }) {
18+
const { refetchDeployments } = useProjectData();
19+
20+
const authorize = trpc.deploy.deployment.authorize.useMutation({
21+
onSuccess: () => {
22+
refetchDeployments();
23+
},
24+
});
25+
26+
const shortSha = deployment.gitCommitSha?.slice(0, 7) ?? "";
27+
28+
return (
29+
<div className="flex items-center justify-center min-h-[40vh]">
30+
<div className="w-full max-w-md space-y-6">
31+
{/* Logo connection indicator */}
32+
<div className="flex items-center justify-center gap-4">
33+
<div className="flex items-center justify-center w-12 h-12 rounded-full border border-border bg-background">
34+
<Github iconSize="xl-thin" />
35+
</div>
36+
<div className="flex items-center gap-1.5">
37+
<div className="w-6 border-t border-dashed border-warning-9" />
38+
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-warning-3 border border-warning-6">
39+
<XMark iconSize="sm-regular" className="text-warning-9" />
40+
</div>
41+
<div className="w-6 border-t border-dashed border-warning-9" />
42+
</div>
43+
<div className="flex items-center justify-center w-12 h-12 rounded-full border border-border bg-background">
44+
<ShieldAlert iconSize="xl-thin" className="text-warning-9" />
45+
</div>
46+
</div>
47+
48+
{/* Title */}
49+
<div className="text-center space-y-2">
50+
<h1 className="text-xl font-semibold text-content">Authorization Required</h1>
51+
<p className="text-sm text-content-subtle">
52+
An external contributor pushed a commit. A team member must authorize this deployment
53+
before it can proceed.
54+
</p>
55+
</div>
56+
57+
{/* Commit details */}
58+
<div className="border border-border rounded-lg divide-y divide-border">
59+
{deployment.gitBranch && (
60+
<div className="flex items-center gap-3 px-4 py-3">
61+
<CodeBranch iconSize="md-thin" className="text-content-subtle shrink-0" />
62+
<span className="text-sm text-content-subtle">Branch</span>
63+
<span className="ml-auto text-sm font-mono text-content bg-background-subtle px-2 py-0.5 rounded">
64+
{deployment.gitBranch}
65+
</span>
66+
</div>
67+
)}
68+
69+
{shortSha && (
70+
<div className="flex items-center gap-3 px-4 py-3">
71+
<CodeCommit iconSize="md-thin" className="text-content-subtle shrink-0" />
72+
<span className="text-sm text-content-subtle">Commit</span>
73+
<span className="ml-auto text-sm font-mono text-content bg-background-subtle px-2 py-0.5 rounded">
74+
{shortSha}
75+
</span>
76+
</div>
77+
)}
78+
79+
{deployment.gitCommitMessage && (
80+
<div className="px-4 py-3">
81+
<p className="text-sm text-content truncate">{deployment.gitCommitMessage}</p>
82+
</div>
83+
)}
84+
85+
{deployment.gitCommitAuthorHandle && (
86+
<div className="flex items-center gap-3 px-4 py-3">
87+
{deployment.gitCommitAuthorAvatarUrl ? (
88+
<img
89+
src={deployment.gitCommitAuthorAvatarUrl}
90+
alt={deployment.gitCommitAuthorHandle}
91+
className="w-5 h-5 rounded-full shrink-0"
92+
/>
93+
) : (
94+
<User iconSize="md-thin" className="text-content-subtle shrink-0" />
95+
)}
96+
<span className="text-sm text-content">{deployment.gitCommitAuthorHandle}</span>
97+
</div>
98+
)}
99+
</div>
100+
101+
{/* Error */}
102+
{authorize.error && (
103+
<div className="flex items-start gap-2 p-3 bg-error-3 border border-error-6 rounded-lg">
104+
<CircleXMark iconSize="md-thin" className="text-error-9 mt-0.5 shrink-0" />
105+
<p className="text-sm text-error-11">{authorize.error.message}</p>
106+
</div>
107+
)}
108+
109+
{/* Action */}
110+
<Button
111+
variant="primary"
112+
size="xlg"
113+
className="w-full"
114+
loading={authorize.isLoading}
115+
onClick={() => authorize.mutate({ deploymentId: deployment.id })}
116+
>
117+
Authorize Deployment
118+
</Button>
119+
120+
<p className="text-xs text-center text-content-subtle">
121+
Only workspace members can authorize deployments.
122+
</p>
123+
</div>
124+
</div>
125+
);
126+
}

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/deployment-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const DEPLOYMENT_STATUSES: ReadonlySet<string> = new Set<DeploymentStatus>([
1010
"finalizing",
1111
"ready",
1212
"failed",
13+
"awaiting_approval",
1314
]);
1415

1516
function isDeploymentStatus(value: string): value is DeploymentStatus {
@@ -20,6 +21,11 @@ export function deriveStatusFromSteps(
2021
steps: StepsData | undefined,
2122
fallback: string,
2223
): DeploymentStatus {
24+
// awaiting_approval is authoritative from the DB — steps can't derive it
25+
if (fallback === "awaiting_approval") {
26+
return "awaiting_approval";
27+
}
28+
2329
if (!steps) {
2430
return isDeploymentStatus(fallback) ? fallback : "pending";
2531
}

web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/page.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useMemo } from "react";
44
import { DeploymentDomainsCard } from "../../../components/deployment-domains-card";
55
import { ProjectContentWrapper } from "../../../components/project-content-wrapper";
66
import { useProjectData } from "../../data-provider";
7+
import { DeploymentApproval } from "./(deployment-progress)/deployment-approval";
78
import { DeploymentInfo } from "./(deployment-progress)/deployment-info";
89
import { DeploymentProgress } from "./(deployment-progress)/deployment-progress";
910
import { DeploymentNetworkSection } from "./(overview)/components/sections/deployment-network-section";
@@ -15,10 +16,11 @@ export default function DeploymentOverview() {
1516
const { refetchDomains } = useProjectData();
1617

1718
const ready = deployment.status === "ready";
19+
const awaitingApproval = deployment.status === "awaiting_approval";
1820

1921
const stepsQuery = trpc.deploy.deployment.steps.useQuery(
2022
{ deploymentId: deployment.id },
21-
{ refetchInterval: ready ? false : 1_000, refetchOnWindowFocus: false },
23+
{ refetchInterval: ready || awaitingApproval ? false : 1_000, refetchOnWindowFocus: false },
2224
);
2325

2426
const derivedStatus = useMemo(
@@ -33,6 +35,17 @@ export default function DeploymentOverview() {
3335
}
3436
}, [ready, refetchDomains, stepsQuery.refetch]);
3537

38+
if (awaitingApproval) {
39+
return (
40+
<ProjectContentWrapper centered>
41+
<DeploymentInfo statusOverride={derivedStatus} />
42+
<div className="animate-fade-slide-in">
43+
<DeploymentApproval deployment={deployment} />
44+
</div>
45+
</ProjectContentWrapper>
46+
);
47+
}
48+
3649
return (
3750
<ProjectContentWrapper centered>
3851
<DeploymentInfo statusOverride={derivedStatus} />

0 commit comments

Comments
 (0)