+
{
/>
- {environmentPolicy != null && (
-
-
+ {loading && (
+
+
)}
+ {!loading && environmentPolicy != null && deployments != null && (
+
+ )}
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx
index 32f8a895f..25323bf91 100644
--- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/Overview.tsx
@@ -1,85 +1,50 @@
-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,
FormField,
FormItem,
FormLabel,
FormMessage,
- useForm,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";
import { Textarea } from "@ctrlplane/ui/textarea";
-import { api } from "~/trpc/react";
-
-const schema = z.object({ name: z.string(), description: z.string() });
+import type { PolicyFormSchema } from "./PolicyFormSchema";
export const Overview: React.FC<{
- environmentPolicy: SCHEMA.EnvironmentPolicy;
-}> = ({ environmentPolicy }) => {
- const form = useForm({
- schema,
- defaultValues: {
- name: environmentPolicy.name,
- description: environmentPolicy.description ?? "",
- },
- });
-
- 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 (
-
-
-
-
- );
-};
+ form: PolicyFormSchema;
+}> = ({ form }) => (
+
+ (
+
+ Name
+
+
+
+
+
+ )}
+ />
+ (
+
+ Description
+
+
+
+
+
+ )}
+ />
+
+);
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyDeleteDialog.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyDeleteDialog.tsx
index f7d5c5730..51dcc45e1 100644
--- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyDeleteDialog.tsx
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyDeleteDialog.tsx
@@ -14,7 +14,7 @@ import {
import { buttonVariants } from "@ctrlplane/ui/button";
import { api } from "~/trpc/react";
-import { useEnvironmentPolicyDrawer } from "./EnvironmentPolicyDrawer";
+import { useEnvironmentPolicyDrawer } from "./useEnvironmentPolicyDrawer";
export const DeleteEnvironmentPolicyDialog: React.FC<{
environmentPolicy: SCHEMA.EnvironmentPolicy;
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts
new file mode 100644
index 000000000..97cd11548
--- /dev/null
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/PolicyFormSchema.ts
@@ -0,0 +1,19 @@
+import type { UseFormReturn } from "react-hook-form";
+import ms from "ms";
+import { z } from "zod";
+
+import * as SCHEMA from "@ctrlplane/db/schema";
+
+const isValidDuration = (str: string) => !isNaN(ms(str));
+
+export const policyFormSchema = SCHEMA.updateEnvironmentPolicy
+ .omit({
+ rolloutDuration: true,
+ })
+ .extend({
+ rolloutDuration: z.string().refine(isValidDuration, {
+ message: "Invalid duration pattern",
+ }),
+ });
+
+export type PolicyFormSchema = UseFormReturn
>;
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx
index 3316001bc..2170ab996 100644
--- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseChannels.tsx
@@ -1,10 +1,8 @@
import type * as SCHEMA from "@ctrlplane/db/schema";
-import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { IconPlus } from "@tabler/icons-react";
-import { Button } from "@ctrlplane/ui/button";
import { Label } from "@ctrlplane/ui/label";
import {
Select,
@@ -14,17 +12,16 @@ import {
SelectValue,
} from "@ctrlplane/ui/select";
-import { api } from "~/trpc/react";
-
-type Policy = SCHEMA.EnvironmentPolicy & {
- releaseChannels: SCHEMA.ReleaseChannel[];
-};
+import type { PolicyFormSchema } from "./PolicyFormSchema";
type Deployment = SCHEMA.Deployment & {
releaseChannels: SCHEMA.ReleaseChannel[];
};
-type ReleaseChannelProps = { policy: Policy; deployments: Deployment[] };
+type ReleaseChannelProps = {
+ form: PolicyFormSchema;
+ deployments: Deployment[];
+};
type DeploymentSelectProps = {
deployment: Deployment;
@@ -90,39 +87,20 @@ const DeploymentSelect: React.FC = ({
};
export const ReleaseChannels: React.FC = ({
- policy,
+ form,
deployments,
}) => {
- const updateReleaseChannels =
- api.environment.policy.updateReleaseChannels.useMutation();
- const utils = api.useUtils();
-
- const deploymentsWithReleaseChannels = deployments.filter(
- (d) => d.releaseChannels.length > 0,
- );
-
- const currReleaseChannels = Object.fromEntries(
- deploymentsWithReleaseChannels.map((d) => [
- d.id,
- policy.releaseChannels.find((rc) => rc.deploymentId === d.id)?.id ?? null,
- ]),
- );
-
- const [releaseChannels, setReleaseChannels] =
- useState>(currReleaseChannels);
+ const policyReleaseChannels = form.watch("releaseChannels");
const updateReleaseChannel = (
deploymentId: string,
channelId: string | null,
- ) => setReleaseChannels((prev) => ({ ...prev, [deploymentId]: channelId }));
-
- const onSubmit = () =>
- updateReleaseChannels
- .mutateAsync({
- id: policy.id,
- releaseChannels,
- })
- .then(() => utils.environment.policy.byId.invalidate(policy.id));
+ ) =>
+ form.setValue(
+ "releaseChannels",
+ { ...policyReleaseChannels, [deploymentId]: channelId },
+ { shouldValidate: true, shouldDirty: true, shouldTouch: true },
+ );
return (
@@ -136,14 +114,11 @@ export const ReleaseChannels: React.FC = ({
))}
-
);
};
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx
index f0fecb9ea..d23bf3d39 100644
--- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/ReleaseManagement.tsx
@@ -1,96 +1,62 @@
-import type * as SCHEMA from "@ctrlplane/db/schema";
-import { z } from "zod";
-
-import { Button } from "@ctrlplane/ui/button";
import {
- Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
- useForm,
} from "@ctrlplane/ui/form";
import { RadioGroup, RadioGroupItem } from "@ctrlplane/ui/radio-group";
-import { api } from "~/trpc/react";
-
-const schema = z.object({ releaseSequencing: z.enum(["wait", "cancel"]) });
+import type { PolicyFormSchema } from "./PolicyFormSchema";
export const ReleaseManagement: 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 (
-
-
- );
-};
+ form: PolicyFormSchema;
+}> = ({ form }) => (
+
+
+
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.
+
+
+
(
+
+
+ Release Sequencing
+
+ Specify whether pending releases should be cancelled or awaited
+ when a new release is triggered.
+
+
+
+
+
+
+
+
+
+ Keep pending releases
+
+
+
+
+
+
+
+ Cancel pending releases
+
+
+
+
+
+ )}
+ />
+
+);
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx
index 61c02e5e1..99ea8baae 100644
--- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/environment-policy-drawer/RolloutAndTiming.tsx
@@ -1,16 +1,12 @@
-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,
@@ -18,7 +14,6 @@ import {
FormLabel,
FormMessage,
useFieldArray,
- useForm,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";
import { Label } from "@ctrlplane/ui/label";
@@ -31,7 +26,8 @@ import {
SelectValue,
} from "@ctrlplane/ui/select";
-import { api } from "~/trpc/react";
+import type { PolicyFormSchema } from "./PolicyFormSchema";
+import { useEnvironmentPolicyDrawer } from "./useEnvironmentPolicyDrawer";
const toZonedDateTime = (date: Date): ZonedDateTime => {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -57,226 +53,166 @@ const toZonedDateTime = (date: Date): ZonedDateTime => {
);
};
-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(),
- }),
- ),
- rolloutDuration: z.string().refine(isValidDuration, {
- message: "Invalid duration pattern",
- }),
-});
-
export const RolloutAndTiming: React.FC<{
- environmentPolicy: SCHEMA.EnvironmentPolicy & {
- releaseWindows: SCHEMA.EnvironmentPolicyReleaseWindow[];
- };
-}> = ({ environmentPolicy }) => {
- const rolloutDuration = prettyMilliseconds(environmentPolicy.rolloutDuration);
- const form = useForm({
- schema,
- defaultValues: { ...environmentPolicy, rolloutDuration },
- });
-
+ form: PolicyFormSchema;
+}> = ({ form }) => {
+ const { environmentPolicyId } = useEnvironmentPolicyDrawer();
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, rolloutDuration: durationString } = data;
- const rolloutDuration = ms(durationString);
- await setPolicyWindows.mutateAsync({ policyId, releaseWindows });
- await updatePolicy.mutateAsync({ id: policyId, data: { rolloutDuration } });
-
- form.reset(data);
- await utils.environment.policy.byId.invalidate(policyId);
- await utils.environment.policy.bySystemId.invalidate(systemId);
- });
-
return (
-