From adb0fd94437fad2ef1a7241205192855396d1bae Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 3 May 2025 10:57:18 -0700 Subject: [PATCH 1/2] fix: readd dropdown menu to cells and add more options --- .../flow-diagram/ApprovalCheck.tsx | 64 ---- .../_components/flow-diagram/FlowNode.tsx | 65 ---- .../flow-diagram/FlowPolicyNode.tsx | 189 ------------ .../flow-diagram/checks/Approval.tsx | 92 +----- .../release-table/EnvironmentApprovalRow.tsx | 7 +- .../release-table/ResourceReleaseTable.tsx | 1 + .../(sidebar)/DeploymentPageContent.tsx | 1 + .../release-cell/ActiveJobsCell.tsx | 92 ++++++ .../release-cell/ApprovalRequiredCell.tsx | 167 +++++++++++ .../BlockedByVersionSelectorCell.tsx | 121 ++++++++ .../DeploymentVersionEnvironmentCell.tsx | 280 ++---------------- .../release-cell/policy-evaluation.ts | 9 + .../deployments/TableDeployments.tsx | 2 +- .../TableDeployments.tsx | 2 +- .../deployment-version/ApprovalDialog.tsx | 130 ++++---- .../DeploymentVersionDropdownMenu.tsx | 44 +-- .../DeploymentEnvironmentCell.tsx | 7 +- 17 files changed, 497 insertions(+), 776 deletions(-) delete mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/ApprovalCheck.tsx delete mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowNode.tsx delete mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowPolicyNode.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ActiveJobsCell.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/BlockedByVersionSelectorCell.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/policy-evaluation.ts diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/ApprovalCheck.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/ApprovalCheck.tsx deleted file mode 100644 index d04675eb5..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/ApprovalCheck.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Button } from "@ctrlplane/ui/button"; - -import { ApprovalDialog } from "~/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ApprovalDialog"; -import { api } from "~/trpc/react"; -import { Cancelled, Failing, Loading, Passing, Waiting } from "./StatusIcons"; - -export const ApprovalCheck: React.FC<{ - policyId: string; - deploymentVersion: { id: string; tag: string; deploymentId: string }; -}> = ({ policyId, deploymentVersion }) => { - const approvalStatus = - api.environment.policy.approval.statusByVersionPolicyId.useQuery({ - policyId, - versionId: deploymentVersion.id, - }); - - if (approvalStatus.isLoading) - return ( -
- Loading approval status -
- ); - - if (approvalStatus.data == null) - return ( -
- Approval skipped -
- ); - - const status = approvalStatus.data.status; - return ( -
-
- {status === "approved" && ( - <> - Approved - - )} - {status === "rejected" && ( - <> - Rejected - - )} - {status === "pending" && ( - <> - Pending approval - - )} -
- - {status === "pending" && ( - - - - )} -
- ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowNode.tsx deleted file mode 100644 index ac262263f..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowNode.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type * as SCHEMA from "@ctrlplane/db/schema"; -import type { NodeProps } from "reactflow"; -import React from "react"; -import { Handle, Position } from "reactflow"; -import colors from "tailwindcss/colors"; - -import { cn } from "@ctrlplane/ui"; -import { Badge } from "@ctrlplane/ui/badge"; -import { JobStatus } from "@ctrlplane/validators/jobs"; - -import { api } from "~/trpc/react"; - -type EnvironmentNodeProps = NodeProps< - SCHEMA.Environment & { - label: string; - deploymentVersion: SCHEMA.DeploymentVersion; - } ->; - -export const EnvironmentNode: React.FC = (node) => { - const { data } = node; - const releaseJobTriggers = api.job.config.byDeploymentVersionId.useQuery( - { versionId: data.deploymentVersion.id }, - { refetchInterval: 10_000 }, - ); - const environmentJobs = releaseJobTriggers.data?.filter( - (job) => job.environmentId === data.id, - ); - const successful = environmentJobs?.filter( - (job) => job.job.status === JobStatus.Successful, - ); - return ( - <> -
- {data.label} - - {releaseJobTriggers.data != null && ( - - {successful?.length} / {environmentJobs?.length} - - )} -
- - - - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowPolicyNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowPolicyNode.tsx deleted file mode 100644 index 22957ff6e..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowPolicyNode.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import type * as SCHEMA from "@ctrlplane/db/schema"; -import type { NodeProps } from "reactflow"; -import { useEffect, useState } from "react"; -import { differenceInMilliseconds } from "date-fns"; -import prettyMilliseconds from "pretty-ms"; -import { Handle, Position } from "reactflow"; -import colors from "tailwindcss/colors"; - -import { cn } from "@ctrlplane/ui"; -import { JobStatus } from "@ctrlplane/validators/jobs"; - -import { api } from "~/trpc/react"; -import { ApprovalCheck } from "./ApprovalCheck"; -import { Cancelled, Loading, Passing, Waiting } from "./StatusIcons"; - -type PolicyNodeProps = NodeProps< - SCHEMA.EnvironmentPolicy & { - deploymentVersion: SCHEMA.DeploymentVersion; - policyDeployments: Array; - } ->; - -const MinSuccessCheck: React.FC = ({ - successMinimum, - successType, - deploymentVersion, - policyDeployments, -}) => { - const allJobs = api.job.config.byDeploymentVersionId.useQuery( - { versionId: deploymentVersion.id }, - { refetchInterval: 10_000 }, - ); - const envIds = policyDeployments.map((p) => p.environmentId); - const jobs = allJobs.data?.filter((j) => envIds.includes(j.environmentId)); - - if (successType === "optional") return null; - - if (successType === "some") { - const passing = - jobs?.filter((job) => job.job.status === JobStatus.Successful).length ?? - 0; - - const isMinSatified = passing >= successMinimum; - return ( -
- {isMinSatified ? : } ≥ {successMinimum}{" "} - completed sucessfully -
- ); - } - - const areAllSuccessful = - jobs?.every((job) => job.job.status === JobStatus.Successful) ?? true; - - return ( -
- {areAllSuccessful ? ( - <> - All jobs successful - - ) : ( - <> - Waiting for all jobs to complete - - )} -
- ); -}; - -const GradualRolloutCheck: React.FC = (data) => { - const [timeLeft, setTimeLeft] = useState(null); - - const { data: approvalStatus, isLoading } = - api.environment.policy.approval.statusByVersionPolicyId.useQuery( - { policyId: data.id, versionId: data.deploymentVersion.id }, - { enabled: data.approvalRequirement === "manual" }, - ); - - const startDate = - data.approvalRequirement === "manual" - ? (approvalStatus?.approvedAt ?? data.deploymentVersion.createdAt) - : data.deploymentVersion.createdAt; - - useEffect(() => { - const calculateTimeLeft = () => { - const timePassed = differenceInMilliseconds(new Date(), startDate); - return Math.max(0, data.rolloutDuration - timePassed); - }; - - setTimeLeft(calculateTimeLeft()); - - const interval = setInterval(() => { - const remaining = calculateTimeLeft(); - setTimeLeft(remaining); - - if (remaining <= 0) clearInterval(interval); - }, 1000); - - return () => clearInterval(interval); - }, [startDate, data.rolloutDuration]); - - if (timeLeft == null) return null; - - if (isLoading) - return ( -
- Loading rollout status -
- ); - - const isApprovalPending = - approvalStatus == null || approvalStatus.status === "pending"; - - if (data.approvalRequirement === "manual" && isApprovalPending) - return ( -
- Rollout pending approval -
- ); - - if ( - data.approvalRequirement === "manual" && - approvalStatus?.status === "rejected" - ) - return ( -
- Rollout skipped due to approval rejection -
- ); - - return ( -
- {timeLeft <= 0 ? : }{" "} - {timeLeft <= 0 ? ( - "Rollout completed" - ) : ( - <> - Rollout completes in{" "} - {prettyMilliseconds(timeLeft, { - compact: true, - keepDecimalsOnWholeSeconds: false, - })} - - )} -
- ); -}; - -export const PolicyNode: React.FC = ({ data }) => { - const noMinSuccess = data.successType === "optional"; - const noRollout = data.rolloutDuration === 0; - const noApproval = data.approvalRequirement === "automatic"; - - return ( - <> -
- {!noMinSuccess && } - {!noRollout && } - {!noApproval && ( - - )} - - {noMinSuccess && noRollout && noApproval && ( -
No policy checks.
- )} -
- - - - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/checks/Approval.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/checks/Approval.tsx index 67c13e27e..edb1dad8e 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/checks/Approval.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/checks/Approval.tsx @@ -1,16 +1,4 @@ -import { useState } from "react"; - -import * as SCHEMA from "@ctrlplane/db/schema"; import { Button } from "@ctrlplane/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTrigger, -} from "@ctrlplane/ui/dialog"; -import { Textarea } from "@ctrlplane/ui/textarea"; import { Tooltip, TooltipContent, @@ -18,76 +6,10 @@ import { TooltipTrigger, } from "@ctrlplane/ui/tooltip"; +import { ApprovalDialog } from "~/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ApprovalDialog"; import { api } from "~/trpc/react"; import { Loading, Passing, Waiting } from "../StatusIcons"; -const ApprovalDialog: React.FC<{ - versionId: string; - versionTag: string; - environmentId: string; - onSubmit: () => void; -}> = ({ versionId, versionTag, environmentId, onSubmit }) => { - const [open, setOpen] = useState(false); - const addRecord = - api.deployment.version.checks.approval.addRecord.useMutation(); - - const [reason, setReason] = useState(""); - - const handleSubmit = (status: SCHEMA.ApprovalStatus) => - addRecord - .mutateAsync({ - deploymentVersionId: versionId, - environmentId, - status, - reason, - }) - .then(() => setOpen(false)) - .then(() => onSubmit()); - - return ( - - - - - - Approve Release - - Are you sure you want to approve version {versionTag}? - - -