From f6b48a6b32c30c63f38856fc7bbd8dc43dd73550 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Tue, 8 Oct 2024 17:22:22 -0700 Subject: [PATCH 1/3] fix: Clean up policy drawer --- .../environment-policy-drawer/Approval.tsx | 80 ----- ...Criteria.tsx => ApprovalAndGovernance.tsx} | 73 ++++- .../environment-policy-drawer/Concurrency.tsx | 107 ------- .../DeploymentControl.tsx | 285 ++++++++++++++++++ .../EnvironmentPolicyDrawer.tsx | 117 +++---- .../GradualRollouts.tsx | 94 ------ .../environment-policy-drawer/Overview.tsx | 75 +---- .../PolicyDeleteDialog.tsx | 57 ++++ .../ReleaseFilter.tsx | 220 -------------- ...seSequencing.tsx => ReleaseManagement.tsx} | 15 +- .../ReleaseWindows.tsx | 203 ------------- .../RolloutAndTiming.tsx | 284 +++++++++++++++++ .../ComparisonConditionRender.tsx | 144 ++++----- 13 files changed, 844 insertions(+), 910 deletions(-) delete mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Approval.tsx rename apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/{SuccessCriteria.tsx => ApprovalAndGovernance.tsx} (62%) delete mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Concurrency.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/DeploymentControl.tsx delete mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/GradualRollouts.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/PolicyDeleteDialog.tsx delete mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseFilter.tsx rename apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/{ReleaseSequencing.tsx => ReleaseManagement.tsx} (79%) delete mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseWindows.tsx create mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Approval.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Approval.tsx deleted file mode 100644 index 512f91676..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Approval.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type * as SCHEMA from "@ctrlplane/db/schema"; -import React from "react"; -import { z } from "zod"; - -import { Button } from "@ctrlplane/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - useForm, -} from "@ctrlplane/ui/form"; -import { Switch } from "@ctrlplane/ui/switch"; - -import { api } from "~/trpc/react"; - -const schema = z.object({ - approvalRequirement: z.enum(["automatic", "manual"]), -}); - -export const Approval: React.FC<{ - environmentPolicy: SCHEMA.EnvironmentPolicy; -}> = ({ environmentPolicy }) => { - const form = useForm({ schema, defaultValues: { ...environmentPolicy } }); - - const updatePolicy = api.environment.policy.update.useMutation(); - const utils = api.useUtils(); - - const { id, systemId } = environmentPolicy; - const onSubmit = form.handleSubmit((data) => - updatePolicy - .mutateAsync({ id, data }) - .then(() => form.reset(data)) - .then(() => utils.environment.policy.byId.invalidate(id)) - .then(() => utils.environment.policy.bySystemId.invalidate(systemId)), - ); - - return ( -
- - ( - -
- Approval gates - - If enabled, a release will require approval from an authorized - user before it can be deployed to any environment with this - policy. - -
- -
- Enabled:{" "} - - onChange(checked ? "manual" : "automatic") - } - /> -
-
-
- )} - /> - - - - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/SuccessCriteria.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ApprovalAndGovernance.tsx similarity index 62% rename from apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/SuccessCriteria.tsx rename to apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ApprovalAndGovernance.tsx index 7207c8353..aee552423 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/SuccessCriteria.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ApprovalAndGovernance.tsx @@ -1,4 +1,5 @@ -import type * as schema from "@ctrlplane/db/schema"; +import type * as SCHEMA from "@ctrlplane/db/schema"; +import React from "react"; import { z } from "zod"; import { Button } from "@ctrlplane/ui/button"; @@ -13,24 +14,27 @@ import { } from "@ctrlplane/ui/form"; import { Input } from "@ctrlplane/ui/input"; import { RadioGroup, RadioGroupItem } from "@ctrlplane/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ctrlplane/ui/select"; import { api } from "~/trpc/react"; -const successCriteriaForm = z.object({ +const schema = z.object({ + approvalRequirement: z.enum(["automatic", "manual"]), successType: z.enum(["all", "some", "optional"]), successMinimum: z.number().min(0, "Must be a positive number"), }); -export const SuccessCriteria: React.FC<{ - environmentPolicy: schema.EnvironmentPolicy; +export const ApprovalAndGovernance: React.FC<{ + environmentPolicy: SCHEMA.EnvironmentPolicy; }> = ({ environmentPolicy }) => { - const form = useForm({ - schema: successCriteriaForm, - defaultValues: { - successType: environmentPolicy.successType, - successMinimum: environmentPolicy.successMinimum, - }, - }); + const form = useForm({ schema, defaultValues: { ...environmentPolicy } }); + const { successMinimum } = form.watch(); const updatePolicy = api.environment.policy.update.useMutation(); const utils = api.useUtils(); @@ -44,16 +48,55 @@ export const SuccessCriteria: React.FC<{ .then(() => utils.environment.policy.bySystemId.invalidate(systemId)), ); - const { successMinimum } = form.watch(); - return (
- + +
+

Approval & Governance

+ + This category defines policies that govern the oversight and + approval process for deployments. These policies ensure that + deployments meet specific criteria or gain necessary approvals + before proceeding, contributing to compliance, quality assurance, + and overall governance of the deployment process. + +
+ + ( + +
+ Approval gates + + If enabled, a release will require approval from an authorized + user before it can be deployed to any environment with this + policy. + +
+ +
+ +
+
+
+ )} + /> + ( - +
Previous Deploy Status diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Concurrency.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Concurrency.tsx deleted file mode 100644 index 15008c1d2..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Concurrency.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import type * as SCHEMA from "@ctrlplane/db/schema"; -import React from "react"; -import { z } from "zod"; - -import { Button } from "@ctrlplane/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - useForm, -} from "@ctrlplane/ui/form"; -import { Input } from "@ctrlplane/ui/input"; -import { RadioGroup, RadioGroupItem } from "@ctrlplane/ui/radio-group"; - -import { api } from "~/trpc/react"; - -const schema = z.object({ - concurrencyType: z.enum(["all", "some"]), - concurrencyLimit: z.number().min(1, "Must be a positive number"), -}); - -export const Concurrency: React.FC<{ - environmentPolicy: SCHEMA.EnvironmentPolicy; -}> = ({ environmentPolicy }) => { - const form = useForm({ schema, defaultValues: { ...environmentPolicy } }); - - const updatePolicy = api.environment.policy.update.useMutation(); - const utils = api.useUtils(); - - const { id, systemId } = environmentPolicy; - const onSubmit = form.handleSubmit((data) => - updatePolicy - .mutateAsync({ id, data }) - .then(() => form.reset(data)) - .then(() => utils.environment.policy.byId.invalidate(id)) - .then(() => utils.environment.policy.bySystemId.invalidate(systemId)), - ); - - const { concurrencyLimit } = form.watch(); - - return ( - - - ( - -
-
- Concurrency - - The number of jobs that can run concurrently in an - environment. - -
- - - - - - - - All jobs can run concurrently - - - - - - - - A maximum of - - form.setValue( - "concurrencyLimit", - e.target.valueAsNumber, - ) - } - className="border-b-1 h-6 w-16 text-xs" - /> - jobs can run concurrently - - - - -
-
- )} - /> - - - - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/DeploymentControl.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/DeploymentControl.tsx new file mode 100644 index 000000000..69f5a205a --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/DeploymentControl.tsx @@ -0,0 +1,285 @@ +import type * as SCHEMA from "@ctrlplane/db/schema"; +import type { VersionCheck } from "@ctrlplane/validators/environment-policies"; +import React from "react"; +import _ from "lodash"; +import { validRange } from "semver"; +import { z } from "zod"; + +import { Button } from "@ctrlplane/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + useForm, +} from "@ctrlplane/ui/form"; +import { Input } from "@ctrlplane/ui/input"; +import { RadioGroup, RadioGroupItem } from "@ctrlplane/ui/radio-group"; +import { + isFilterCheck, + isNoneCheck, + isRegexCheck, + isSemverCheck, +} from "@ctrlplane/validators/environment-policies"; +import { + defaultCondition, + isValidReleaseCondition, + releaseCondition, +} from "@ctrlplane/validators/releases"; + +import { api } from "~/trpc/react"; +import { ReleaseConditionRender } from "../release-condition/ReleaseConditionRender"; + +const isValidRegex = (str: string) => { + try { + new RegExp(str); + return true; + } catch { + return false; + } +}; + +const filterSchema = z + .object({ + evaluateWith: z.literal("regex"), + evaluate: z.string().refine(isValidRegex, { + message: "Invalid regex pattern", + }), + }) + .or( + z.object({ + evaluateWith: z.literal("none"), + evaluate: z.null(), + }), + ) + .or( + z.object({ + evaluateWith: z.literal("semver"), + evaluate: z + .string() + .refine((s) => validRange(s) !== null, "Invalid semver range"), + }), + ) + .or( + z.object({ + evaluateWith: z.literal("filter"), + evaluate: releaseCondition.refine(isValidReleaseCondition, { + message: "Invalid release condition", + }), + }), + ); + +const schema = z + .object({ + concurrencyType: z.enum(["all", "some"]), + concurrencyLimit: z.number().min(1, "Must be a positive number"), + }) + .and(filterSchema); + +export const DeploymentControl: React.FC<{ + environmentPolicy: SCHEMA.EnvironmentPolicy; +}> = ({ environmentPolicy }) => { + const check: VersionCheck = { ...environmentPolicy }; + const defaultValues = _.merge( + {}, + environmentPolicy, + isFilterCheck(check) && { evaluate: check.evaluate }, + isNoneCheck(check) && { evaluate: check.evaluate }, + isSemverCheck(check) && { evaluate: check.evaluate }, + isRegexCheck(check) && { evaluate: check.evaluate }, + ); + const form = useForm({ schema, defaultValues }); + const { evaluateWith, evaluate } = form.watch(); + + const updatePolicy = api.environment.policy.update.useMutation(); + const utils = api.useUtils(); + + const { id, systemId } = environmentPolicy; + const onSubmit = form.handleSubmit((data) => + updatePolicy + .mutateAsync({ id, data }) + .then(() => form.reset(data)) + .then(() => utils.environment.policy.byId.invalidate(id)) + .then(() => utils.environment.policy.bySystemId.invalidate(systemId)), + ); + + const { concurrencyLimit } = form.watch(); + + return ( +
+ +
+

Deployment Control

+ + Deployment control policies focus on regulating how deployments are + executed within an environment. These policies manage concurrency, + filtering of releases, and other operational constraints, ensuring + efficient and orderly deployment processes without overwhelming + resources or violating environment-specific rules. + +
+ ( + +
+
+ Concurrency + + The number of jobs that can run concurrently in an + environment. + +
+ + + + + + + + All jobs can run concurrently + + + + + + + + A maximum of + + form.setValue( + "concurrencyLimit", + e.target.valueAsNumber, + ) + } + className="border-b-1 h-6 w-16 text-xs" + /> + jobs can run concurrently + + + + +
+
+ )} + /> + +
+ ( + +
+ Release Filter + + Filter which releases can be deployed to this environment. + +
+ + + + + + + None + + + + + + + + Regex + + + + + + + + Semver + + + + + + + + Filter + + + +
+ )} + /> + + {evaluateWith === "regex" && ( + ( + + Regex + + + )} + /> + )} + + {evaluateWith === "semver" && ( + ( + + Semver + + + )} + /> + )} + + {evaluateWith === "filter" && ( + ( + + Filter + + + + + {form.formState.isDirty && ( + + Save to apply + + )} + + )} + /> + )} +
+ + + + + ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx index 0c4512cc9..63af1e5aa 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx @@ -6,27 +6,32 @@ import { useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { IconCalendar, - IconCheck, - IconChecklist, IconCircuitDiode, - IconClock, + IconDotsVertical, + IconEye, IconFilter, IconInfoCircle, - IconPlayerPause, + IconRocket, + IconTrash, } from "@tabler/icons-react"; +import { Button } from "@ctrlplane/ui/button"; import { Drawer, DrawerContent, DrawerTitle } from "@ctrlplane/ui/drawer"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@ctrlplane/ui/dropdown-menu"; import { api } from "~/trpc/react"; import { TabButton } from "../TabButton"; -import { Approval } from "./Approval"; -import { Concurrency } from "./Concurrency"; -import { GradualRollouts } from "./GradualRollouts"; +import { ApprovalAndGovernance } from "./ApprovalAndGovernance"; +import { DeploymentControl } from "./DeploymentControl"; import { Overview } from "./Overview"; -import { ReleaseFilter } from "./ReleaseFilter"; -import { ReleaseSequencing } from "./ReleaseSequencing"; -import { ReleaseWindows } from "./ReleaseWindows"; -import { SuccessCriteria } from "./SuccessCriteria"; +import { DeleteEnvironmentPolicyDialog } from "./PolicyDeleteDialog"; +import { ReleaseManagement } from "./ReleaseManagement"; +import { RolloutAndTiming } from "./RolloutAndTiming"; const param = "environment_policy_id"; export const useEnvironmentPolicyDrawer = () => { @@ -61,22 +66,35 @@ const View: React.FC<{ }> = ({ activeTab, environmentPolicy }) => { return { overview: , - approval: , - concurrency: , - "gradual-rollout": ( - - ), - "success-criteria": ( - - ), - "release-sequencing": ( - - ), - "release-windows": , - "release-filter": , + approval: , + concurrency: , + management: , + rollout: , }[activeTab]; }; +const PolicyDropdownMenu: React.FC<{ + environmentPolicy: SCHEMA.EnvironmentPolicy; + children: React.ReactNode; +}> = ({ environmentPolicy, children }) => { + return ( + + {children} + + + e.preventDefault()} + > + + Delete + + + + + ); +}; + export const EnvironmentPolicyDrawer: React.FC = () => { const { environmentPolicyId, removeEnvironmentPolicyId } = useEnvironmentPolicyDrawer(); @@ -100,7 +118,18 @@ export const EnvironmentPolicyDrawer: React.FC = () => {
- {environmentPolicy?.name ?? "Policy"} + {(environmentPolicy == null || environmentPolicy.name === "") && + "Policy"} + {environmentPolicy != null && environmentPolicy.name !== "" && ( + {environmentPolicy.name} + )} + {environmentPolicy != null && ( + + + + )}
@@ -114,44 +143,26 @@ export const EnvironmentPolicyDrawer: React.FC = () => { setActiveTab("approval")} - icon={} - label="Approval" + icon={} + label="Approval & Governance" /> setActiveTab("concurrency")} icon={} - label="Concurrency" - /> - setActiveTab("gradual-rollout")} - icon={} - label="Gradual Rollout" + label="Deployment Control" /> setActiveTab("success-criteria")} - icon={} - label="Success Criteria" + active={activeTab === "management"} + onClick={() => setActiveTab("management")} + icon={} + label="Release Management" /> setActiveTab("release-sequencing")} - icon={} - label="Release Sequencing" - /> - setActiveTab("release-windows")} + active={activeTab === "rollout"} + onClick={() => setActiveTab("rollout")} icon={} - label="Release Windows" - /> - setActiveTab("release-filter")} - icon={} - label="Release Filter" + label="Rollout and Timing" />
diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/GradualRollouts.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/GradualRollouts.tsx deleted file mode 100644 index df5a8cbdb..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/GradualRollouts.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import type * as SCHEMA from "@ctrlplane/db/schema"; -import ms from "ms"; -import prettyMilliseconds from "pretty-ms"; -import { z } from "zod"; - -import { Button } from "@ctrlplane/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, - useForm, -} from "@ctrlplane/ui/form"; -import { Input } from "@ctrlplane/ui/input"; - -import { api } from "~/trpc/react"; - -const isValidDuration = (str: string) => !isNaN(ms(str)); - -const schema = z.object({ - duration: z.string().refine(isValidDuration, { - message: "Invalid duration pattern", - }), -}); - -export const GradualRollouts: React.FC<{ - environmentPolicy: SCHEMA.EnvironmentPolicy; -}> = ({ environmentPolicy }) => { - const form = useForm({ - schema, - defaultValues: { duration: prettyMilliseconds(environmentPolicy.duration) }, - }); - - const updatePolicy = api.environment.policy.update.useMutation(); - const utils = api.useUtils(); - - const { id, systemId } = environmentPolicy; - const onSubmit = form.handleSubmit((data) => - updatePolicy - .mutateAsync({ id, data: { duration: ms(data.duration) } }) - .then(() => form.reset(data)) - .then(() => utils.environment.policy.byId.invalidate(id)) - .then(() => utils.environment.policy.bySystemId.invalidate(systemId)), - ); - - return ( -
- - ( - -
- Gradual Rollout - - Enabling gradual rollouts will spread deployments out over a - given duration. A default duration of 0ms means that - deployments will be rolled out immediately. - -
- -
-
- - Spread deployments out over - - -
- -
-
-
- )} - /> - - - - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Overview.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Overview.tsx index d6c7ef09d..9f664b385 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Overview.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Overview.tsx @@ -2,18 +2,7 @@ import type * as SCHEMA from "@ctrlplane/db/schema"; import React from "react"; import { z } from "zod"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@ctrlplane/ui/alert-dialog"; -import { Button, buttonVariants } from "@ctrlplane/ui/button"; +import { Button } from "@ctrlplane/ui/button"; import { Form, FormControl, @@ -26,50 +15,6 @@ import { Input } from "@ctrlplane/ui/input"; import { Textarea } from "@ctrlplane/ui/textarea"; import { api } from "~/trpc/react"; -import { useEnvironmentPolicyDrawer } from "./EnvironmentPolicyDrawer"; - -const DeleteEnvironmentPolicyDialog: React.FC<{ - environmentPolicy: SCHEMA.EnvironmentPolicy; - children: React.ReactNode; -}> = ({ environmentPolicy, children }) => { - const deleteEnvironmentPolicy = api.environment.policy.delete.useMutation(); - const utils = api.useUtils(); - const { removeEnvironmentPolicyId } = useEnvironmentPolicyDrawer(); - - const onDelete = () => - deleteEnvironmentPolicy - .mutateAsync(environmentPolicy.id) - .then(removeEnvironmentPolicyId) - .then(() => - utils.environment.policy.bySystemId.invalidate( - environmentPolicy.systemId, - ), - ); - - return ( - - {children} - - - Delete Environment Policy - - - Are you sure you want to delete this environment policy? You will have - to recreate it from scratch. - - - Cancel - - Delete - - - - - ); -}; const schema = z.object({ name: z.string(), description: z.string() }); @@ -125,18 +70,12 @@ export const Overview: React.FC<{ )} /> -
- -
- - - -
+ ); diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/PolicyDeleteDialog.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/PolicyDeleteDialog.tsx new file mode 100644 index 000000000..f7d5c5730 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/PolicyDeleteDialog.tsx @@ -0,0 +1,57 @@ +import type * as SCHEMA from "@ctrlplane/db/schema"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@ctrlplane/ui/alert-dialog"; +import { buttonVariants } from "@ctrlplane/ui/button"; + +import { api } from "~/trpc/react"; +import { useEnvironmentPolicyDrawer } from "./EnvironmentPolicyDrawer"; + +export const DeleteEnvironmentPolicyDialog: React.FC<{ + environmentPolicy: SCHEMA.EnvironmentPolicy; + children: React.ReactNode; +}> = ({ environmentPolicy, children }) => { + const deleteEnvironmentPolicy = api.environment.policy.delete.useMutation(); + const utils = api.useUtils(); + const { removeEnvironmentPolicyId } = useEnvironmentPolicyDrawer(); + + const { id, systemId } = environmentPolicy; + const onDelete = () => + deleteEnvironmentPolicy + .mutateAsync(id) + .then(removeEnvironmentPolicyId) + .then(() => utils.environment.policy.bySystemId.invalidate(systemId)); + + return ( + + {children} + + + Delete Environment Policy + + + Are you sure you want to delete this environment policy? You will have + to recreate it from scratch. + + + Cancel + + Delete + + + + + ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseFilter.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseFilter.tsx deleted file mode 100644 index 780589c28..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseFilter.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import type * as SCHEMA from "@ctrlplane/db/schema"; -import type { VersionCheck } from "@ctrlplane/validators/environment-policies"; -import _ from "lodash"; -import { validRange } from "semver"; -import { z } from "zod"; - -import { Button } from "@ctrlplane/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - useForm, -} from "@ctrlplane/ui/form"; -import { Input } from "@ctrlplane/ui/input"; -import { RadioGroup, RadioGroupItem } from "@ctrlplane/ui/radio-group"; -import { - isFilterCheck, - isNoneCheck, - isRegexCheck, - isSemverCheck, -} from "@ctrlplane/validators/environment-policies"; -import { - defaultCondition, - isValidReleaseCondition, - releaseCondition, -} from "@ctrlplane/validators/releases"; - -import { api } from "~/trpc/react"; -import { ReleaseConditionRender } from "../release-condition/ReleaseConditionRender"; - -const isValidRegex = (str: string) => { - try { - new RegExp(str); - return true; - } catch { - return false; - } -}; - -const schema = z - .object({ - evaluateWith: z.literal("regex"), - evaluate: z.string().refine(isValidRegex, { - message: "Invalid regex pattern", - }), - }) - .or( - z.object({ - evaluateWith: z.literal("none"), - evaluate: z.null(), - }), - ) - .or( - z.object({ - evaluateWith: z.literal("semver"), - evaluate: z - .string() - .refine((s) => validRange(s) !== null, "Invalid semver range"), - }), - ) - .or( - z.object({ - evaluateWith: z.literal("filter"), - evaluate: releaseCondition.refine(isValidReleaseCondition, { - message: "Invalid release condition", - }), - }), - ); - -export const ReleaseFilter: React.FC<{ - environmentPolicy: SCHEMA.EnvironmentPolicy; -}> = ({ environmentPolicy }) => { - const check: VersionCheck = { ...environmentPolicy }; - // schema complains about type mismatching unless we add type guards - const defaultValues = _.merge( - {}, - environmentPolicy, - isFilterCheck(check) && { evaluate: check.evaluate }, - isNoneCheck(check) && { evaluate: check.evaluate }, - isSemverCheck(check) && { evaluate: check.evaluate }, - isRegexCheck(check) && { evaluate: check.evaluate }, - ); - - const form = useForm({ schema, defaultValues }); - const { evaluateWith, evaluate } = form.watch(); - - const policyUpdate = api.environment.policy.update.useMutation(); - const utils = api.useUtils(); - - const { id, systemId } = environmentPolicy; - const onSubmit = form.handleSubmit((data) => - policyUpdate - .mutateAsync({ id, data }) - .then(() => form.reset(data)) - .then(() => utils.environment.policy.byId.invalidate(id)) - .then(() => utils.environment.policy.bySystemId.invalidate(systemId)), - ); - - return ( -
- - ( - - Evaluate With - - - - - - - None - - - - - - - - Regex - - - - - - - - Semver - - - - - - - - Filter - - - - - )} - /> - - {evaluateWith === "regex" && ( - ( - - Regex - - - )} - /> - )} - - {evaluateWith === "semver" && ( - ( - - Semver - - - )} - /> - )} - - {evaluateWith === "filter" && ( - ( - - Filter - - - - - {form.formState.isDirty && ( - - Save to apply - - )} - - )} - /> - )} - -
- - - {evaluateWith === "filter" && ( - - )} -
- - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseSequencing.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseManagement.tsx similarity index 79% rename from apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseSequencing.tsx rename to apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseManagement.tsx index b8b5c96a2..0fb04c1f1 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseSequencing.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/ReleaseManagement.tsx @@ -17,7 +17,7 @@ import { api } from "~/trpc/react"; const schema = z.object({ releaseSequencing: z.enum(["wait", "cancel"]) }); -export const ReleaseSequencing: React.FC<{ +export const ReleaseManagement: React.FC<{ environmentPolicy: SCHEMA.EnvironmentPolicy; }> = ({ environmentPolicy }) => { const form = useForm({ schema, defaultValues: { ...environmentPolicy } }); @@ -36,7 +36,18 @@ export const ReleaseSequencing: React.FC<{ return (
- + +
+

Release Management

+ + Release management policies are concerned with how new and pending + releases are handled within the deployment pipeline. These include + defining sequencing rules, such as whether to cancel or await + pending releases when a new release is triggered, ensuring that + releases happen in a controlled and predictable manner without + conflicts or disruptions. + +
{ - const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const offset = -date.getTimezoneOffset() * ms("1m"); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const hour = date.getHours(); - const minute = date.getMinutes(); - const second = date.getSeconds(); - const millisecond = date.getMilliseconds(); - - return new ZonedDateTime( - year, - month, - day, - timeZone, - offset, - hour, - minute, - second, - millisecond, - ); -}; - -export const ReleaseWindows: React.FC<{ - environmentPolicy: schema.EnvironmentPolicy & { - releaseWindows: schema.EnvironmentPolicyReleaseWindow[]; - }; -}> = ({ environmentPolicy }) => { - const form = useForm({ - schema: releaseWindowsForm, - defaultValues: { - releaseWindows: environmentPolicy.releaseWindows, - }, - }); - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: "releaseWindows", - }); - - const setPolicyWindows = api.environment.policy.setWindows.useMutation(); - const utils = api.useUtils(); - - const { id: policyId, systemId } = environmentPolicy; - const onSubmit = form.handleSubmit((data) => - setPolicyWindows - .mutateAsync({ policyId, releaseWindows: data.releaseWindows }) - .then(() => form.reset(data)) - .then(() => utils.environment.policy.byId.invalidate(policyId)) - .then(() => utils.environment.policy.bySystemId.invalidate(systemId)), - ); - - return ( - - -
- - - Release windows allow you to control when deployments can be - released into an environment. - -
- - {fields.map((field, index) => { - return ( - { - return ( -
- { - onChange({ - ...value, - startTime: t.toDate( - Intl.DateTimeFormat().resolvedOptions().timeZone, - ), - }); - }} - />{" "} - to{" "} - { - onChange({ - ...value, - endTime: t.toDate( - Intl.DateTimeFormat().resolvedOptions().timeZone, - ), - }); - }} - aria-label="End Time" - /> - recurring -
- -
- -
- ); - }} - /> - ); - })} - -
- - -
- - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx new file mode 100644 index 000000000..a04e9c6bf --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx @@ -0,0 +1,284 @@ +import type * as SCHEMA from "@ctrlplane/db/schema"; +import React from "react"; +import { ZonedDateTime } from "@internationalized/date"; +import { IconX } from "@tabler/icons-react"; +import _ from "lodash"; +import ms from "ms"; +import prettyMilliseconds from "pretty-ms"; +import { z } from "zod"; + +import { Button } from "@ctrlplane/ui/button"; +import { DateTimePicker } from "@ctrlplane/ui/date-time-picker/date-time-picker"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + useFieldArray, + useForm, +} from "@ctrlplane/ui/form"; +import { Input } from "@ctrlplane/ui/input"; +import { Label } from "@ctrlplane/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ctrlplane/ui/select"; + +import { api } from "~/trpc/react"; + +const toZonedDateTime = (date: Date): ZonedDateTime => { + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const offset = -date.getTimezoneOffset() * ms("1m"); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hour = date.getHours(); + const minute = date.getMinutes(); + const second = date.getSeconds(); + const millisecond = date.getMilliseconds(); + + return new ZonedDateTime( + year, + month, + day, + timeZone, + offset, + hour, + minute, + second, + millisecond, + ); +}; + +const isValidDuration = (str: string) => !isNaN(ms(str)); + +const schema = z.object({ + releaseWindows: z.array( + z.object({ + policyId: z.string().uuid(), + recurrence: z.enum(["hourly", "daily", "weekly", "monthly"]), + startTime: z.date(), + endTime: z.date(), + }), + ), + duration: z.string().refine(isValidDuration, { + message: "Invalid duration pattern", + }), +}); + +export const RolloutAndTiming: React.FC<{ + environmentPolicy: SCHEMA.EnvironmentPolicy & { + releaseWindows: SCHEMA.EnvironmentPolicyReleaseWindow[]; + }; +}> = ({ environmentPolicy }) => { + const form = useForm({ + schema, + defaultValues: { + ...environmentPolicy, + duration: prettyMilliseconds(environmentPolicy.duration), + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "releaseWindows", + }); + + const setPolicyWindows = api.environment.policy.setWindows.useMutation(); + const updatePolicy = api.environment.policy.update.useMutation(); + const utils = api.useUtils(); + + const { id: policyId, systemId } = environmentPolicy; + const onSubmit = form.handleSubmit(async (data) => { + const { releaseWindows, duration: durationString } = data; + const duration = ms(durationString); + await setPolicyWindows.mutateAsync({ policyId, releaseWindows }); + await updatePolicy.mutateAsync({ id: policyId, data: { duration } }); + + form.reset(data); + await utils.environment.policy.byId.invalidate(policyId); + await utils.environment.policy.bySystemId.invalidate(systemId); + }); + + return ( +
+ +
+

Rollout and Timing

+ + Rollout and timing policies govern how and when deployments are + rolled out to environments. These include incremental rollout + strategies, scheduling deployments during specific windows, and + managing release timing to minimize risk and ensure stability during + the deployment process. + +
+ +
+
+ + + Release windows allow you to control when deployments can be + released into an environment. + +
+ + {fields.length > 0 && ( +
+ {fields.map((field, index) => { + return ( + { + return ( +
+ { + onChange({ + ...value, + startTime: t.toDate( + Intl.DateTimeFormat().resolvedOptions() + .timeZone, + ), + }); + }} + />{" "} + to{" "} + { + onChange({ + ...value, + endTime: t.toDate( + Intl.DateTimeFormat().resolvedOptions() + .timeZone, + ), + }); + }} + aria-label="End Time" + /> + + recurring + +
+ +
+ +
+ ); + }} + /> + ); + })} +
+ )} + + +
+ + ( + +
+ Gradual Rollout + + Enabling gradual rollouts will spread deployments out over a + given duration. A default duration of 0ms means that + deployments will be rolled out immediately. + +
+ +
+
+ + Spread deployments out over + + +
+ +
+
+
+ )} + /> + + + + + ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/release-condition/ComparisonConditionRender.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/release-condition/ComparisonConditionRender.tsx index 930602630..9b32fca50 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/release-condition/ComparisonConditionRender.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/release-condition/ComparisonConditionRender.tsx @@ -121,6 +121,8 @@ export const ComparisonConditionRender: React.FC< onChange(newCondition); }; + const clear = () => onChange({ ...condition, conditions: [] }); + const not = condition.not ?? false; return ( @@ -239,90 +241,96 @@ export const ComparisonConditionRender: React.FC< ))}
- - - - - - - - addCondition({ - type: ReleaseFilterType.Metadata, - operator: ReleaseOperator.Equals, - key: "", - value: "", - }) - } - > - Metadata - - - addCondition({ - type: ReleaseFilterType.CreatedAt, - operator: ReleaseOperator.Before, - value: new Date().toISOString(), - }) - } - > - Created at - - - addCondition({ - type: ReleaseFilterType.Version, - operator: ReleaseOperator.Equals, - value: "", - }) - } + + + + + + addCondition({ + type: ReleaseFilterType.Metadata, + operator: ReleaseOperator.Equals, + key: "", + value: "", + }) + } + > + Metadata + addCondition({ - type: ReleaseFilterType.Comparison, - operator: ReleaseOperator.And, - conditions: [], - not: false, + type: ReleaseFilterType.CreatedAt, + operator: ReleaseOperator.Before, + value: new Date().toISOString(), }) } > - Filter group + Created at - )} - {depth < 2 && ( addCondition({ - type: ReleaseFilterType.Comparison, - operator: ReleaseOperator.And, - not: true, - conditions: [], + type: ReleaseFilterType.Version, + operator: ReleaseOperator.Equals, + value: "", }) } > - Not group + Version - )} - - - + {depth < 2 && ( + + addCondition({ + type: ReleaseFilterType.Comparison, + operator: ReleaseOperator.And, + conditions: [], + not: false, + }) + } + > + Filter group + + )} + {depth < 2 && ( + + addCondition({ + type: ReleaseFilterType.Comparison, + operator: ReleaseOperator.And, + not: true, + conditions: [], + }) + } + > + Not group + + )} + + + +
+ +
); }; From 73680504187afc1b4f6eff3db5ddbe5eb4f90fc9 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Tue, 8 Oct 2024 17:23:43 -0700 Subject: [PATCH 2/3] nit --- .../EnvironmentPolicyDrawer.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx index 63af1e5aa..3e26dce6f 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx @@ -76,24 +76,22 @@ const View: React.FC<{ const PolicyDropdownMenu: React.FC<{ environmentPolicy: SCHEMA.EnvironmentPolicy; children: React.ReactNode; -}> = ({ environmentPolicy, children }) => { - return ( - - {children} - - - e.preventDefault()} - > - - Delete - - - - - ); -}; +}> = ({ environmentPolicy, children }) => ( + + {children} + + + e.preventDefault()} + > + + Delete + + + + +); export const EnvironmentPolicyDrawer: React.FC = () => { const { environmentPolicyId, removeEnvironmentPolicyId } = From 02fa8eb7321fbb73e602c6ade08976d06b0ac245 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Tue, 8 Oct 2024 17:26:17 -0700 Subject: [PATCH 3/3] nit --- .../environment-policy-drawer/RolloutAndTiming.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx index a04e9c6bf..bf22aca97 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/RolloutAndTiming.tsx @@ -78,12 +78,10 @@ export const RolloutAndTiming: React.FC<{ releaseWindows: SCHEMA.EnvironmentPolicyReleaseWindow[]; }; }> = ({ environmentPolicy }) => { + const duration = prettyMilliseconds(environmentPolicy.duration); const form = useForm({ schema, - defaultValues: { - ...environmentPolicy, - duration: prettyMilliseconds(environmentPolicy.duration), - }, + defaultValues: { ...environmentPolicy, duration }, }); const { fields, append, remove } = useFieldArray({