feat: domain slide
This commit is contained in:
parent
0fd3056259
commit
f067a01e47
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { uuidv7 } from "uuidv7";
|
||||
import { z } from "zod";
|
||||
|
@ -37,8 +37,10 @@ export type DomainsListForm = z.infer<typeof formValidator>;
|
|||
|
||||
export default function DomainsList({
|
||||
defaultData,
|
||||
children,
|
||||
}: {
|
||||
defaultData: RouterOutputs["projects"]["services"]["get"];
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const service = useService(undefined, defaultData);
|
||||
const updateDomain = api.projects.services.updateDomain.useMutation();
|
||||
|
@ -65,11 +67,6 @@ export default function DomainsList({
|
|||
);
|
||||
}, [form, service.data?.domains]);
|
||||
|
||||
console.log(
|
||||
"Rendering fields with ids: ",
|
||||
domainsForm.fields.map((field) => field.id),
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
@ -153,8 +150,6 @@ export default function DomainsList({
|
|||
internalPort: 8080,
|
||||
};
|
||||
|
||||
console.log("add domain: ", domain.domainId);
|
||||
|
||||
domainsForm.append(domain);
|
||||
}}
|
||||
>
|
||||
|
@ -164,6 +159,8 @@ export default function DomainsList({
|
|||
{/* <FormUnsavedChangesIndicator form={form} /> */}
|
||||
</motion.div>
|
||||
{/* </AnimatePresence> */}
|
||||
|
||||
<AnimatePresence>{children}</AnimatePresence>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "~/components/ui/sheet";
|
||||
|
||||
export default function DomainSlideLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
// need to keep state separately just so DOM elements dont get unloaded and break close animation
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
// re-open on new nav
|
||||
useEffect(() => {
|
||||
setOpen(true);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setOpen(false);
|
||||
router.push(
|
||||
new URL(pathname + "/../", window.location.href).pathname,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit Domain</SheetTitle>
|
||||
<SheetDescription>
|
||||
Make advanced changes to the reverse-proxy configuration for this
|
||||
domain. Hit the save button on the main page to apply changes.
|
||||
Reloading will cause you to lose unsaved edits.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
{children}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { CgSpinner } from "react-icons/cg";
|
||||
|
||||
export default function DomainSlideLoading() {
|
||||
return (
|
||||
<div className="flex min-h-24 w-full items-center justify-center">
|
||||
<CgSpinner className="flex-shrink animate-spin" size="24" />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,46 +1,42 @@
|
|||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "~/components/ui/sheet";
|
||||
"use client";
|
||||
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { SimpleFormField } from "~/hooks/forms";
|
||||
import { type DomainsListForm } from "../DomainsList";
|
||||
|
||||
export default function DomainPage() {
|
||||
const form = useFormContext<DomainsListForm>();
|
||||
const domainId = useSelectedLayoutSegment();
|
||||
|
||||
const index = useMemo(
|
||||
() =>
|
||||
form.getValues("domains").findIndex(
|
||||
(domain) =>
|
||||
domain.domainId === domainId ||
|
||||
// @ts-expect-error this exists
|
||||
domain.id === domainId,
|
||||
),
|
||||
[form, domainId],
|
||||
);
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit profile</SheetTitle>
|
||||
<SheetDescription>
|
||||
Make changes to your profile here. Click save when you're done.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input id="name" value="Pedro Duarte" className="col-span-3" />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="username" className="text-right">
|
||||
Username
|
||||
</Label>
|
||||
<Input id="username" value="@peduarte" className="col-span-3" />
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button type="submit">Save changes</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex flex-col gap-4 pt-4">
|
||||
<SimpleFormField
|
||||
control={form.control}
|
||||
friendlyName="Force HTTPS"
|
||||
description={
|
||||
<>
|
||||
Automatically redirects all HTTP requests to HTTPS. Recommended by
|
||||
default. Redirects will go to the same page using the same domain
|
||||
and all query paramters will be preserved.
|
||||
</>
|
||||
}
|
||||
name={`domains.${index}.forceSSL`}
|
||||
render={({ field }) => <Switch {...field} className="my-4 block" />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,39 +1,35 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight, CogIcon, TrashIcon } from "lucide-react";
|
||||
import { forwardRef, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { forwardRef } from "react";
|
||||
import { useFormContext, type UseFieldArrayReturn } from "react-hook-form";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card } from "~/components/ui/card";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { SimpleFormField } from "~/hooks/forms";
|
||||
import { useProject } from "../../../../_context/ProjectContext";
|
||||
import { type DomainsListForm } from "../DomainsList";
|
||||
|
||||
type FieldData = {
|
||||
id: string; // internal ID for react-form-hook
|
||||
domainId?: string;
|
||||
domain: string;
|
||||
internalPort: number;
|
||||
https: boolean;
|
||||
forceSSL: boolean;
|
||||
};
|
||||
|
||||
const DomainEntry = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
field: FieldData;
|
||||
field: DomainsListForm["domains"][number] & {
|
||||
id: string; // react-hook-form adds this
|
||||
};
|
||||
index: number;
|
||||
domains: UseFieldArrayReturn<DomainsListForm, "domains", "id">;
|
||||
}
|
||||
>(({ field, index, domains }, ref) => {
|
||||
const form = useFormContext();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const project = useProject();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
// layout
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
|
@ -80,7 +76,16 @@ const DomainEntry = forwardRef<
|
|||
icon={CogIcon}
|
||||
className="mr-2"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
const domains = pathname.trim().endsWith("/domains")
|
||||
? pathname.trim()
|
||||
: pathname.replace(/\/[A-z0-9]*?$/, ``);
|
||||
|
||||
console.log(`${domains}/${field.domainId ?? field.id}`);
|
||||
router.push(
|
||||
`${project.servicePath}/domains/${
|
||||
field.domainId ?? field.id
|
||||
}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -95,27 +100,6 @@ const DomainEntry = forwardRef<
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div className="mt-4 grid grid-cols-2 rounded-md bg-background p-8">
|
||||
<h1 className="col-span-2 pb-4">Advanced Settings</h1>
|
||||
|
||||
<SimpleFormField
|
||||
control={form.control}
|
||||
name={`domains.${index}.forceSSL`}
|
||||
friendlyName="Force SSL"
|
||||
render={({ field }) => (
|
||||
<div className="pt-2">
|
||||
<Switch {...field} className="mr-auto block" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* TODO: allow custom SSL certificates */}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
|
|
|
@ -15,5 +15,20 @@ export default async function DomainsLayout(props: {
|
|||
serviceId: props.params.serviceId,
|
||||
});
|
||||
|
||||
return <DomainsList defaultData={service} />;
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row gap-4 rounded-lg border-border bg-card">
|
||||
<div className="w-2 rounded-bl-lg rounded-tl-lg bg-primary" />
|
||||
<p className="py-4 pr-4 text-primary-foreground">
|
||||
By exposing this service, it will be added to a global network for the
|
||||
reverse proxy and thus will be able to communicate with other services
|
||||
from different projects that also have an exposed domain. To avoid
|
||||
this, consider setting up a separate proxy (traefik, freenginx, etc)
|
||||
for this project and expose that instead.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DomainsList defaultData={service}>{props.children}</DomainsList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GetInputProps, UseFormReturnType } from "@mantine/form/lib/types";
|
||||
import { type UseFormReturnType } from "@mantine/form/lib/types";
|
||||
import { Label } from "./ui/label";
|
||||
import { Required } from "./ui/required";
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "~/utils/utils.ts"
|
||||
import { cn } from "~/utils/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
|
@ -22,13 +22,13 @@ const SheetOverlay = React.forwardRef<
|
|||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
|
@ -46,8 +46,8 @@ const sheetVariants = cva(
|
|||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
|
@ -71,8 +71,8 @@ const SheetContent = React.forwardRef<
|
|||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
|
@ -81,12 +81,12 @@ const SheetHeader = ({
|
|||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
|
@ -95,12 +95,12 @@ const SheetFooter = ({
|
|||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
|
@ -111,8 +111,8 @@ const SheetTitle = React.forwardRef<
|
|||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
|
@ -123,18 +123,18 @@ const SheetDescription = React.forwardRef<
|
|||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue