-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Release filter validation on policy (#121)
- Loading branch information
1 parent
de2b7d9
commit cc8a3b7
Showing
37 changed files
with
9,494 additions
and
1,114 deletions.
There are no files selected for viewing
403 changes: 403 additions & 0 deletions
403
apps/webservice/src/app/[workspaceSlug]/_components/EnvironmentDrawer.tsx
Large diffs are not rendered by default.
Oops, something went wrong.
23 changes: 23 additions & 0 deletions
23
apps/webservice/src/app/[workspaceSlug]/_components/TabButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); |
80 changes: 80 additions & 0 deletions
80
apps/webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Approval.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
107 changes: 107 additions & 0 deletions
107
.../webservice/src/app/[workspaceSlug]/_components/environment-policy-drawer/Concurrency.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
170 changes: 170 additions & 0 deletions
170
...src/app/[workspaceSlug]/_components/environment-policy-drawer/EnvironmentPolicyDrawer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.