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
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
Loading