Skip to content
Merged
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
Expand Up @@ -18,6 +18,7 @@ import { ReleaseStatus } from "@ctrlplane/validators/releases";

import { useReleaseChannelDrawer } from "~/app/[workspaceSlug]/(app)/_components/release-channel-drawer/useReleaseChannelDrawer";
import { api } from "~/trpc/react";
import { ApprovalDialog } from "./[deploymentSlug]/releases/[versionId]/ApprovalCheck";
import { DeployButton } from "./DeployButton";
import { Release } from "./TableCells";

Expand Down Expand Up @@ -48,6 +49,12 @@ const ReleaseEnvironmentCell: React.FC<ReleaseEnvironmentCellProps> = ({
const { data: blockedEnvsResult, isLoading: isBlockedEnvsLoading } =
api.release.blocked.useQuery([release.id]);

const { data: approval, isLoading: isApprovalLoading } =
api.environment.policy.approval.statusByReleasePolicyId.useQuery(
{ releaseId: release.id, policyId: environment.policyId ?? "" },
{ enabled: environment.policyId != null },
);

const blockedEnv = blockedEnvsResult?.find(
(b) => b.environmentId === environment.id,
);
Expand Down Expand Up @@ -85,7 +92,9 @@ const ReleaseEnvironmentCell: React.FC<ReleaseEnvironmentCellProps> = ({
isWorkspaceLoading ||
isStatusesLoading ||
isResourcesLoading ||
isBlockedEnvsLoading;
isBlockedEnvsLoading ||
isApprovalLoading;

if (isLoading)
return <p className="text-xs text-muted-foreground">Loading...</p>;

Expand All @@ -95,19 +104,14 @@ const ReleaseEnvironmentCell: React.FC<ReleaseEnvironmentCellProps> = ({
const hasJobAgent = deployment.jobAgentId != null;
const isBlockedByReleaseChannel = blockedEnv != null;

const isPendingApproval = approval?.status === "pending";

const showBlockedByReleaseChannel =
isBlockedByReleaseChannel &&
!statuses?.some((s) => s.job.status === JobStatus.InProgress);

const isReady = release.status === ReleaseStatus.Ready;

const showRelease = isAlreadyDeployed && !showBlockedByReleaseChannel;
const showDeployButton =
!isAlreadyDeployed &&
hasJobAgent &&
hasResources &&
!isBlockedByReleaseChannel &&
isReady;
const showRelease =
isAlreadyDeployed && !showBlockedByReleaseChannel && !isPendingApproval;

if (showRelease)
return (
Expand All @@ -124,11 +128,6 @@ const ReleaseEnvironmentCell: React.FC<ReleaseEnvironmentCellProps> = ({
/>
);

if (showDeployButton)
return (
<DeployButton releaseId={release.id} environmentId={environment.id} />
);

if (release.status === ReleaseStatus.Building)
return (
<div className="text-center text-xs text-muted-foreground/70">
Expand Down Expand Up @@ -172,11 +171,20 @@ const ReleaseEnvironmentCell: React.FC<ReleaseEnvironmentCellProps> = ({
</div>
);

return (
<div className="text-center text-xs text-muted-foreground/70">
Release not deployed
</div>
);
if (isPendingApproval)
return (
<ApprovalDialog policyId={approval.policyId} release={release}>
<Button
className="w-full border-dashed border-neutral-800/50 bg-transparent text-center text-neutral-800 hover:border-blue-400 hover:bg-transparent hover:text-blue-400"
variant="outline"
size="sm"
>
Pending approval
</Button>
</ApprovalDialog>
);

return <DeployButton releaseId={release.id} environmentId={environment.id} />;
};

export const LazyReleaseEnvironmentCell: React.FC<
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Environment } from "@ctrlplane/db/schema";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { IconLoader2 } from "@tabler/icons-react";

import {
AlertDialog,
Expand All @@ -22,24 +22,34 @@ import { Cancelled, Failing, Loading, Passing, Waiting } from "./StatusIcons";
export const ApprovalDialog: React.FC<{
release: { id: string; version: string };
policyId: string;
linkedEnvironments: Array<Environment>;
children: React.ReactNode;
}> = ({ release, policyId, linkedEnvironments, children }) => {
}> = ({ release, policyId, children }) => {
const policyQ = api.environment.policy.byId.useQuery(policyId);

const [open, setOpen] = useState(false);
const approve = api.environment.policy.approval.approve.useMutation();
const reject = api.environment.policy.approval.reject.useMutation();
const utils = api.useUtils();
const invalidateApproval = () =>
utils.environment.policy.approval.statusByReleasePolicyId.invalidate({
policyId,
releaseId: release.id,
});
const releaseId = release.id;
const onApprove = () =>
approve
.mutateAsync({ releaseId, policyId })
.then(() => router.refresh())
.then(() => invalidateApproval())
.then(() => setOpen(false));
const onReject = () =>
reject
.mutateAsync({ releaseId, policyId })
.then(() => router.refresh())
.then(() => invalidateApproval())
.then(() => setOpen(false));
const router = useRouter();

return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
Expand All @@ -48,23 +58,36 @@ export const ApprovalDialog: React.FC<{
<AlertDialogTitle className="text-xl font-semibold">
Approve release <span className="truncate">{release.version}</span>
</AlertDialogTitle>
<AlertDialogDescription>
<div className="flex flex-col gap-2">
Approves this release for the following environments:
<div className="flex flex-wrap gap-2">
{linkedEnvironments.map((env) => (
<Badge key={env.id} variant="secondary" className="max-w-40">
<span className="truncate">{env.name}</span>
</Badge>
))}
{policyQ.isLoading && (
<AlertDialogDescription className="flex items-center justify-center">
<IconLoader2 className="animate-spin" />
</AlertDialogDescription>
)}
{!policyQ.isLoading && (
<AlertDialogDescription>
<div className="flex flex-col gap-2">
Approves this release for the following environments:
<div className="flex flex-wrap gap-2">
{policyQ.data?.environments.map((env) => (
<Badge
key={env.id}
variant="secondary"
className="max-w-40"
>
<span className="truncate">{env.name}</span>
</Badge>
))}
</div>
</div>
</div>
</AlertDialogDescription>
</AlertDialogDescription>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onReject}>Reject</AlertDialogCancel>
<AlertDialogAction onClick={onApprove}>Approve</AlertDialogAction>
</AlertDialogFooter>
{!policyQ.isLoading && (
<AlertDialogFooter>
<AlertDialogCancel onClick={onReject}>Reject</AlertDialogCancel>
<AlertDialogAction onClick={onApprove}>Approve</AlertDialogAction>
</AlertDialogFooter>
)}
</AlertDialogContent>
</AlertDialog>
);
Expand All @@ -73,8 +96,7 @@ export const ApprovalDialog: React.FC<{
export const ApprovalCheck: React.FC<{
policyId: string;
release: { id: string; version: string };
linkedEnvironments: Array<Environment>;
}> = ({ policyId, release, linkedEnvironments }) => {
}> = ({ policyId, release }) => {
const approvalStatus =
api.environment.policy.approval.statusByReleasePolicyId.useQuery({
policyId,
Expand Down Expand Up @@ -117,11 +139,7 @@ export const ApprovalCheck: React.FC<{
</div>

{status === "pending" && (
<ApprovalDialog
policyId={policyId}
release={release}
linkedEnvironments={linkedEnvironments}
>
<ApprovalDialog policyId={policyId} release={release}>
<Button size="sm" className="h-6 px-2 py-1">
Review
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
"use client";

import type {
Environment,
EnvironmentPolicyApproval,
User,
} from "@ctrlplane/db/schema";
import type { EnvironmentPolicyApproval, User } from "@ctrlplane/db/schema";

import { Button } from "@ctrlplane/ui/button";

Expand All @@ -13,21 +9,15 @@ import { ApprovalDialog } from "./ApprovalCheck";
type EnvironmentApprovalRowProps = {
approval: EnvironmentPolicyApproval & { user?: User | null };
release: { id: string; version: string };
linkedEnvironments: Environment[];
};

export const EnvironmentApprovalRow: React.FC<EnvironmentApprovalRowProps> = ({
approval,
release,
linkedEnvironments,
}) => {
if (approval.status === "pending")
return (
<ApprovalDialog
release={release}
policyId={approval.policyId}
linkedEnvironments={linkedEnvironments}
>
<ApprovalDialog release={release} policyId={approval.policyId}>
<Button size="sm" className="h-6">
Review
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export const FlowDiagram: React.FC<{
policyDeployments: policyDeployments.filter(
(p) => p.policyId === policy.id,
),
linkedEnvironments: envs.filter((e) => e.policyId === policy.id),
label: policy.name,
release,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {
Environment,
EnvironmentPolicy,
EnvironmentPolicyDeployment,
Release,
Expand All @@ -22,7 +21,6 @@ type PolicyNodeProps = NodeProps<
EnvironmentPolicy & {
release: Release;
policyDeployments: Array<EnvironmentPolicyDeployment>;
linkedEnvironments: Array<Environment>;
}
>;

Expand Down Expand Up @@ -131,11 +129,7 @@ export const PolicyNode: React.FC<PolicyNodeProps> = ({ data }) => {
{!noMinSuccess && <MinSuccessCheck {...data} />}
{!noRollout && <GradualRolloutCheck {...data} />}
{!noApproval && (
<ApprovalCheck
policyId={data.id}
release={data.release}
linkedEnvironments={data.linkedEnvironments}
/>
<ApprovalCheck policyId={data.id} release={data.release} />
)}

{noMinSuccess && noRollout && noApproval && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,6 @@ const CollapsibleTableRow: React.FC<CollapsibleTableRowProps> = ({
Record<string, boolean>
>({});

const environmentPolicyQ = api.environment.policy.byId.useQuery(
environment.policyId ?? "",
{ enabled: environment.policyId != null },
);

const linkedEnvironments = environmentPolicyQ.data?.environments ?? [];

const switchResourceExpandedState = (resourceId: string) =>
setExpandedResources((prev) => {
const newState = { ...prev };
Expand Down Expand Up @@ -153,7 +146,6 @@ const CollapsibleTableRow: React.FC<CollapsibleTableRowProps> = ({
key={approval.id}
approval={approval}
release={release}
linkedEnvironments={linkedEnvironments}
/>
))}
</div>
Expand Down
Loading