more visual enhancements and update readme

This commit is contained in:
miloschwartz
2025-03-01 23:03:42 -05:00
parent 0e38f58a7f
commit 759434e9f8
18 changed files with 248 additions and 209 deletions

View File

@@ -67,6 +67,7 @@ import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import { Label } from "@app/components/ui/label";
import { ListDomainsResponse } from "@server/routers/domain";
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
import { StrategySelect } from "@app/components/StrategySelect";
const createResourceFormSchema = z
.object({
@@ -222,6 +223,7 @@ export default function CreateResourceForm({
await fetchSites();
await fetchDomains();
await new Promise((r) => setTimeout(r, 200));
setLoadingPage(false);
};
@@ -241,7 +243,7 @@ export default function CreateResourceForm({
protocol: data.protocol,
proxyPort: data.http ? undefined : data.proxyPort,
siteId: data.siteId,
isBaseDomain: data.http ? undefined : data.isBaseDomain
isBaseDomain: data.http ? data.isBaseDomain : undefined
}
)
.catch((e) => {
@@ -263,6 +265,7 @@ export default function CreateResourceForm({
goToResource(id);
} else {
setShowSnippets(true);
router.refresh();
}
}
}
@@ -272,6 +275,21 @@ export default function CreateResourceForm({
router.push(`/${orgId}/settings/resources/${id || resourceId}`);
}
const launchOptions = [
{
id: "http",
title: "HTTPS Resource",
description:
"Proxy requests to your app over HTTPS using a subdomain or base domain."
},
{
id: "raw",
title: "Raw TCP/UDP Resource",
description:
"Proxy requests to your app over TCP/UDP using a port number."
}
];
return (
<>
<Credenza
@@ -293,7 +311,7 @@ export default function CreateResourceForm({
</CredenzaHeader>
<CredenzaBody>
{loadingPage ? (
<LoaderPlaceholder height="500px" />
<LoaderPlaceholder height="300px" />
) : (
<div>
{!showSnippets && (
@@ -305,59 +323,6 @@ export default function CreateResourceForm({
className="space-y-4"
id="create-resource-form"
>
{!env.flags.allowRawResources || (
<FormField
control={form.control}
name="http"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
HTTP
Resource
</FormLabel>
<FormDescription>
Toggle if
this is an
HTTP
resource or
a raw
TCP/UDP
resource.
</FormDescription>
</div>
<FormControl>
<Switch
checked={
field.value
}
onCheckedChange={
field.onChange
}
/>
</FormControl>
</FormItem>
)}
/>
)}
{!form.watch("http") && (
<Link
className="text-sm text-primary flex items-center gap-1"
href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
target="_blank"
rel="noopener noreferrer"
>
<span>
Learn how to configure
TCP/UDP resources
</span>
<SquareArrowOutUpRight
size={14}
/>
</Link>
)}
<FormField
control={form.control}
name="name"
@@ -374,6 +339,121 @@ export default function CreateResourceForm({
)}
/>
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>
Site
</FormLabel>
<Popover>
<PopoverTrigger
asChild
>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"justify-between",
!field.value &&
"text-muted-foreground"
)}
>
{field.value
? sites.find(
(
site
) =>
site.siteId ===
field.value
)
?.name
: "Select site"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<CommandInput placeholder="Search site" />
<CommandList>
<CommandEmpty>
No
site
found.
</CommandEmpty>
<CommandGroup>
{sites.map(
(
site
) => (
<CommandItem
value={`${site.siteId}:${site.name}:${site.niceId}`}
key={
site.siteId
}
onSelect={() => {
form.setValue(
"siteId",
site.siteId
);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
field.value
? "opacity-100"
: "opacity-0"
)}
/>
{
site.name
}
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
<FormDescription>
This site will
provide connectivity
to the resource.
</FormDescription>
</FormItem>
)}
/>
{!env.flags.allowRawResources || (
<div className="space-y-2">
<FormLabel>
Resource Type
</FormLabel>
<StrategySelect
options={launchOptions}
defaultValue="http"
onChange={(value) =>
form.setValue(
"http",
value === "http"
)
}
/>
<FormDescription>
You cannot change the
type of resource after
creation.
</FormDescription>
</div>
)}
{form.watch("http") &&
env.flags
.allowBaseDomainResources && (
@@ -391,14 +471,19 @@ export default function CreateResourceForm({
}
onValueChange={(
val
) =>
) => {
setDomainType(
val ===
"basedomain"
? "basedomain"
: "subdomain"
)
}
);
form.setValue(
"isBaseDomain",
val ===
"basedomain"
);
}}
>
<FormControl>
<SelectTrigger>
@@ -430,7 +515,7 @@ export default function CreateResourceForm({
Subdomain
</FormLabel>
<div className="flex">
<div className="w-1/2 mr-1">
<div className="w-1/2">
<FormField
control={
form.control
@@ -443,6 +528,7 @@ export default function CreateResourceForm({
<FormControl>
<Input
{...field}
className="border-r-0 rounded-r-none"
/>
</FormControl>
<FormMessage />
@@ -472,7 +558,7 @@ export default function CreateResourceForm({
}
>
<FormControl>
<SelectTrigger>
<SelectTrigger className="rounded-l-none">
<SelectValue />
</SelectTrigger>
</FormControl>
@@ -642,98 +728,6 @@ export default function CreateResourceForm({
/>
</>
)}
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>
Site
</FormLabel>
<Popover>
<PopoverTrigger
asChild
>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"justify-between",
!field.value &&
"text-muted-foreground"
)}
>
{field.value
? sites.find(
(
site
) =>
site.siteId ===
field.value
)
?.name
: "Select site"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<CommandInput placeholder="Search site" />
<CommandList>
<CommandEmpty>
No
site
found.
</CommandEmpty>
<CommandGroup>
{sites.map(
(
site
) => (
<CommandItem
value={`${site.siteId}:${site.name}:${site.niceId}`}
key={
site.siteId
}
onSelect={() => {
form.setValue(
"siteId",
site.siteId
);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
field.value
? "opacity-100"
: "opacity-0"
)}
/>
{
site.name
}
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
<FormDescription>
This site will
provide connectivity
to the resource.
</FormDescription>
</FormItem>
)}
/>
</form>
</Form>
)}
@@ -775,8 +769,8 @@ export default function CreateResourceForm({
rel="noopener noreferrer"
>
<span>
Make sure to follow the full
guide
Learn how to configure TCP/UDP
resources
</span>
<SquareArrowOutUpRight size={14} />
</Link>

View File

@@ -486,7 +486,7 @@ export default function ReverseProxyTargets(props: {
onSubmit={addTargetForm.handleSubmit(addTarget)}
className="space-y-4"
>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-start">
{resource.http && (
<FormField
control={addTargetForm.control}
@@ -562,7 +562,7 @@ export default function ReverseProxyTargets(props: {
</FormItem>
)}
/>
<Button type="submit" variant="outlinePrimary">
<Button type="submit" variant="outlinePrimary" className="mt-8">
Add Target
</Button>
</div>

View File

@@ -322,14 +322,21 @@ export default function GeneralForm() {
}
onValueChange={(
val
) =>
) => {
setDomainType(
val ===
"basedomain"
? "basedomain"
: "subdomain"
)
}
);
form.setValue(
"isBaseDomain",
val ===
"basedomain"
? true
: false
);
}}
>
<FormControl>
<SelectTrigger>
@@ -359,7 +366,7 @@ export default function GeneralForm() {
Subdomain
</FormLabel>
<div className="flex">
<div className="w-1/2 mr-1">
<div className="w-1/2">
<FormField
control={
form.control
@@ -372,6 +379,7 @@ export default function GeneralForm() {
<FormControl>
<Input
{...field}
className="border-r-0 rounded-r-none"
/>
</FormControl>
<FormMessage />
@@ -401,7 +409,7 @@ export default function GeneralForm() {
}
>
<FormControl>
<SelectTrigger>
<SelectTrigger className="rounded-l-none">
<SelectValue />
</SelectTrigger>
</FormControl>

View File

@@ -130,7 +130,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
<OrgProvider org={org}>
<ResourceProvider resource={resource} authInfo={authInfo}>
<SidebarSettings sidebarNavItems={sidebarNavItems}>
<div className="mb-8">
<div className="mb-4">
<ResourceInfoBox />
</div>
{children}

View File

@@ -149,6 +149,7 @@ export default function CreateSiteForm({
setSiteDefaults(res.data.data);
}
});
await new Promise((resolve) => setTimeout(resolve, 200));
setLoadingPage(false);
};
@@ -270,7 +271,7 @@ PersistentKeepalive = 5`
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
return loadingPage ? (
<LoaderPlaceholder height="300px"/>
<LoaderPlaceholder height="300px" />
) : (
<div className="space-y-4">
<Form {...form}>
@@ -344,7 +345,6 @@ PersistentKeepalive = 5`
rel="noopener noreferrer"
>
<span>
{" "}
Learn how to install Newt on your system
</span>
<SquareArrowOutUpRight size={14} />
@@ -371,12 +371,16 @@ PersistentKeepalive = 5`
onOpenChange={setIsOpen}
className="space-y-2"
>
<div className="mx-auto">
<div className="mx-auto mb-2">
<CopyTextBox
text={newtConfig}
wrapText={false}
/>
</div>
<span className="text-sm text-muted-foreground">
You will only be able to see the
configuration once.
</span>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
<Button
@@ -418,10 +422,6 @@ PersistentKeepalive = 5`
</CollapsibleContent>
</Collapsible>
</div>
<span className="text-sm text-muted-foreground">
You will only be able to see the
configuration once.
</span>
</>
) : null}
</div>

View File

@@ -198,8 +198,7 @@ export default function VerifyEmailForm({
<FormMessage />
<FormDescription>
We sent a verification code to your
email address. Please enter the code
to verify your email address.
email address.
</FormDescription>
</FormItem>
)}

View File

@@ -78,7 +78,7 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
return (
<CredenzaClose className={cn("mb-3 md:mb-0", className)} {...props}>
<CredenzaClose className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)} {...props}>
{children}
</CredenzaClose>
);
@@ -168,7 +168,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
return (
<CredenzaFooter className={className} {...props}>
<CredenzaFooter className={cn("mt-8 md:mt-0", className)} {...props}>
{children}
</CredenzaFooter>
);

View File

@@ -19,7 +19,7 @@ export function SettingsSectionTitle({ children }: { children: React.ReactNode }
}
export function SettingsSectionDescription({ children }: { children: React.ReactNode }) {
return <p className="text-muted-foreground">{children}</p>
return <p className="text-muted-foreground text-sm">{children}</p>
}
export function SettingsSectionBody({ children }: { children: React.ReactNode }) {

View File

@@ -0,0 +1,53 @@
"use client";
import { cn } from "@app/lib/cn";
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
interface StrategyOption {
id: string;
title: string;
description: string;
}
interface StrategySelectProps {
options: StrategyOption[];
defaultValue?: string;
onChange?: (value: string) => void;
}
export function StrategySelect({
options,
defaultValue,
onChange
}: StrategySelectProps) {
return (
<RadioGroup
defaultValue={defaultValue}
onValueChange={onChange}
className="grid gap-4"
>
{options.map((option) => (
<label
key={option.id}
htmlFor={option.id}
className={cn(
"relative flex cursor-pointer rounded-lg border-2 p-4",
"data-[state=checked]:border-primary data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary"
)}
>
<RadioGroupItem
value={option.id}
id={option.id}
className="absolute left-4 top-5 h-4 w-4 border-primary text-primary"
/>
<div className="pl-7">
<div className="font-medium">{option.title}</div>
<div className="text-sm text-muted-foreground">
{option.description}
</div>
</div>
</label>
))}
</RadioGroup>
);
}

View File

@@ -20,8 +20,8 @@ const SelectTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between border-2 border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
"rounded-md"
"rounded-md",
className
)}
{...props}
>