Skip to content

Commit da070a2

Browse files
chore: init policy form create
1 parent a9ad971 commit da070a2

File tree

6 files changed

+195
-2
lines changed

6 files changed

+195
-2
lines changed

apps/web/app/routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export default [
7373
route("runners", "routes/ws/runners.tsx"),
7474
route("providers", "routes/ws/providers.tsx"),
7575
route("policies", "routes/ws/policies.tsx"),
76+
route("policies", "routes/ws/policies/_layout.tsx", [
77+
route("create", "routes/ws/policies/page.create.tsx"),
78+
]),
7679

7780
route("settings", "routes/ws/settings/_layout.tsx", [
7881
route("general", "routes/ws/settings/general.tsx"),

apps/web/app/routes/ws/policies.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
TableRow,
2222
} from "~/components/ui/table";
2323
import { useWorkspace } from "~/components/WorkspaceProvider";
24+
import { buttonVariants } from "../../components/ui/button";
2425

2526
export function meta() {
2627
return [
@@ -90,8 +91,16 @@ export default function Policies() {
9091
</BreadcrumbList>
9192
</Breadcrumb>
9293
</div>
93-
<div className="text-sm text-muted-foreground">
94-
{total} {total === 1 ? "policy" : "policies"}
94+
<div className="flex items-center gap-4">
95+
<div className="text-sm text-muted-foreground">
96+
{total} {total === 1 ? "policy" : "policies"}
97+
</div>
98+
<a
99+
href={`/${workspace.slug}/policies/create`}
100+
className={buttonVariants({ variant: "default" })}
101+
>
102+
New Policy
103+
</a>
95104
</div>
96105
</div>
97106
</header>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Link, useLocation } from "react-router";
2+
3+
import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs";
4+
import { useWorkspace } from "~/components/WorkspaceProvider";
5+
6+
type PolicyTab = "general";
7+
8+
const usePolicyTab = (baseUrl: string): PolicyTab => {
9+
const { pathname } = useLocation();
10+
if (pathname === baseUrl) return "general";
11+
return "general";
12+
};
13+
14+
export const PoliciesNavbarTabs = () => {
15+
const { workspace } = useWorkspace();
16+
const baseUrl = `/${workspace.slug}/policies/create`;
17+
const value = usePolicyTab(baseUrl);
18+
19+
return (
20+
<Tabs value={value}>
21+
<TabsList>
22+
<TabsTrigger value="general" asChild>
23+
<Link to={`${baseUrl}`}>General</Link>
24+
</TabsTrigger>
25+
</TabsList>
26+
</Tabs>
27+
);
28+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { UseFormReturn } from "react-hook-form";
2+
import { createContext, useContext } from "react";
3+
import { zodResolver } from "@hookform/resolvers/zod";
4+
import { useForm } from "react-hook-form";
5+
import { z } from "zod";
6+
7+
import { Form } from "~/components/ui/form";
8+
9+
const selectorSchema = z.object({
10+
cel: z.string().min(1, "CEL expression is required"),
11+
});
12+
13+
export const policyCreateFormSchema = z.object({
14+
name: z.string().min(1, "Policy name is required"),
15+
description: z.string().optional(),
16+
priority: z.number().min(0, "Priority must be greater than 0"),
17+
enabled: z.boolean().default(true),
18+
target: z.object({
19+
deploymentSelector: selectorSchema,
20+
environmentSelector: selectorSchema,
21+
resourceSelector: selectorSchema,
22+
}),
23+
anyApproval: z
24+
.object({
25+
minApprovals: z
26+
.number()
27+
.min(1, "Minimum approvals must be greater than 0"),
28+
})
29+
.optional(),
30+
});
31+
32+
export type PolicyCreateFormSchema = z.infer<typeof policyCreateFormSchema>;
33+
34+
type PolicyFormContextType = {
35+
form: UseFormReturn<PolicyCreateFormSchema>;
36+
};
37+
38+
const PolicyFormContext = createContext<PolicyFormContextType | null>(null);
39+
40+
export function usePolicyCreateForm() {
41+
const context = useContext(PolicyFormContext);
42+
if (context == null)
43+
throw new Error(
44+
"usePolicyCreateForm must be used within a PolicyCreateFormContext",
45+
);
46+
return context;
47+
}
48+
49+
export const PolicyCreateFormContextProvider: React.FC<{
50+
children: React.ReactNode;
51+
}> = ({ children }) => {
52+
const form = useForm({
53+
resolver: zodResolver(policyCreateFormSchema),
54+
defaultValues: {
55+
name: "",
56+
priority: 0,
57+
enabled: true,
58+
target: {
59+
deploymentSelector: { cel: "false" },
60+
environmentSelector: { cel: "false" },
61+
resourceSelector: { cel: "false" },
62+
},
63+
},
64+
});
65+
66+
const onSubmit = form.handleSubmit((data) => {
67+
console.log(data);
68+
});
69+
70+
return (
71+
<PolicyFormContext.Provider value={{ form }}>
72+
<Form {...form}>
73+
<form onSubmit={onSubmit}>{children}</form>
74+
</Form>
75+
</PolicyFormContext.Provider>
76+
);
77+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Link, Outlet } from "react-router";
2+
3+
import {
4+
Breadcrumb,
5+
BreadcrumbItem,
6+
BreadcrumbList,
7+
BreadcrumbPage,
8+
BreadcrumbSeparator,
9+
} from "~/components/ui/breadcrumb";
10+
import { Separator } from "~/components/ui/separator";
11+
import { SidebarTrigger } from "~/components/ui/sidebar";
12+
import { useWorkspace } from "~/components/WorkspaceProvider";
13+
import { PolicyCreateFormContextProvider } from "./_components/create/PolicyFormContext";
14+
import { PoliciesNavbarTabs } from "./_components/PoliciesNavbarTabs";
15+
16+
export default function PoliciesLayout() {
17+
const { workspace } = useWorkspace();
18+
19+
return (
20+
<PolicyCreateFormContextProvider>
21+
<header className="flex h-16 shrink-0 items-center gap-2 border-b">
22+
<div className="flex items-center gap-2 px-4">
23+
<SidebarTrigger className="-ml-1" />
24+
<Separator
25+
orientation="vertical"
26+
className="mr-2 data-[orientation=vertical]:h-4"
27+
/>
28+
<Breadcrumb>
29+
<BreadcrumbList>
30+
<BreadcrumbItem>
31+
<Link to={`/${workspace.slug}/policies`}>Policies</Link>
32+
</BreadcrumbItem>
33+
<BreadcrumbSeparator />
34+
<BreadcrumbItem>
35+
<BreadcrumbPage>Create</BreadcrumbPage>
36+
</BreadcrumbItem>
37+
</BreadcrumbList>
38+
</Breadcrumb>
39+
</div>
40+
</header>
41+
<main className="flex-1 overflow-auto p-6">
42+
<div className="container mx-auto max-w-3xl space-y-6">
43+
<PoliciesNavbarTabs />
44+
<Outlet />
45+
</div>
46+
</main>
47+
</PolicyCreateFormContextProvider>
48+
);
49+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
2+
3+
export function meta() {
4+
return [
5+
{ title: "Create Policy - Ctrlplane" },
6+
{
7+
name: "description",
8+
content: "Create a new policy",
9+
},
10+
];
11+
}
12+
13+
export default function PageCreate() {
14+
return (
15+
<Card>
16+
<CardHeader>
17+
<CardTitle>Create New Policy</CardTitle>
18+
</CardHeader>
19+
<CardContent>
20+
{/* TODO: Add policy creation form here */}
21+
<p className="text-muted-foreground">
22+
Policy creation form coming soon...
23+
</p>
24+
</CardContent>
25+
</Card>
26+
);
27+
}

0 commit comments

Comments
 (0)