From 22eaf743d6c019aaa91c5416cfd459583380b868 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Sat, 3 May 2025 18:49:06 -0700 Subject: [PATCH 1/2] chore: clean up edit policy ui --- .../DeploymentVersionConditionRender.tsx | 3 +- .../DeploymentVersionTagConditionRender.tsx | 3 +- .../[policyId]/_components/PolicyTabs.tsx | 30 +- .../edit/_components/PolicyFormContext.tsx | 103 +++ .../edit/configuration/EditConfiguration.tsx | 730 ++++++++---------- .../[policyId]/edit/configuration/page.tsx | 14 +- .../deployment-flow/EditDeploymentFlow.tsx | 151 ++-- .../(app)/policies/[policyId]/edit/layout.tsx | 50 +- .../quality-security/EditQualitySecurity.tsx | 284 +++---- .../[policyId]/edit/quality-security/page.tsx | 13 +- .../edit/time-windows/EditTimeWindow.tsx | 366 +++++---- .../(app)/policies/[policyId]/layout.tsx | 2 +- apps/webservice/src/app/urls.ts | 1 + 13 files changed, 810 insertions(+), 940 deletions(-) create mode 100644 apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/deployments/version/condition/DeploymentVersionConditionRender.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/deployments/version/condition/DeploymentVersionConditionRender.tsx index fa6cfb53f..fb3c715f8 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/deployments/version/condition/DeploymentVersionConditionRender.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/deployments/version/condition/DeploymentVersionConditionRender.tsx @@ -5,6 +5,7 @@ import { isComparisonCondition, isCreatedAtCondition, isMetadataCondition, + isTagCondition, isVersionCondition, } from "@ctrlplane/validators/releases"; @@ -54,7 +55,7 @@ export const DeploymentVersionConditionRender: React.FC< /> ); - if (isVersionCondition(condition)) + if (isVersionCondition(condition) || isTagCondition(condition)) return ( + DeploymentVersionConditionRenderProps > = ({ condition, onChange, className }) => { const setOperator = (operator: ColumnOperatorType) => onChange({ ...condition, operator }); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/_components/PolicyTabs.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/_components/PolicyTabs.tsx index 498890565..ada9c7e56 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/_components/PolicyTabs.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/_components/PolicyTabs.tsx @@ -19,26 +19,35 @@ export const PolicyTabs: React.FC = () => { const editUrls = policyUrls.edit(policyId); const baseUrl = policyUrls.byId(policyId); - const editBaseUrl = editUrls.baseUrl(); + + const configurationUrl = editUrls.configuration(); + const timeWindowsUrl = editUrls.timeWindows(); + const deploymentFlowUrl = editUrls.deploymentFlow(); + const qualitySecurityUrl = editUrls.qualitySecurity(); const pathname = usePathname(); const getInitialTab = () => { if (pathname === baseUrl) return "overview"; - if (pathname.startsWith(editBaseUrl)) return "edit"; + if (pathname === configurationUrl) return "configuration"; + if (pathname === timeWindowsUrl) return "time-windows"; + if (pathname === deploymentFlowUrl) return "deployment-flow"; + if (pathname === qualitySecurityUrl) return "quality-security"; return "overview"; }; + console.log(pathname, getInitialTab()); + const [activeTab, setActiveTab] = useState(getInitialTab()); const router = useRouter(); const onTabChange = (value: string) => { if (value === "overview") router.push(baseUrl); - if (value === "edit") router.push(editBaseUrl); - if (value === "time-windows") router.push(editUrls.timeWindows()); - if (value === "version-conditions") router.push(editUrls.qualitySecurity()); - if (value === "approval-gates") router.push(policyUrls.approvalGates()); + if (value === "configuration") router.push(configurationUrl); + if (value === "time-windows") router.push(timeWindowsUrl); + if (value === "deployment-flow") router.push(deploymentFlowUrl); + if (value === "quality-security") router.push(qualitySecurityUrl); setActiveTab(value); }; @@ -46,19 +55,22 @@ export const PolicyTabs: React.FC = () => { Overview - Edit + Edit Time Windows Version Conditions - + Approval Gates diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx new file mode 100644 index 000000000..7f2875495 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/_components/PolicyFormContext.tsx @@ -0,0 +1,103 @@ +"use client"; + +import type { UpdatePolicy } from "@ctrlplane/db/schema"; +import type React from "react"; +import type { UseFormReturn } from "react-hook-form"; +import { createContext, useContext } from "react"; +import { useRouter } from "next/navigation"; + +import * as SCHEMA from "@ctrlplane/db/schema"; +import { Form, useForm } from "@ctrlplane/ui/form"; +import { toast } from "@ctrlplane/ui/toast"; + +import { api } from "~/trpc/react"; +import { + convertEmptySelectorsToNull, + isValidTarget, +} from "../../../_utils/policy-targets"; + +type Policy = SCHEMA.Policy & { + targets: SCHEMA.PolicyTarget[]; + denyWindows: SCHEMA.PolicyRuleDenyWindow[]; + deploymentVersionSelector: SCHEMA.PolicyDeploymentVersionSelector | null; + versionAnyApprovals: SCHEMA.PolicyRuleAnyApproval | null; + versionUserApprovals: SCHEMA.PolicyRuleUserApproval[]; + versionRoleApprovals: SCHEMA.PolicyRuleRoleApproval[]; +}; + +type PolicyFormContextType = { + form: UseFormReturn; + policy: Policy; +}; + +const PolicyFormContext = createContext(null); + +export function usePolicyFormContext() { + const ctx = useContext(PolicyFormContext); + if (ctx == null) + throw new Error( + "usePolicyFormContext must be used within a PolicyFormContext.Provider", + ); + return ctx; +} + +export const PolicyFormContextProvider: React.FC<{ + children: React.ReactNode; + policy: Policy; +}> = ({ children, policy }) => { + console.log(policy); + + const form = useForm({ + schema: SCHEMA.updatePolicy, + defaultValues: policy, + }); + + console.log(form.getValues()); + + const router = useRouter(); + const utils = api.useUtils(); + + const updatePolicy = api.policy.update.useMutation(); + + const onSubmit = form.handleSubmit((data) => { + const targets = + data.targets === undefined + ? undefined + : data.targets.map(convertEmptySelectorsToNull); + + const isTargetsValid = + targets === undefined || targets.every(isValidTarget); + if (!isTargetsValid) { + const errorStr = "One or more of your targets are invalid"; + form.setError("targets", { message: errorStr }); + toast.error("Error creating policy", { description: errorStr }); + return; + } + + return updatePolicy + .mutateAsync({ + id: policy.id, + data: { ...data, targets }, + }) + .then(() => { + toast.success("Policy updated successfully"); + form.reset(data); + router.refresh(); + utils.policy.byId.invalidate(); + utils.policy.list.invalidate(); + }) + .catch((error) => { + toast.error("Failed to update policy", { + description: error.message, + }); + }); + }); + + return ( + +
+ {children}
+ +
+ ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/configuration/EditConfiguration.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/configuration/EditConfiguration.tsx index 322532911..8a48b3b45 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/configuration/EditConfiguration.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/policies/[policyId]/edit/configuration/EditConfiguration.tsx @@ -1,12 +1,6 @@ "use client"; -import type * as SCHEMA from "@ctrlplane/db/schema"; -import type { DeploymentCondition } from "@ctrlplane/validators/deployments"; -import type { EnvironmentCondition } from "@ctrlplane/validators/environments"; -import type { ResourceCondition } from "@ctrlplane/validators/resources"; -import { useRouter } from "next/navigation"; import { IconPlus, IconTrash } from "@tabler/icons-react"; -import { z } from "zod"; import { cn } from "@ctrlplane/ui"; import { Button } from "@ctrlplane/ui/button"; @@ -17,7 +11,6 @@ import { DropdownMenuTrigger, } from "@ctrlplane/ui/dropdown-menu"; import { - Form, FormControl, FormDescription, FormField, @@ -25,40 +18,16 @@ import { FormLabel, FormMessage, useFieldArray, - useForm, } from "@ctrlplane/ui/form"; import { Input } from "@ctrlplane/ui/input"; import { Label } from "@ctrlplane/ui/label"; import { Switch } from "@ctrlplane/ui/switch"; import { Textarea } from "@ctrlplane/ui/textarea"; -import { toast } from "@ctrlplane/ui/toast"; -import { deploymentCondition } from "@ctrlplane/validators/deployments"; -import { environmentCondition } from "@ctrlplane/validators/environments"; -import { resourceCondition } from "@ctrlplane/validators/resources"; import { DeploymentConditionRender } from "~/app/[workspaceSlug]/(app)/_components/deployments/condition/DeploymentConditionRender"; import { EnvironmentConditionRender } from "~/app/[workspaceSlug]/(app)/_components/environment/condition/EnvironmentConditionRender"; import { ResourceConditionRender } from "~/app/[workspaceSlug]/(app)/_components/resources/condition/ResourceConditionRender"; -import { - convertEmptySelectorsToNull, - convertNullSelectorsToEmptyConditions, - isValidTarget, -} from "~/app/[workspaceSlug]/(app)/policies/_utils/policy-targets"; -import { api } from "~/trpc/react"; - -const editConfigSchema = z.object({ - name: z.string(), - description: z.string().nullable(), - priority: z.number(), - enabled: z.boolean(), - targets: z.array( - z.object({ - deploymentSelector: deploymentCondition.nullable(), - environmentSelector: environmentCondition.nullable(), - resourceSelector: resourceCondition.nullable(), - }), - ), -}); +import { usePolicyFormContext } from "../_components/PolicyFormContext"; // Available options for environments and deployments const ENVIRONMENTS = ["production", "staging", "development"] as const; @@ -100,234 +69,181 @@ const TARGET_SCOPE_OPTIONS = [ }, ]; -export const EditConfiguration: React.FC<{ - policy: SCHEMA.Policy & { - targets: Array<{ - deploymentSelector: DeploymentCondition | null; - environmentSelector: EnvironmentCondition | null; - resourceSelector: ResourceCondition | null; - }>; - }; -}> = ({ policy }) => { - const form = useForm({ - schema: editConfigSchema, - defaultValues: { - name: policy.name, - description: policy.description, - priority: policy.priority, - enabled: policy.enabled, - targets: policy.targets.map(convertNullSelectorsToEmptyConditions), - }, - }); - - const updatePolicy = api.policy.update.useMutation(); - const router = useRouter(); +export const EditConfiguration: React.FC = () => { + const { form } = usePolicyFormContext(); - const { id } = policy; - const onSubmit = form.handleSubmit((data) => { - const targets = data.targets.map(convertEmptySelectorsToNull); - const isTargetsValid = targets.every(isValidTarget); - if (!isTargetsValid) { - const errorStr = "One or more of your targets are invalid"; - form.setError("targets", { message: errorStr }); - toast.error("Error updating policy", { description: errorStr }); - return; - } + console.log(form.getValues()); - updatePolicy - .mutateAsync({ id, data: { ...data, targets } }) - .then((res) => form.reset(res)) - .then(() => router.refresh()); - }); + const isTargetsError = form.formState.errors.targets != null; const { fields, append, remove, update } = useFieldArray({ control: form.control, name: "targets", }); - const isTargetsError = form.formState.errors.targets != null; - return ( -
- -
-

Basic Policy Configuration

+
+
+

Basic Policy Configuration

+

+ Configure the basic settings for your policy +

+
+ +
+
+

General Settings

- Configure the basic settings for your policy + Configure the basic policy information

-
-
-

General Settings

-

- Configure the basic policy information -

-
+
+ ( + + Policy Name + + + + + A unique name to identify this policy + + + + )} + /> -
- ( - - Policy Name - - - + ( + + Description + +