From a7955cb8d26a86a4fa6aaca6795e7c1f768da4f9 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 10 Nov 2024 22:29:20 -0500 Subject: [PATCH] create site modal --- server/routers/site/pickSiteDefaults.ts | 1 + .../settings/access/users/[userId]/layout.tsx | 21 ++ .../sites/components/CreateSiteForm.tsx | 294 ++++++++++++++++++ .../settings/sites/components/SitesTable.tsx | 212 ++++++------- src/components/ui/breadcrumb.tsx | 115 +++++++ 5 files changed, 539 insertions(+), 104 deletions(-) create mode 100644 src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx create mode 100644 src/components/ui/breadcrumb.tsx diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 3e12ad61..cddddcea 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -76,6 +76,7 @@ export async function pickSiteDefaults( status: HttpCode.OK, }); } catch (error) { + throw error; logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") diff --git a/src/app/[orgId]/settings/access/users/[userId]/layout.tsx b/src/app/[orgId]/settings/access/users/[userId]/layout.tsx index b7abc544..66bace60 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/layout.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/layout.tsx @@ -5,6 +5,16 @@ import { authCookieHeader } from "@app/api/cookies"; import { SidebarSettings } from "@app/components/SidebarSettings"; import { GetOrgUserResponse } from "@server/routers/user"; import OrgUserProvider from "@app/providers/OrgUserProvider"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; interface UserLayoutProps { children: React.ReactNode; @@ -37,6 +47,17 @@ export default async function UserLayoutProps(props: UserLayoutProps) { return ( <> +
+ +
+ All Users +
+ +
+

User {user?.email} diff --git a/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx new file mode 100644 index 00000000..da3fe58f --- /dev/null +++ b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx @@ -0,0 +1,294 @@ +"use client"; + +import api from "@app/api"; +import { Button, buttonVariants } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { useToast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { set, z } from "zod"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle, +} from "@app/components/Credenza"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { useParams, useRouter } from "next/navigation"; +import { PickSiteDefaultsResponse } from "@server/routers/site"; +import { generateKeypair } from "../[niceId]/components/wireguardConfig"; +import { cn } from "@app/lib/utils"; +import { ChevronDownIcon } from "lucide-react"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { Checkbox } from "@app/components/ui/checkbox"; + +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; + +const defaultValues: Partial = { + name: "", + method: "wg", +}; + +type CreateSiteFormProps = { + open: boolean; + setOpen: (open: boolean) => void; +}; + +export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) { + const { toast } = useToast(); + + const [loading, setLoading] = useState(false); + + 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(null); + + const handleCheckboxChange = (checked: boolean) => { + setIsChecked(checked); + }; + + const form = useForm({ + resolver: zodResolver(accountFormSchema), + defaultValues, + }); + + useEffect(() => { + if (!open) return; + + if (typeof window !== "undefined") { + const generatedKeypair = generateKeypair(); + setKeypair(generatedKeypair); + setIsLoading(false); + + api.get(`/org/${orgId}/pick-site-defaults`) + .catch((e) => { + toast({ + title: "Error picking site defaults", + }); + }) + .then((res) => { + if (res && res.status === 200) { + setSiteDefaults(res.data.data); + } + }); + } + }, [open]); + + async function onSubmit(data: AccountFormValues) { + setLoading(true); + 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}`); + } + + setLoading(false); + } + + 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 ( + <> + { + setOpen(val); + setLoading(false); + + // reset all values + form.reset(); + setIsChecked(false); + setKeypair(null); + setSiteDefaults(null); + }} + > + + + Create Site + + Create a new site to start connecting your resources + + + +
+ + ( + + Name + + + + + This is the name that will be + displayed for this site. + + + + )} + /> + ( + + Method +
+ + + + +
+ + This is how you will connect + your site to Fossorial. + + +
+ )} + /> + +
+ {form.watch("method") === "wg" && + !isLoading ? ( + + ) : form.watch("method") === "wg" && + isLoading ? ( +

+ Loading WireGuard configuration... +

+ ) : ( + + )} +
+ +
+ + +
+ + +
+ + + + + + +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/sites/components/SitesTable.tsx b/src/app/[orgId]/settings/sites/components/SitesTable.tsx index 6634b41f..254f07e3 100644 --- a/src/app/[orgId]/settings/sites/components/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesTable.tsx @@ -13,8 +13,9 @@ import { ArrowUpDown, MoreHorizontal } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import api from "@app/api"; -import { authCookieHeader } from "@app/api/cookies"; import { AxiosResponse } from "axios"; +import { useState } from "react"; +import CreateSiteForm from "./CreateSiteForm"; export type SiteRow = { id: number; @@ -25,95 +26,6 @@ export type SiteRow = { orgId: string; }; -export const columns: ColumnDef[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - }, - }, - { - accessorKey: "nice", - header: ({ column }) => { - return ( - - ); - }, - }, - { - accessorKey: "mbIn", - header: "MB In", - }, - { - accessorKey: "mbOut", - header: "MB Out", - }, - { - id: "actions", - cell: ({ row }) => { - const router = useRouter(); - - const siteRow = row.original; - - const deleteSite = (siteId: number) => { - api.delete(`/site/${siteId}`) - .catch((e) => { - console.error("Error deleting site", e); - }) - .then(() => { - router.refresh(); - }); - }; - - return ( - - - - - - - - View settings - - - - - - - - ); - }, - }, -]; - type SitesTableProps = { sites: SiteRow[]; orgId: string; @@ -122,26 +34,118 @@ type SitesTableProps = { export default function SitesTable({ sites, orgId }: SitesTableProps) { const router = useRouter(); - const callApi = async () => { + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const res = await api.put>( - `/newt` - ); + const callApi = async () => { + const res = await api.put>(`/newt`); console.log(res); - }; + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "nice", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "mbIn", + header: "MB In", + }, + { + accessorKey: "mbOut", + header: "MB Out", + }, + { + id: "actions", + cell: ({ row }) => { + const router = useRouter(); + + const siteRow = row.original; + + const deleteSite = (siteId: number) => { + api.delete(`/site/${siteId}`) + .catch((e) => { + console.error("Error deleting site", e); + }) + .then(() => { + router.refresh(); + }); + }; + + return ( + + + + + + + + View settings + + + + + + + + ); + }, + }, + ]; + return ( <> - { - router.push(`/${orgId}/settings/sites/create`); - }} - /> - - + + { + // router.push(`/${orgId}/settings/sites/create`); + setIsCreateModalOpen(true); + }} + /> + + ); } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..60e6c96f --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>