diff --git a/apps/event-worker/src/workers/compute-systems-release-targets.ts b/apps/event-worker/src/workers/compute-systems-release-targets.ts index 7981a446b..8a8974c70 100644 --- a/apps/event-worker/src/workers/compute-systems-release-targets.ts +++ b/apps/event-worker/src/workers/compute-systems-release-targets.ts @@ -11,9 +11,7 @@ import { } from "@ctrlplane/events"; import { logger } from "@ctrlplane/logger"; -const log = logger.child({ - component: "computeSystemsReleaseTargetsWorker", -}); +const log = logger.child({ component: "computeSystemsReleaseTargetsWorker" }); const findMatchingEnvironmentDeploymentPairs = ( tx: Tx, diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowDiagram.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowDiagram.tsx index 7e5e4152f..5b58f3b8d 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowDiagram.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/FlowDiagram.tsx @@ -22,9 +22,10 @@ const markerEnd = { export const FlowDiagram: React.FC<{ workspace: SCHEMA.Workspace; + system: { id: string }; deploymentVersion: SCHEMA.DeploymentVersion; envs: Array; -}> = ({ workspace, deploymentVersion, envs }) => { +}> = ({ workspace, system, deploymentVersion, envs }) => { const [nodes, _, onNodesChange] = useNodesState<{ label: string }>([ { id: "trigger", @@ -44,6 +45,7 @@ export const FlowDiagram: React.FC<{ deploymentId: deploymentVersion.deploymentId, environmentId: env.id, environmentName: env.name, + systemId: system.id, label: env.name, }, }; 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 781e5e0fb..25cdee7a8 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 @@ -6,6 +6,7 @@ import { Loading, Passing, Waiting } from "../StatusIcons"; export const ApprovalCheck: React.FC<{ workspaceId: string; + systemId: string; environmentId: string; versionId: string; versionTag: string; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/nodes/EnvironmentNode.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/nodes/EnvironmentNode.tsx index 93b494ffb..f5913a74d 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/nodes/EnvironmentNode.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/_components/flow-diagram/nodes/EnvironmentNode.tsx @@ -18,6 +18,7 @@ type EnvironmentNodeProps = NodeProps<{ deploymentId: string; environmentId: string; environmentName: string; + systemId: string; }>; export const EnvironmentNode: React.FC = ({ data }) => ( diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/page.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/page.tsx index 5774a0ac7..6d9c8683a 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/page.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/checks/page.tsx @@ -65,6 +65,7 @@ export default async function ChecksPage(props: PageProps) { diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/DeploymentPageContent.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/DeploymentPageContent.tsx index 4faa6e692..4ce5b3497 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/DeploymentPageContent.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/DeploymentPageContent.tsx @@ -327,7 +327,7 @@ export const DeploymentPageContent: React.FC = ({ environment={env} deployment={deployment} deploymentVersion={version} - system={{ slug: systemSlug }} + system={{ id: deployment.systemId, slug: systemSlug }} /> ))} diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx index 3af379883..7789890cd 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx @@ -74,7 +74,7 @@ export const ApprovalRequiredCell: React.FC<{ deploymentVersion: { id: string; tag: string }; deployment: { id: string; name: string; slug: string }; environment: { id: string; name: string }; - system: { slug: string }; + system: { id: string; slug: string }; }> = ({ policies, deploymentVersion, deployment, environment, system }) => { const { workspaceSlug } = useParams<{ workspaceSlug: string }>(); @@ -143,6 +143,7 @@ export const ApprovalRequiredCell: React.FC<{ ); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ApprovalDialog.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ApprovalDialog.tsx index 1f91d28d5..d659dcec0 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ApprovalDialog.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/_components/deployment-version/ApprovalDialog.tsx @@ -1,87 +1,238 @@ "use client"; -import { useState } from "react"; +import type * as schema from "@ctrlplane/db/schema"; +import React, { useState } from "react"; import { useRouter } from "next/navigation"; +import { IconLoader2, IconSelector, IconX } from "@tabler/icons-react"; import * as SCHEMA from "@ctrlplane/db/schema"; +import { Badge } from "@ctrlplane/ui/badge"; import { Button } from "@ctrlplane/ui/button"; +import { + Command, + CommandInput, + CommandItem, + CommandList, +} from "@ctrlplane/ui/command"; import { Dialog, DialogContent, - DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@ctrlplane/ui/dialog"; +import { Label } from "@ctrlplane/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover"; import { Textarea } from "@ctrlplane/ui/textarea"; import { api } from "~/trpc/react"; -export const ApprovalDialog: React.FC<{ +const EnvironmentCombobox: React.FC<{ + allEnvironments: schema.Environment[]; + selectedEnvironmentIds: string[]; + onSelect: (environmentId: string[]) => void; + onRemove: (environmentId: string) => void; +}> = ({ allEnvironments, selectedEnvironmentIds, onSelect, onRemove }) => { + const unselectedEnvironments = allEnvironments.filter( + (environment) => !selectedEnvironmentIds.includes(environment.id), + ); + + const selectedEnvironments = allEnvironments.filter((environment) => + selectedEnvironmentIds.includes(environment.id), + ); + + return ( +
+
+ +

+ Select the environments to approve the release for. +

+
+ +
+ {selectedEnvironments.map((environment) => ( + + {environment.name} + + + ))} +
+ + + + + + + + + + onSelect(allEnvironments.map((e) => e.id))} + > + All environments + + {unselectedEnvironments.map((environment) => ( + onSelect([environment.id])} + > + {environment.name} + + ))} + + + + +
+ ); +}; + +const ApprovalDialogControl: React.FC<{ versionId: string; - versionTag: string; + environments: schema.Environment[]; environmentId: string; - children: React.ReactNode; - onSubmit?: () => void; -}> = ({ versionId, versionTag, environmentId, children, onSubmit }) => { - const [open, setOpen] = useState(false); - const addRecord = api.deployment.version.addApprovalRecord.useMutation(); + onSubmit: () => void; + onCancel: () => void; +}> = ({ versionId, environments, environmentId, onSubmit, onCancel }) => { + const [environmentIds, setEnvironmentIds] = useState([ + environmentId, + ]); const router = useRouter(); - const [reason, setReason] = useState(""); + const addRecord = api.deployment.version.addApprovalRecord.useMutation(); const handleSubmit = (status: SCHEMA.ApprovalStatus) => addRecord .mutateAsync({ deploymentVersionId: versionId, - environmentId, + environmentIds, status, reason, }) - .then(() => setOpen(false)) - .then(() => onSubmit?.()) + .then(() => onSubmit()) .then(() => router.refresh()); + const setEnvironmentSelected = (environmentIds: string[]) => + setEnvironmentIds((prev) => [...prev, ...environmentIds]); + + const setEnvironmentUnselected = (environmentId: string) => + setEnvironmentIds((prev) => prev.filter((id) => id !== environmentId)); + + return ( +
+ + +
+
+ +

+ Provide a reason for the approval or rejection (optional). +

+
+