Skip to content

Commit

Permalink
fix: Release filter validation on policy (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Oct 8, 2024
1 parent de2b7d9 commit cc8a3b7
Show file tree
Hide file tree
Showing 37 changed files with 9,494 additions and 1,114 deletions.

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/_components/TabButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cn } from "@ctrlplane/ui";
import { Button } from "@ctrlplane/ui/button";

export const TabButton: React.FC<{
active: boolean;
onClick: () => void;
icon: React.ReactNode;
label: string;
}> = ({ active, onClick, icon, label }) => (
<Button
onClick={onClick}
variant="ghost"
className={cn(
"flex h-7 w-full items-center justify-normal gap-2 p-2 py-0 pr-3",
active
? "bg-blue-500/10 text-blue-300 hover:bg-blue-500/10 hover:text-blue-300"
: "text-muted-foreground",
)}
>
{icon}
{label}
</Button>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 (
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-6 p-2">
<FormField
control={form.control}
name="approvalRequirement"
render={({ field: { value, onChange } }) => (
<FormItem>
<div className="space-y-1">
<FormLabel>Approval gates</FormLabel>
<FormDescription>
If enabled, a release will require approval from an authorized
user before it can be deployed to any environment with this
policy.
</FormDescription>
</div>
<FormControl>
<div className="flex items-center gap-2">
<span className="text-xs text-neutral-400">Enabled:</span>{" "}
<Switch
checked={value === "manual"}
onCheckedChange={(checked) =>
onChange(checked ? "manual" : "automatic")
}
/>
</div>
</FormControl>
</FormItem>
)}
/>

<Button
type="submit"
disabled={form.formState.isSubmitting || !form.formState.isDirty}
>
Save
</Button>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 (
<Form {...form}>
<form onSubmit={onSubmit} className="space-y-6 p-2">
<FormField
control={form.control}
name="concurrencyType"
render={({ field: { value, onChange } }) => (
<FormItem>
<div className="space-y-4">
<div className="flex flex-col gap-1">
<FormLabel>Concurrency</FormLabel>
<FormDescription>
The number of jobs that can run concurrently in an
environment.
</FormDescription>
</div>
<FormControl>
<RadioGroup value={value} onValueChange={onChange}>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="all" />
</FormControl>
<FormLabel className="flex items-center gap-2 font-normal">
All jobs can run concurrently
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="some" className="min-w-4" />
</FormControl>
<FormLabel className="flex flex-wrap items-center gap-2 font-normal">
A maximum of
<Input
disabled={value !== "some"}
type="number"
value={concurrencyLimit}
onChange={(e) =>
form.setValue(
"concurrencyLimit",
e.target.valueAsNumber,
)
}
className="border-b-1 h-6 w-16 text-xs"
/>
jobs can run concurrently
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
</div>
</FormItem>
)}
/>

<Button
type="submit"
disabled={form.formState.isSubmitting || !form.formState.isDirty}
>
Save
</Button>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"use client";

import type * as SCHEMA from "@ctrlplane/db/schema";
import type React from "react";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
IconCalendar,
IconCheck,
IconChecklist,
IconCircuitDiode,
IconClock,
IconFilter,
IconInfoCircle,
IconPlayerPause,
} from "@tabler/icons-react";

import { Drawer, DrawerContent, DrawerTitle } from "@ctrlplane/ui/drawer";

import { api } from "~/trpc/react";
import { TabButton } from "../TabButton";
import { Approval } from "./Approval";
import { Concurrency } from "./Concurrency";
import { GradualRollouts } from "./GradualRollouts";
import { Overview } from "./Overview";
import { ReleaseFilter } from "./ReleaseFilter";
import { ReleaseSequencing } from "./ReleaseSequencing";
import { ReleaseWindows } from "./ReleaseWindows";
import { SuccessCriteria } from "./SuccessCriteria";

const param = "environment_policy_id";
export const useEnvironmentPolicyDrawer = () => {
const router = useRouter();
const params = useSearchParams();
const environmentPolicyId = params.get(param);

const setEnvironmentPolicyId = (id: string | null) => {
const url = new URL(window.location.href);
if (id === null) {
url.searchParams.delete(param);
} else {
url.searchParams.set(param, id);
}
router.replace(url.toString());
};

const removeEnvironmentPolicyId = () => setEnvironmentPolicyId(null);

return {
environmentPolicyId,
setEnvironmentPolicyId,
removeEnvironmentPolicyId,
};
};

const View: React.FC<{
activeTab: string;
environmentPolicy: SCHEMA.EnvironmentPolicy & {
releaseWindows: SCHEMA.EnvironmentPolicyReleaseWindow[];
};
}> = ({ activeTab, environmentPolicy }) => {
return {
overview: <Overview environmentPolicy={environmentPolicy} />,
approval: <Approval environmentPolicy={environmentPolicy} />,
concurrency: <Concurrency environmentPolicy={environmentPolicy} />,
"gradual-rollout": (
<GradualRollouts environmentPolicy={environmentPolicy} />
),
"success-criteria": (
<SuccessCriteria environmentPolicy={environmentPolicy} />
),
"release-sequencing": (
<ReleaseSequencing environmentPolicy={environmentPolicy} />
),
"release-windows": <ReleaseWindows environmentPolicy={environmentPolicy} />,
"release-filter": <ReleaseFilter environmentPolicy={environmentPolicy} />,
}[activeTab];
};

export const EnvironmentPolicyDrawer: React.FC = () => {
const { environmentPolicyId, removeEnvironmentPolicyId } =
useEnvironmentPolicyDrawer();
const isOpen = environmentPolicyId != null && environmentPolicyId != "";
const setIsOpen = removeEnvironmentPolicyId;
const environmentPolicyQ = api.environment.policy.byId.useQuery(
environmentPolicyId ?? "",
{ enabled: isOpen },
);
const environmentPolicy = environmentPolicyQ.data;

const [activeTab, setActiveTab] = useState("overview");

return (
<Drawer open={isOpen} onOpenChange={setIsOpen}>
<DrawerContent
showBar={false}
className="left-auto right-0 top-0 mt-0 h-screen w-[1200px] overflow-auto rounded-none focus-visible:outline-none"
>
<DrawerTitle className="flex items-center gap-2 border-b p-6">
<div className="flex-shrink-0 rounded bg-purple-500/20 p-1 text-purple-400">
<IconFilter className="h-4 w-4" />
</div>
{environmentPolicy?.name ?? "Policy"}
</DrawerTitle>

<div className="flex w-full gap-6 p-6">
<div className="space-y-1">
<TabButton
active={activeTab === "overview"}
onClick={() => setActiveTab("overview")}
icon={<IconInfoCircle className="h-4 w-4" />}
label="Overview"
/>
<TabButton
active={activeTab === "approval"}
onClick={() => setActiveTab("approval")}
icon={<IconCheck className="h-4 w-4" />}
label="Approval"
/>
<TabButton
active={activeTab === "concurrency"}
onClick={() => setActiveTab("concurrency")}
icon={<IconCircuitDiode className="h-4 w-4" />}
label="Concurrency"
/>
<TabButton
active={activeTab === "gradual-rollout"}
onClick={() => setActiveTab("gradual-rollout")}
icon={<IconClock className="h-4 w-4" />}
label="Gradual Rollout"
/>
<TabButton
active={activeTab === "success-criteria"}
onClick={() => setActiveTab("success-criteria")}
icon={<IconChecklist className="h-4 w-4" />}
label="Success Criteria"
/>
<TabButton
active={activeTab === "release-sequencing"}
onClick={() => setActiveTab("release-sequencing")}
icon={<IconPlayerPause className="h-4 w-4" />}
label="Release Sequencing"
/>
<TabButton
active={activeTab === "release-windows"}
onClick={() => setActiveTab("release-windows")}
icon={<IconCalendar className="h-4 w-4" />}
label="Release Windows"
/>
<TabButton
active={activeTab === "release-filter"}
onClick={() => setActiveTab("release-filter")}
icon={<IconFilter className="h-4 w-4" />}
label="Release Filter"
/>
</div>

{environmentPolicy != null && (
<div className="w-full overflow-auto">
<View
activeTab={activeTab}
environmentPolicy={environmentPolicy}
/>
</div>
)}
</div>
</DrawerContent>
</Drawer>
);
};
Loading

0 comments on commit cc8a3b7

Please sign in to comment.