Skip to content

Commit

Permalink
fix: Revert policy approval stuff with better UI (#281)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Jan 18, 2025
1 parent f0b8b9e commit 40589ff
Show file tree
Hide file tree
Showing 17 changed files with 4,812 additions and 188 deletions.
11 changes: 7 additions & 4 deletions apps/jobs/src/policy-checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const run = async () => {
const isPassingApprovalGate = or(
isNull(schema.environment.policyId),
eq(schema.environmentPolicy.approvalRequirement, "automatic"),
eq(schema.environmentApproval.status, "approved"),
eq(schema.environmentPolicyApproval.status, "approved"),
);

const releaseJobTriggers = await db
Expand All @@ -29,11 +29,14 @@ export const run = async () => {
eq(schema.environment.policyId, schema.environmentPolicy.id),
)
.leftJoin(
schema.environmentApproval,
schema.environmentPolicyApproval,
and(
eq(schema.environmentApproval.environmentId, schema.environment.id),
eq(
schema.environmentApproval.releaseId,
schema.environmentPolicyApproval.policyId,
schema.environmentPolicy.id,
),
eq(
schema.environmentPolicyApproval.releaseId,
schema.releaseJobTrigger.releaseId,
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Environment } from "@ctrlplane/db/schema";
import { useState } from "react";
import { useRouter } from "next/navigation";

import {
Expand All @@ -11,35 +13,56 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@ctrlplane/ui/alert-dialog";
import { Badge } from "@ctrlplane/ui/badge";
import { Button } from "@ctrlplane/ui/button";

import { api } from "~/trpc/react";
import { Cancelled, Failing, Loading, Passing, Waiting } from "./StatusIcons";

const ApprovalDialog: React.FC<{
releaseId: string;
environmentId: string;
export const ApprovalDialog: React.FC<{
release: { id: string; version: string };
policyId: string;
linkedEnvironments: Array<Environment>;
children: React.ReactNode;
}> = ({ releaseId, environmentId, children }) => {
const approve = api.environment.approval.approve.useMutation();
const rejected = api.environment.approval.reject.useMutation();
}> = ({ release, policyId, linkedEnvironments, children }) => {
const [open, setOpen] = useState(false);
const approve = api.environment.policy.approval.approve.useMutation();
const reject = api.environment.policy.approval.reject.useMutation();
const releaseId = release.id;
const onApprove = () =>
approve
.mutateAsync({ releaseId, environmentId })
.then(() => router.refresh());
.mutateAsync({ releaseId, policyId })
.then(() => router.refresh())
.then(() => setOpen(false));
const onReject = () =>
rejected
.mutateAsync({ releaseId, environmentId })
.then(() => router.refresh());
reject
.mutateAsync({ releaseId, policyId })
.then(() => router.refresh())
.then(() => setOpen(false));
const router = useRouter();
return (
<AlertDialog>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Approval</AlertDialogTitle>
<AlertDialogTitle className="text-xl font-semibold">
Approve release <span className="truncate">{release.version}</span>
</AlertDialogTitle>
<AlertDialogDescription>
Approving this action will initiate the deployment of the release to
all currently linked environments.
<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-24 truncate"
>
{env.name}
</Badge>
))}
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
Expand All @@ -52,13 +75,14 @@ const ApprovalDialog: React.FC<{
};

export const ApprovalCheck: React.FC<{
environmentId: string;
releaseId: string;
}> = ({ environmentId, releaseId }) => {
policyId: string;
release: { id: string; version: string };
linkedEnvironments: Array<Environment>;
}> = ({ policyId, release, linkedEnvironments }) => {
const approvalStatus =
api.environment.approval.statusByReleaseEnvironmentId.useQuery({
environmentId,
releaseId,
api.environment.policy.approval.statusByReleasePolicyId.useQuery({
policyId,
releaseId: release.id,
});

if (approvalStatus.isLoading)
Expand All @@ -77,25 +101,36 @@ export const ApprovalCheck: React.FC<{

const status = approvalStatus.data.status;
return (
<ApprovalDialog environmentId={environmentId} releaseId={releaseId}>
<button
disabled={status === "approved" || status === "rejected"}
className="flex w-full items-center gap-2 rounded-md hover:bg-neutral-800/50"
>
{status === "approved" ? (
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center gap-2">
{status === "approved" && (
<>
<Passing /> Approved
</>
) : status === "rejected" ? (
)}
{status === "rejected" && (
<>
<Failing /> Rejected
</>
) : (
)}
{status === "pending" && (
<>
<Waiting /> Pending approval
</>
)}
</button>
</ApprovalDialog>
</div>

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

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

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

import { api } from "~/trpc/react";
import { ApprovalDialog } from "./ApprovalCheck";

type EnvironmentApprovalRowProps = {
approval: EnvironmentApproval & { user?: User | null };
environment?: Environment;
approval: EnvironmentPolicyApproval & { user?: User | null };
release: { id: string; version: string };
linkedEnvironments: Environment[];
};

export const EnvironmentApprovalRow: React.FC<EnvironmentApprovalRowProps> = ({
approval,
environment,
release,
linkedEnvironments,
}) => {
const router = useRouter();
const utils = api.useUtils();

if (!environment) {
console.error("Environment is undefined for approval:", approval);
return null;
}

const environmentName = environment.name;
const { releaseId, environmentId, status } = approval;

const rejectMutation = api.environment.approval.reject.useMutation({
onSuccess: ({ cancelledJobCount }) => {
router.refresh();
utils.environment.policy.invalidate();
utils.job.config.invalidate();
toast.success(
`Rejected release to ${environmentName} and cancelled ${cancelledJobCount} job${cancelledJobCount !== 1 ? "s" : ""}`,
);
},
onError: () => toast.error("Error rejecting release"),
});

const approveMutation = api.environment.approval.approve.useMutation({
onSuccess: () => {
router.refresh();
utils.environment.policy.invalidate();
utils.job.config.invalidate();
toast.success(`Approved release to ${environmentName}`);
},
onError: () => toast.error("Error approving release"),
});

const handleReject = () =>
rejectMutation.mutate({ releaseId, environmentId });
const handleApprove = () =>
approveMutation.mutate({ releaseId, environmentId });
if (approval.status === "pending")
return (
<ApprovalDialog
release={release}
policyId={approval.policyId}
linkedEnvironments={linkedEnvironments}
>
<Button size="sm" className="h-6">
Review
</Button>
</ApprovalDialog>
);

return (
<div className="flex items-center gap-2 rounded-md text-sm">
{status === "pending" ? (
<div className="flex items-center gap-2">
<Button
variant="secondary"
className="h-6 px-2"
onClick={handleReject}
>
Reject
</Button>
<Button className="h-6 px-2" onClick={handleApprove}>
Approve
</Button>
</div>
<div className="ml-2 flex flex-grow items-center gap-2 rounded-md text-sm font-medium">
{approval.status === "approved" ? (
<span className="text-green-300">Approved</span>
) : (
<div className="ml-2 flex-grow">
<span className="font-medium">
{status === "approved" ? (
<span className="text-green-300">Approved</span>
) : (
<span className="text-red-300">Rejected</span>
)}{" "}
by {approval.user?.name}
</span>
</div>
)}
<span className="text-red-300">Rejected</span>
)}{" "}
{approval.user?.name ? `by ${approval.user.name}` : null}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { EnvironmentPolicyDrawerTab } from "~/app/[workspaceSlug]/(app)/_compone
import { useReleaseChannelDrawer } from "~/app/[workspaceSlug]/(app)/_components/release-channel-drawer/useReleaseChannelDrawer";
import { useQueryParams } from "~/app/[workspaceSlug]/(app)/_components/useQueryParams";
import { api } from "~/trpc/react";
import { ApprovalCheck } from "./ApprovalCheck";
import { Cancelled, Failing, Loading, Passing, Waiting } from "./StatusIcons";

type EnvironmentNodeProps = NodeProps<{
Expand Down Expand Up @@ -329,7 +328,6 @@ export const EnvironmentNode: React.FC<EnvironmentNodeProps> = ({ data }) => (
<WaitingOnActiveCheck {...data} />
<ReleaseChannelCheck {...data} />
<MinReleaseIntervalCheck {...data} />
<ApprovalCheck {...data} />
</div>
</div>
<Handle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ 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,4 +1,5 @@
import type {
Environment,
EnvironmentPolicy,
EnvironmentPolicyDeployment,
Release,
Expand All @@ -15,12 +16,14 @@ import { cn } from "@ctrlplane/ui";
import { JobStatus } from "@ctrlplane/validators/jobs";

import { api } from "~/trpc/react";
import { ApprovalCheck } from "./ApprovalCheck";
import { Passing, Waiting } from "./StatusIcons";

type PolicyNodeProps = NodeProps<
EnvironmentPolicy & {
release: Release;
policyDeployments: Array<EnvironmentPolicyDeployment>;
linkedEnvironments: Array<Environment>;
}
>;

Expand Down Expand Up @@ -99,6 +102,7 @@ const GradualRolloutCheck: React.FC<PolicyNodeProps["data"]> = (data) => {
export const PolicyNode: React.FC<PolicyNodeProps> = ({ data }) => {
const noMinSuccess = data.successType === "optional";
const noRollout = data.rolloutDuration === 0;
const noApproval = data.approvalRequirement === "automatic";

return (
<>
Expand All @@ -109,8 +113,15 @@ export const PolicyNode: React.FC<PolicyNodeProps> = ({ data }) => {
>
{!noMinSuccess && <MinSuccessCheck {...data} />}
{!noRollout && <GradualRolloutCheck {...data} />}
{!noApproval && (
<ApprovalCheck
policyId={data.id}
release={data.release}
linkedEnvironments={data.linkedEnvironments}
/>
)}

{noMinSuccess && noRollout && (
{noMinSuccess && noRollout && noApproval && (
<div className="text-muted-foreground">No policy checks.</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ const CollapsibleTableRow: React.FC<CollapsibleTableRowProps> = ({
}) => {
const { setJobId } = useJobDrawer();

const approvalsQ = api.environment.approval.byReleaseId.useQuery({
const approvalsQ = api.environment.policy.approval.byReleaseId.useQuery({
releaseId: release.id,
});

const approvals = approvalsQ.data ?? [];
const environmentApprovals = approvals.filter(
(approval) => approval.environmentId === environment.id,
(a) => a.policyId === environment.policyId,
);

const allTriggers = Object.values(triggersByResource).flat();
Expand All @@ -80,6 +80,13 @@ 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 @@ -145,7 +152,8 @@ const CollapsibleTableRow: React.FC<CollapsibleTableRowProps> = ({
<EnvironmentApprovalRow
key={approval.id}
approval={approval}
environment={environment}
release={release}
linkedEnvironments={linkedEnvironments}
/>
))}
</div>
Expand Down
Loading

0 comments on commit 40589ff

Please sign in to comment.