refactor sites settings general form

This commit is contained in:
Milo Schwartz 2024-11-10 23:08:06 -05:00
parent a7955cb8d2
commit e77fb37ef1
No known key found for this signature in database
25 changed files with 159 additions and 367 deletions

View file

@ -76,7 +76,7 @@ export async function listSites(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map((e) => e.message).join(", ") fromError(parsedParams.error)
) )
); );
} }

View file

@ -118,7 +118,7 @@ export default function AccessControlsPage() {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8" className="space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}

View file

@ -5,5 +5,4 @@ export default async function UserPage(props: {
}) { }) {
const { orgId, userId } = await props.params; const { orgId, userId } = await props.params;
redirect(`/${orgId}/settings/access/users/${userId}/access-controls`); redirect(`/${orgId}/settings/access/users/${userId}/access-controls`);
return <></>;
} }

View file

@ -118,7 +118,7 @@ export function CreateResourceForm() {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8" className="space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}

View file

@ -97,7 +97,7 @@ export function GeneralForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"

View file

@ -1,218 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { cn } from "@/lib/utils";
import { toast } from "@/hooks/useToast";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { generateKeypair } from "./wireguardConfig";
import React, { useState, useEffect } from "react";
import { api } from "@/api";
import { useParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { Checkbox } from "@app/components/ui/checkbox";
import { PickSiteDefaultsResponse } from "@server/routers/site";
import CopyTextBox from "@app/components/CopyTextBox";
const method = [
{ label: "Wireguard", value: "wg" },
{ label: "Newt", value: "newt" },
] as const;
const accountFormSchema = z.object({
name: z
.string()
.min(2, {
message: "Name must be at least 2 characters.",
})
.max(30, {
message: "Name must not be longer than 30 characters.",
}),
method: z.enum(["wg", "newt"]),
});
type AccountFormValues = z.infer<typeof accountFormSchema>;
const defaultValues: Partial<AccountFormValues> = {
name: "",
method: "wg",
};
export function CreateSiteForm() {
const params = useParams();
const orgId = params.orgId;
const router = useRouter();
const [keypair, setKeypair] = useState<{
publicKey: string;
privateKey: string;
} | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isChecked, setIsChecked] = useState(false);
const [siteDefaults, setSiteDefaults] =
useState<PickSiteDefaultsResponse | null>(null);
const handleCheckboxChange = (checked: boolean) => {
setIsChecked(checked);
};
const form = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema),
defaultValues,
});
useEffect(() => {
if (typeof window !== "undefined") {
const generatedKeypair = generateKeypair();
setKeypair(generatedKeypair);
setIsLoading(false);
api.get(`/org/${orgId}/pick-site-defaults`)
.catch((e) => {
toast({
title: "Error creating site...",
});
})
.then((res) => {
if (res && res.status === 200) {
setSiteDefaults(res.data.data);
}
});
}
}, []);
async function onSubmit(data: AccountFormValues) {
const res = await api
.put(`/org/${orgId}/site/`, {
name: data.name,
subnet: siteDefaults?.subnet,
exitNodeId: siteDefaults?.exitNodeId,
pubKey: keypair?.publicKey,
})
.catch((e) => {
toast({
title: "Error creating site...",
});
});
if (res && res.status === 201) {
const niceId = res.data.data.niceId;
// navigate to the site page
router.push(`/${orgId}/settings/sites/${niceId}`);
}
}
const wgConfig =
keypair && siteDefaults
? `[Interface]
Address = ${siteDefaults.subnet}
ListenPort = 51820
PrivateKey = ${keypair.privateKey}
[Peer]
PublicKey = ${siteDefaults.publicKey}
AllowedIPs = ${siteDefaults.address.split("/")[0]}/32
Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort}
PersistentKeepalive = 5`
: "";
const newtConfig = `curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh`;
return (
<>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormDescription>
This is the name that will be displayed for
this site.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<div className="relative w-max">
<FormControl>
<select
className={cn(
buttonVariants({
variant: "outline",
}),
"w-[200px] appearance-none font-normal"
)}
{...field}
>
<option value="wg">
WireGuard
</option>
<option value="newt">Newt</option>
</select>
</FormControl>
<ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
</div>
<FormDescription>
This is how you will connect your site to
Fossorial.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{form.watch("method") === "wg" && !isLoading ? (
<CopyTextBox text={wgConfig} />
) : form.watch("method") === "wg" && isLoading ? (
<p>Loading WireGuard configuration...</p>
) : (
<CopyTextBox text={newtConfig} wrapText={false} />
)}
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={isChecked}
onCheckedChange={handleCheckboxChange}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I have copied the config
</label>
</div>
<Button type="submit" disabled={!isChecked}>
Create Site
</Button>
</form>
</Form>
</>
);
}

View file

@ -1,80 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useSiteContext } from "@app/hooks/useSiteContext";
import { useForm } from "react-hook-form";
import api from "@app/api";
import { useToast } from "@app/hooks/useToast";
const GeneralFormSchema = z.object({
name: z.string(),
});
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
export function GeneralForm() {
const { site, updateSite } = useSiteContext();
const { toast } = useToast();
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: site?.name,
},
mode: "onChange",
});
async function onSubmit(data: GeneralFormValues) {
updateSite({ name: data.name });
await api
.post(`/site/${site?.siteId}`, {
name: data.name,
})
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
description:
e.message ||
"An error occurred while updating the site.",
});
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This is the display name of the site.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update Site</Button>
</form>
</Form>
);
}

View file

@ -0,0 +1,99 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useSiteContext } from "@app/hooks/useSiteContext";
import { useForm } from "react-hook-form";
import api from "@app/api";
import { useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
const GeneralFormSchema = z.object({
name: z.string(),
});
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
export default function GeneralPage() {
const { site, updateSite } = useSiteContext();
const { toast } = useToast();
const router = useRouter();
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: site?.name,
},
mode: "onChange",
});
async function onSubmit(data: GeneralFormValues) {
await api
.post(`/site/${site?.siteId}`, {
name: data.name,
})
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
description:
e.message ||
"An error occurred while updating the site.",
});
});
updateSite({ name: data.name });
router.refresh();
}
return (
<>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
General Settings
</h2>
<p className="text-muted-foreground">
Configure the general settings for this site
</p>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This is the display name of the site
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update Site</Button>
</form>
</Form>
</>
);
}

View file

@ -5,6 +5,8 @@ import { AxiosResponse } from "axios";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/api/cookies"; import { authCookieHeader } from "@app/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings"; import { SidebarSettings } from "@app/components/SidebarSettings";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
interface SettingsLayoutProps { interface SettingsLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -17,50 +19,53 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const { children } = props; const { children } = props;
let site = null; let site = null;
try {
if (params.niceId !== "create") { const res = await internal.get<AxiosResponse<GetSiteResponse>>(
try { `/org/${params.orgId}/site/${params.niceId}`,
const res = await internal.get<AxiosResponse<GetSiteResponse>>( await authCookieHeader()
`/org/${params.orgId}/site/${params.niceId}`, );
await authCookieHeader() site = res.data.data;
); } catch {
site = res.data.data; redirect(`/${params.orgId}/settings/sites`);
} catch {
redirect(`/${params.orgId}/settings/sites`);
}
} }
const sidebarNavItems = [ const sidebarNavItems = [
{ {
title: "General", title: "General",
href: "/{orgId}/settings/sites/{niceId}", href: "/{orgId}/settings/sites/{niceId}/general",
}, },
]; ];
const isCreate = params.niceId === "create";
return ( return (
<> <>
<div className="mb-4">
<Link
href="../../"
className="text-muted-foreground hover:underline"
>
<div className="flex flex-row items-center gap-1">
<ArrowLeft /> <span>All Sites</span>
</div>
</Link>
</div>
<div className="space-y-0.5 select-none mb-6"> <div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight"> <h2 className="text-2xl font-bold tracking-tight">
{isCreate ? "New Site" : site?.name + " Settings"} {site?.name + " Settings"}
</h2> </h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{isCreate Configure the settings on your site
? "Create a new site"
: "Configure the settings on your site: " +
site?.name || ""}
.
</p> </p>
</div> </div>
<SidebarSettings <SiteProvider site={site}>
sidebarNavItems={sidebarNavItems} <SidebarSettings
disabled={isCreate} sidebarNavItems={sidebarNavItems}
limitWidth={true} limitWidth={true}
> >
{children} {children}
</SidebarSettings> </SidebarSettings>
</SiteProvider>
</> </>
); );
} }

View file

@ -1,29 +1,8 @@
import React from "react"; import { redirect } from "next/navigation";
import { Separator } from "@/components/ui/separator";
import { CreateSiteForm } from "./components/CreateSite";
import { GeneralForm } from "./components/GeneralForm";
export default async function SitePage(props: { export default async function SitePage(props: {
params: Promise<{ niceId: string }>; params: Promise<{ orgId: string; niceId: string }>;
}) { }) {
const params = await props.params; const params = await props.params;
const isCreate = params.niceId === "create"; redirect(`/${params.orgId}/settings/sites/${params.niceId}/general`);
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">
{isCreate ? "Create Site" : "General"}
</h3>
<p className="text-sm text-muted-foreground">
{isCreate
? "Create a new site"
: "Edit basic site settings"}
</p>
</div>
<Separator />
{isCreate ? <CreateSiteForm /> : <GeneralForm />}
</div>
);
} }

View file

@ -133,6 +133,9 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
const niceId = res.data.data.niceId; const niceId = res.data.data.niceId;
// navigate to the site page // navigate to the site page
router.push(`/${orgId}/settings/sites/${niceId}`); router.push(`/${orgId}/settings/sites/${niceId}`);
// close the modal
setOpen(false);
} }
setLoading(false); setLoading(false);
@ -258,6 +261,11 @@ sh get-docker.sh`;
)} )}
</div> </div>
<span className="text-sm text-muted-foreground mt-2">
You will only be able to see the
configuration once.
</span>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="terms" id="terms"

View file

@ -141,7 +141,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
columns={columns} columns={columns}
data={sites} data={sites}
addSite={() => { addSite={() => {
// router.push(`/${orgId}/settings/sites/create`);
setIsCreateModalOpen(true); setIsCreateModalOpen(true);
}} }}
/> />

View file

@ -88,7 +88,7 @@ export function AccountForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"

View file

@ -55,7 +55,7 @@ export function AppearanceForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="font" name="font"

View file

@ -76,7 +76,7 @@ export function DisplayForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="items" name="items"

View file

@ -64,7 +64,7 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
</p> </p>
</div> </div>
<Separator className="my-6" /> <Separator className="my-6" />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> <div className="flex flex-col space-y-4 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside className="-mx-4 lg:w-1/5"> <aside className="-mx-4 lg:w-1/5">
<SidebarNav items={sidebarNavItems} /> <SidebarNav items={sidebarNavItems} />
</aside> </aside>

View file

@ -60,7 +60,7 @@ export function NotificationsForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="type" name="type"

View file

@ -88,7 +88,7 @@ export function ProfileForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="username" name="username"

View file

@ -37,6 +37,7 @@ export default function CopyTextBox({ text = "", wrapText = false }) {
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
type="button"
className="absolute top-1 right-1 z-10" className="absolute top-1 right-1 z-10"
onClick={copyToClipboard} onClick={copyToClipboard}
aria-label="Copy to clipboard" aria-label="Copy to clipboard"

View file

@ -88,7 +88,7 @@ export function AccountForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"

View file

@ -62,7 +62,7 @@ export function AppearanceForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="font" name="font"

View file

@ -76,7 +76,7 @@ export function DisplayForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="items" name="items"

View file

@ -60,7 +60,7 @@ export function NotificationsForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="type" name="type"

View file

@ -88,7 +88,7 @@ export function ProfileForm() {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="username" name="username"

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[30%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}