Skip to content

Commit 85fd31a

Browse files
chore: redeploy all for env
1 parent 93769fd commit 85fd31a

File tree

4 files changed

+193
-5
lines changed

4 files changed

+193
-5
lines changed

apps/web/app/routes/ws/deployments/_components/RedeployDialog.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export function RedeployDialog({ releaseTarget }: RedeployDialogProps) {
4444
return (
4545
<Dialog open={open} onOpenChange={setOpen}>
4646
<DialogTrigger asChild>
47-
<Button size="sm" className="h-6 w-20 rounded-sm">
47+
<Button
48+
size="sm"
49+
variant="secondary"
50+
className="h-6 w-fit rounded-sm py-3"
51+
>
4852
Redeploy
4953
</Button>
5054
</DialogTrigger>

apps/web/app/routes/ws/deployments/_components/release-targets/EnvironmentReleaseTargetsGroup.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
JobStatusDisplayName,
1313
} from "../../../_components/JobStatusBadge";
1414
import { RedeployDialog } from "../RedeployDialog";
15+
import { RedeployAllDialog } from "./RedeployAllDialog";
1516
import { VersionDisplay } from "./VersionDisplay";
1617

1718
type ReleaseTarget = WorkspaceEngine["schemas"]["ReleaseTargetWithState"];
@@ -103,10 +104,7 @@ export function EnvironmentReleaseTargetsGroup({
103104
/>
104105
</Button>
105106
<div className="grow">{environment.name} </div>
106-
<span className="max-w-[60vw] shrink-0 truncate font-mono text-xs text-muted-foreground">
107-
{cel?.replaceAll("\n", " ").trim() ??
108-
jsonSelector?.trim().replaceAll("\n", " ")}
109-
</span>
107+
<RedeployAllDialog releaseTargets={rts} />
110108
</div>
111109
</TableCell>
112110
</TableRow>
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type { WorkspaceEngine } from "@ctrlplane/workspace-engine-sdk";
2+
import { useState } from "react";
3+
4+
import type { JobStatus } from "../types";
5+
import { trpc } from "~/api/trpc";
6+
import { Button } from "~/components/ui/button";
7+
import {
8+
Dialog,
9+
DialogClose,
10+
DialogContent,
11+
DialogDescription,
12+
DialogFooter,
13+
DialogHeader,
14+
DialogTitle,
15+
DialogTrigger,
16+
} from "~/components/ui/dialog";
17+
import {
18+
HoverCard,
19+
HoverCardContent,
20+
HoverCardTrigger,
21+
} from "~/components/ui/hover-card";
22+
import {
23+
Select,
24+
SelectContent,
25+
SelectItem,
26+
SelectTrigger,
27+
} from "~/components/ui/select";
28+
import { useWorkspace } from "~/components/WorkspaceProvider";
29+
import { cn } from "~/lib/utils";
30+
import { JobStatusDisplayName } from "../../../_components/JobStatusBadge";
31+
32+
type ReleaseTarget = WorkspaceEngine["schemas"]["ReleaseTargetWithState"];
33+
34+
function useRedeployAll(releaseTargets: ReleaseTarget[]) {
35+
const { workspace } = useWorkspace();
36+
const redeployAll = trpc.redeploy.releaseTargets.useMutation();
37+
38+
const handleRedeployAll = () => {
39+
redeployAll.mutateAsync({
40+
workspaceId: workspace.id,
41+
releaseTargets: releaseTargets.map((rt) => ({
42+
...rt.releaseTarget,
43+
})),
44+
});
45+
};
46+
47+
return { handleRedeployAll, isPending: redeployAll.isPending };
48+
}
49+
50+
function StatusSelector({
51+
status,
52+
setStatus,
53+
}: {
54+
status: JobStatus | "all";
55+
setStatus: (status: JobStatus | "all") => void;
56+
}) {
57+
return (
58+
<Select
59+
value={String(status)}
60+
onValueChange={(value) => setStatus(value as JobStatus | "all")}
61+
>
62+
<SelectTrigger>
63+
{status === "all" ? "All statuses" : JobStatusDisplayName[status]}
64+
</SelectTrigger>
65+
<SelectContent>
66+
<SelectItem value="all">All statuses</SelectItem>
67+
{Object.keys(JobStatusDisplayName).map((status) => (
68+
<SelectItem key={status} value={status}>
69+
{JobStatusDisplayName[status as keyof typeof JobStatusDisplayName]}
70+
</SelectItem>
71+
))}
72+
</SelectContent>
73+
</Select>
74+
);
75+
}
76+
77+
function SelectedTargetsHover({
78+
selectedTargets,
79+
}: {
80+
selectedTargets: ReleaseTarget[];
81+
}) {
82+
return (
83+
<HoverCard>
84+
<HoverCardTrigger asChild>
85+
<Button
86+
size="sm"
87+
variant="ghost"
88+
className="cursor-pointer text-sm text-green-500 hover:text-green-400"
89+
>
90+
{selectedTargets.length} resources selected
91+
</Button>
92+
</HoverCardTrigger>
93+
<HoverCardContent className="p-2">
94+
<div className="flex flex-col gap-1 text-sm">
95+
{selectedTargets.map((rt) => (
96+
<div key={rt.releaseTarget.resourceId}>
97+
<span>{rt.resource.name}</span>
98+
</div>
99+
))}
100+
</div>
101+
</HoverCardContent>
102+
</HoverCard>
103+
);
104+
}
105+
106+
export function RedeployAllDialog({
107+
releaseTargets,
108+
}: {
109+
releaseTargets: ReleaseTarget[];
110+
}) {
111+
const [open, setOpen] = useState(false);
112+
113+
const environmentName = releaseTargets[0]?.environment.name ?? "";
114+
const [status, setStatus] = useState<JobStatus | "all">("all");
115+
const selectedTargets = releaseTargets.filter(
116+
(rt) => status === "all" || rt.state.latestJob?.status === status,
117+
);
118+
const { handleRedeployAll, isPending } = useRedeployAll(selectedTargets);
119+
120+
return (
121+
<Dialog open={open} onOpenChange={setOpen}>
122+
<DialogTrigger asChild>
123+
<Button
124+
size="sm"
125+
variant="secondary"
126+
className="h-6 w-fit rounded-sm py-3"
127+
>
128+
Redeploy {environmentName}
129+
</Button>
130+
</DialogTrigger>
131+
<DialogContent>
132+
<DialogHeader>
133+
<DialogTitle>
134+
{" "}
135+
Redeploy all release targets in {environmentName}{" "}
136+
</DialogTitle>
137+
<DialogDescription>
138+
Are you sure you want to redeploy all release targets?
139+
</DialogDescription>
140+
</DialogHeader>
141+
142+
<div className="flex items-center gap-2">
143+
<StatusSelector status={status} setStatus={setStatus} />
144+
{selectedTargets.length > 0 && (
145+
<SelectedTargetsHover selectedTargets={selectedTargets} />
146+
)}
147+
{selectedTargets.length === 0 && (
148+
<span className="text-sm text-muted-foreground">
149+
No resources selected
150+
</span>
151+
)}
152+
</div>
153+
154+
<DialogFooter>
155+
<DialogClose asChild>
156+
<Button variant="outline">Cancel</Button>
157+
</DialogClose>
158+
<Button
159+
onClick={handleRedeployAll}
160+
disabled={isPending || selectedTargets.length === 0}
161+
>
162+
Redeploy
163+
</Button>
164+
</DialogFooter>
165+
</DialogContent>
166+
</Dialog>
167+
);
168+
}

packages/trpc/src/routes/redeploy.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,22 @@ export const redeployRouter = router({
1919
.mutation(({ input: { workspaceId, releaseTarget } }) =>
2020
eventDispatcher.dispatchRedeploy(workspaceId, releaseTarget),
2121
),
22+
23+
releaseTargets: protectedProcedure
24+
.input(
25+
z.object({
26+
workspaceId: z.string(),
27+
releaseTargets: z.array(
28+
z.object({
29+
deploymentId: z.string(),
30+
environmentId: z.string(),
31+
resourceId: z.string(),
32+
}),
33+
),
34+
}),
35+
)
36+
.mutation(async ({ input: { workspaceId, releaseTargets } }) => {
37+
for (const releaseTarget of releaseTargets)
38+
await eventDispatcher.dispatchRedeploy(workspaceId, releaseTarget);
39+
}),
2240
});

0 commit comments

Comments
 (0)