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 @@ -11,8 +11,8 @@ import { Badge } from "@ctrlplane/ui/badge";
import { TableCell } from "@ctrlplane/ui/table";
import { failedStatuses, JobStatus } from "@ctrlplane/validators/jobs";

import { CollapsibleRow } from "~/app/[workspaceSlug]/(app)/_components/CollapsibleRow";
import { JobTableStatusIcon } from "~/app/[workspaceSlug]/(app)/_components/job/JobTableStatusIcon";
import { CollapsibleRow } from "./CollapsibleRow";
import { EnvironmentRowDropdown } from "./EnvironmentRowDropdown";

const JobStatusBadge: React.FC<{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { RouterOutputs } from "@ctrlplane/api";
import React from "react";
import { IconCheck } from "@tabler/icons-react";
import { formatDistanceToNowStrict } from "date-fns";
import { IconCheck, IconShieldFilled } from "@tabler/icons-react";
import { formatDistanceToNowStrict, isBefore } from "date-fns";

import { Skeleton } from "@ctrlplane/ui/skeleton";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@ctrlplane/ui/hover-card";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@ctrlplane/ui/tooltip";

import { api } from "~/trpc/react";
import {
Expand Down Expand Up @@ -118,7 +120,45 @@ const RolloutCheck: React.FC<{
);
};

export const PolicyEvaluationHover: React.FC<{
const getIsPassingAllRules = (policyEvaluations?: PolicyEvaluation) => {
if (policyEvaluations == null) return true;

const isFailingAnyApprovalRules =
Object.values(policyEvaluations.rules.anyApprovals).flat().length > 0;
if (isFailingAnyApprovalRules) return false;

const isFailingUserApprovalRules =
Object.values(policyEvaluations.rules.userApprovals).flat().length > 0;
if (isFailingUserApprovalRules) return false;

const isFailingRoleApprovalRules =
Object.values(policyEvaluations.rules.roleApprovals).flat().length > 0;
if (isFailingRoleApprovalRules) return false;

const isFailingVersionSelectorRules = Object.values(
policyEvaluations.rules.versionSelector,
)
.flat()
.some((v) => v === false);
if (isFailingVersionSelectorRules) return false;

const now = new Date();
const { rolloutTime } = policyEvaluations.rules.rolloutInfo;
const isFailingRolloutRule =
rolloutTime == null || isBefore(now, rolloutTime);
if (isFailingRolloutRule) return false;

return true;
};

const PolicyBlockCell: React.FC = () => (
<div className="flex w-fit cursor-pointer items-center gap-1">
<IconShieldFilled className="h-3 w-3 text-muted-foreground" />
<span className="text-sm">Blocked by policy</span>
</div>
);

export const PolicyEvaluationTooltip: React.FC<{
releaseTargetId: string;
versionId: string;
children: React.ReactNode;
Expand Down Expand Up @@ -146,28 +186,30 @@ export const PolicyEvaluationHover: React.FC<{
) ?? false;

const hasRules = hasApprovalRules || hasVersionSelectorRule || hasRolloutRule;

if (!hasRules) return <span className="text-sm">No jobs</span>;
const isPassingAllRules = getIsPassingAllRules(policyEvaluations);

return (
<HoverCard>
<HoverCardTrigger asChild onMouseEnter={(e) => e.stopPropagation()}>
{props.children}
</HoverCardTrigger>
<HoverCardContent side="right" className="p-2">
{isLoading && <div>Loading...</div>}
{!isLoading && policyEvaluations != null && (
<div className="flex flex-col gap-2">
{hasApprovalRules && (
<ApprovalCheck policyEvaluations={policyEvaluations} />
)}
{hasVersionSelectorRule && (
<VersionSelectorCheck policyEvaluations={policyEvaluations} />
)}
{hasRolloutRule && <RolloutCheck {...props} />}
</div>
)}
</HoverCardContent>
</HoverCard>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild onMouseEnter={(e) => e.stopPropagation()}>
{isPassingAllRules ? props.children : <PolicyBlockCell />}
</TooltipTrigger>
<TooltipContent side="right" className="border bg-neutral-950 p-2">
{isLoading && <Skeleton className="h-4 w-24" />}
{!isLoading && policyEvaluations != null && (
<div className="flex flex-col gap-2">
{hasApprovalRules && (
<ApprovalCheck policyEvaluations={policyEvaluations} />
)}
{hasVersionSelectorRule && (
<VersionSelectorCheck policyEvaluations={policyEvaluations} />
)}
{hasRolloutRule && <RolloutCheck {...props} />}
</div>
)}
{!isLoading && !hasRules && "No policies applied"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
"use client";

import type * as SCHEMA from "@ctrlplane/db/schema";
import Link from "next/link";
import {
IconAlertTriangle,
IconChevronRight,
IconCopy,
IconDots,
IconExternalLink,
IconReload,
IconShieldFilled,
IconSwitch,
} from "@tabler/icons-react";
import { capitalCase } from "change-case";
import { formatDistanceToNowStrict } from "date-fns";
import { useCopyToClipboard } from "react-use";

import { cn } from "@ctrlplane/ui";
import { Button, buttonVariants } from "@ctrlplane/ui/button";
import { Button } from "@ctrlplane/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -27,13 +24,14 @@ import {
import { TableCell, TableRow } from "@ctrlplane/ui/table";
import { toast } from "@ctrlplane/ui/toast";

import { CollapsibleRow } from "~/app/[workspaceSlug]/(app)/_components/CollapsibleRow";
import { JobLinks } from "~/app/[workspaceSlug]/(app)/_components/job/JobLinks";
import { JobTableStatusIcon } from "~/app/[workspaceSlug]/(app)/_components/job/JobTableStatusIcon";
import { OverrideJobStatusDialog } from "~/app/[workspaceSlug]/(app)/_components/job/OverrideJobStatusDialog";
import { ForceDeployVersionDialog } from "~/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ForceDeployVersion";
import { RedeployVersionDialog } from "~/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/RedeployVersionDialog";
import { api } from "~/trpc/react";
import { CollapsibleRow } from "./CollapsibleRow";
import { PolicyEvaluationHover } from "./PolicyEvaluationHover";
import { PolicyEvaluationTooltip } from "./PolicyEvaluationTooltip";

const ReleaseTargetActionsDropdownMenu: React.FC<{
environment: { id: string; name: string };
Expand Down Expand Up @@ -112,12 +110,19 @@ const ReleaseTargetActionsDropdownMenu: React.FC<{

const JobStatusCell: React.FC<{
status: SCHEMA.JobStatus;
}> = ({ status }) => (
releaseTargetId: string;
versionId: string;
}> = ({ status, releaseTargetId, versionId }) => (
<TableCell className="w-26">
<div className="flex items-center gap-1">
<JobTableStatusIcon status={status} />
{capitalCase(status)}
</div>
<PolicyEvaluationTooltip
releaseTargetId={releaseTargetId}
versionId={versionId}
>
<div className="flex w-fit items-center gap-1">
<JobTableStatusIcon status={status} />
{capitalCase(status)}
</div>
</PolicyEvaluationTooltip>
</TableCell>
);

Expand All @@ -135,29 +140,9 @@ const ExternalIdCell: React.FC<{

const LinksCell: React.FC<{
links: Record<string, string>;
}> = ({ links }) => (
}> = (props) => (
<TableCell onClick={(e) => e.stopPropagation()} className="py-0">
<div className="flex items-center gap-1">
{Object.entries(links).length === 0 && (
<span className="text-sm text-muted-foreground">No links</span>
)}
{Object.entries(links).map(([label, url]) => (
<Link
key={label}
href={url}
target="_blank"
rel="noopener noreferrer"
className={buttonVariants({
variant: "secondary",
size: "xs",
className: "gap-1",
})}
>
<IconExternalLink className="h-4 w-4" />
{label}
</Link>
))}
</div>
<JobLinks {...props} />
</TableCell>
);

Expand Down Expand Up @@ -211,7 +196,11 @@ export const ReleaseTargetRow: React.FC<{
</TableCell>
{latestJob != null && (
<>
<JobStatusCell status={latestJob.status} />
<JobStatusCell
status={latestJob.status}
releaseTargetId={id}
versionId={version.id}
/>
<ExternalIdCell externalId={latestJob.externalId} />
<LinksCell links={latestJob.links} />
<CreatedAtCell createdAt={latestJob.createdAt} />
Expand All @@ -221,15 +210,12 @@ export const ReleaseTargetRow: React.FC<{
{latestJob == null && (
<>
<TableCell>
<PolicyEvaluationHover
<PolicyEvaluationTooltip
releaseTargetId={id}
versionId={version.id}
>
<div className="flex w-fit cursor-pointer items-center gap-1">
<IconShieldFilled className="h-3 w-3 text-muted-foreground" />
<span className="text-sm">Blocked by policy</span>
</div>
</PolicyEvaluationHover>
<span>No jobs</span>
</PolicyEvaluationTooltip>
</TableCell>
<TableCell />
<TableCell />
Expand Down Expand Up @@ -258,7 +244,11 @@ export const ReleaseTargetRow: React.FC<{
<div className="h-10 border-l border-neutral-700/50" />
</div>
</TableCell>
<JobStatusCell status={job.status} />
<JobStatusCell
status={job.status}
releaseTargetId={id}
versionId={version.id}
/>
<ExternalIdCell externalId={job.externalId} />
<LinksCell links={job.links} />
<CreatedAtCell createdAt={job.createdAt} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Link from "next/link";
import { IconExternalLink } from "@tabler/icons-react";

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

export const JobLinks: React.FC<{
links: Record<string, string>;
}> = ({ links }) => (
<div className="flex items-center gap-1">
{Object.entries(links).length === 0 && (
<span className="text-sm text-muted-foreground">No links</span>
)}
{Object.entries(links).map(([label, url]) => (
<Link
key={label}
href={url}
target="_blank"
rel="noopener noreferrer"
className={buttonVariants({
variant: "secondary",
size: "xs",
className: "gap-1",
})}
>
<IconExternalLink className="h-4 w-4" />
{label}
</Link>
))}
</div>
);
Loading
Loading