Skip to content

Commit

Permalink
fix: Make approvals per-environment and clean up release reactflow (#279
Browse files Browse the repository at this point in the history
)
  • Loading branch information
adityachoudhari26 authored Jan 10, 2025
1 parent 935cb7e commit 33c26e0
Show file tree
Hide file tree
Showing 19 changed files with 4,861 additions and 421 deletions.
11 changes: 4 additions & 7 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.environmentPolicyApproval.status, "approved"),
eq(schema.environmentApproval.status, "approved"),
);

const releaseJobTriggers = await db
Expand All @@ -29,14 +29,11 @@ export const run = async () => {
eq(schema.environment.policyId, schema.environmentPolicy.id),
)
.leftJoin(
schema.environmentPolicyApproval,
schema.environmentApproval,
and(
eq(schema.environmentApproval.environmentId, schema.environment.id),
eq(
schema.environmentPolicyApproval.policyId,
schema.environmentPolicy.id,
),
eq(
schema.environmentPolicyApproval.releaseId,
schema.environmentApproval.releaseId,
schema.releaseJobTrigger.releaseId,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,12 @@ export const createEdgesWhereEnvironmentHasNoPolicy = (
};
});

export const createEdgesFromPolicyToReleaseSequencing = (
export const createEdgesFromPolicyToEnvironment = (
envs: Array<{ id: string; policyId?: string | null }>,
) =>
envs.map((e) => ({
id: `${e.policyId ?? "trigger"}-release-sequencing-${e.id}`,
id: `${e.policyId ?? "trigger"}-${e.id}`,
source: e.policyId ?? "trigger",
target: `${e.id}-release-sequencing`,
markerEnd,
}));

export const createEdgesFromReleaseSequencingToEnvironment = (
envs: Array<{ id: string; policyId?: string | null }>,
) =>
envs.map((e) => ({
id: `${e.id}-release-sequencing-${e.id}`,
source: `${e.id}-release-sequencing`,
target: e.id,
markerEnd,
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useRouter } from "next/navigation";

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@ctrlplane/ui/alert-dialog";

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

const ApprovalDialog: React.FC<{
releaseId: string;
environmentId: string;
children: React.ReactNode;
}> = ({ releaseId, environmentId, children }) => {
const approve = api.environment.approval.approve.useMutation();
const rejected = api.environment.approval.reject.useMutation();
const onApprove = () =>
approve
.mutateAsync({ releaseId, environmentId })
.then(() => router.refresh());
const onReject = () =>
rejected
.mutateAsync({ releaseId, environmentId })
.then(() => router.refresh());
const router = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Approval</AlertDialogTitle>
<AlertDialogDescription>
Approving this action will initiate the deployment of the release to
all currently linked environments.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onReject}>Reject</AlertDialogCancel>
<AlertDialogAction onClick={onApprove}>Approve</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

export const ApprovalCheck: React.FC<{
environmentId: string;
releaseId: string;
}> = ({ environmentId, releaseId }) => {
const approvalStatus =
api.environment.approval.statusByReleaseEnvironmentId.useQuery({
environmentId,
releaseId,
});

if (approvalStatus.isLoading)
return (
<div className="flex items-center gap-2">
<Loading /> Loading approval status
</div>
);

if (approvalStatus.data == null)
return (
<div className="flex items-center gap-2">
<Cancelled /> Approval skipped
</div>
);

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" ? (
<>
<Passing /> Approved
</>
) : status === "rejected" ? (
<>
<Failing /> Rejected
</>
) : (
<>
<Waiting /> Pending approval
</>
)}
</button>
</ApprovalDialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type {
Environment,
EnvironmentPolicyApproval,
EnvironmentApproval,
User,
} from "@ctrlplane/db/schema";
import { useRouter } from "next/navigation";
Expand All @@ -12,12 +12,12 @@ import { toast } from "@ctrlplane/ui/toast";

import { api } from "~/trpc/react";

type PolicyApprovalRowProps = {
approval: EnvironmentPolicyApproval & { user?: User | null };
environment: Environment | undefined;
type EnvironmentApprovalRowProps = {
approval: EnvironmentApproval & { user?: User | null };
environment?: Environment;
};

export const PolicyApprovalRow: React.FC<PolicyApprovalRowProps> = ({
export const EnvironmentApprovalRow: React.FC<EnvironmentApprovalRowProps> = ({
approval,
environment,
}) => {
Expand All @@ -30,9 +30,9 @@ export const PolicyApprovalRow: React.FC<PolicyApprovalRowProps> = ({
}

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

const rejectMutation = api.environment.policy.approval.reject.useMutation({
const rejectMutation = api.environment.approval.reject.useMutation({
onSuccess: ({ cancelledJobCount }) => {
router.refresh();
utils.environment.policy.invalidate();
Expand All @@ -44,7 +44,7 @@ export const PolicyApprovalRow: React.FC<PolicyApprovalRowProps> = ({
onError: () => toast.error("Error rejecting release"),
});

const approveMutation = api.environment.policy.approval.approve.useMutation({
const approveMutation = api.environment.approval.approve.useMutation({
onSuccess: () => {
router.refresh();
utils.environment.policy.invalidate();
Expand All @@ -55,15 +55,9 @@ export const PolicyApprovalRow: React.FC<PolicyApprovalRowProps> = ({
});

const handleReject = () =>
rejectMutation.mutate({
releaseId,
policyId,
});
rejectMutation.mutate({ releaseId, environmentId });
const handleApprove = () =>
approveMutation.mutate({
releaseId,
policyId,
});
approveMutation.mutate({ releaseId, environmentId });

return (
<div className="flex items-center gap-2 rounded-md text-sm">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
import type { ReleaseCondition } from "@ctrlplane/validators/releases";
import type { NodeProps } from "reactflow";
import { useEffect, useState } from "react";
import { IconCheck, IconLoader2, IconMinus, IconX } from "@tabler/icons-react";
import { IconPlant } from "@tabler/icons-react";
import { differenceInMilliseconds } from "date-fns";
import _ from "lodash";
import prettyMilliseconds from "pretty-ms";
Expand All @@ -17,6 +17,7 @@ import colors from "tailwindcss/colors";

import { cn } from "@ctrlplane/ui";
import { Button } from "@ctrlplane/ui/button";
import { Separator } from "@ctrlplane/ui/separator";
import {
ColumnOperator,
ComparisonOperator,
Expand All @@ -29,47 +30,20 @@ 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 ReleaseSequencingNodeProps = NodeProps<{
type EnvironmentNodeProps = NodeProps<{
workspaceId: string;
policy?: SCHEMA.EnvironmentPolicy;
releaseId: string;
releaseVersion: string;
deploymentId: string;
environmentId: string;
environmentName: string;
}>;

const Passing: React.FC = () => (
<div className="rounded-full bg-green-400 p-0.5 dark:text-black">
<IconCheck strokeWidth={3} className="h-3 w-3" />
</div>
);

const Failing: React.FC = () => (
<div className="rounded-full bg-red-400 p-0.5 dark:text-black">
<IconX strokeWidth={3} className="h-3 w-3" />
</div>
);

const Waiting: React.FC = () => (
<div className="animate-spin rounded-full bg-blue-400 p-0.5 dark:text-black">
<IconLoader2 strokeWidth={3} className="h-3 w-3" />
</div>
);

const Loading: React.FC = () => (
<div className="rounded-full bg-muted-foreground p-0.5 dark:text-black">
<IconLoader2 strokeWidth={3} className="h-3 w-3 animate-spin" />
</div>
);

const Cancelled: React.FC = () => (
<div className="rounded-full bg-neutral-400 p-0.5 dark:text-black">
<IconMinus strokeWidth={3} className="h-3 w-3" />
</div>
);

const WaitingOnActiveCheck: React.FC<ReleaseSequencingNodeProps["data"]> = ({
const WaitingOnActiveCheck: React.FC<EnvironmentNodeProps["data"]> = ({
workspaceId,
releaseId,
environmentId,
Expand Down Expand Up @@ -162,7 +136,7 @@ const WaitingOnActiveCheck: React.FC<ReleaseSequencingNodeProps["data"]> = ({
);
};

const ReleaseChannelCheck: React.FC<ReleaseSequencingNodeProps["data"]> = ({
const ReleaseChannelCheck: React.FC<EnvironmentNodeProps["data"]> = ({
deploymentId,
environmentId,
releaseVersion,
Expand Down Expand Up @@ -249,7 +223,7 @@ const ReleaseChannelCheck: React.FC<ReleaseSequencingNodeProps["data"]> = ({
);
};

const MinReleaseIntervalCheck: React.FC<ReleaseSequencingNodeProps["data"]> = ({
const MinReleaseIntervalCheck: React.FC<EnvironmentNodeProps["data"]> = ({
policy,
deploymentId,
environmentId,
Expand Down Expand Up @@ -339,18 +313,24 @@ const MinReleaseIntervalCheck: React.FC<ReleaseSequencingNodeProps["data"]> = ({
);
};

export const ReleaseSequencingNode: React.FC<ReleaseSequencingNodeProps> = ({
data,
}) => (
export const EnvironmentNode: React.FC<EnvironmentNodeProps> = ({ data }) => (
<>
<div
className={cn(
"relative w-[250px] space-y-1 rounded-md border px-2 py-1.5 text-sm",
)}
className={cn("relative w-[250px] space-y-1 rounded-md border text-sm")}
>
<WaitingOnActiveCheck {...data} />
<ReleaseChannelCheck {...data} />
<MinReleaseIntervalCheck {...data} />
<div className="flex items-center gap-2 p-2">
<div className="flex-shrink-0 rounded bg-green-500/20 p-1 text-green-400">
<IconPlant className="h-3 w-3" />
</div>
{data.environmentName}
</div>
<Separator className="!m-0 bg-neutral-800" />
<div className="px-2 pb-2">
<WaitingOnActiveCheck {...data} />
<ReleaseChannelCheck {...data} />
<MinReleaseIntervalCheck {...data} />
<ApprovalCheck {...data} />
</div>
</div>
<Handle
type="target"
Expand Down
Loading

0 comments on commit 33c26e0

Please sign in to comment.