minor visual enhancements

This commit is contained in:
miloschwartz 2025-03-01 17:45:38 -05:00
parent 89a59b25fc
commit 0e38f58a7f
No known key found for this signature in database
37 changed files with 1195 additions and 1154 deletions

View file

@ -184,7 +184,8 @@ const configSchema = z.object({
disable_signup_without_invite: z.boolean().optional(), disable_signup_without_invite: z.boolean().optional(),
disable_user_create_org: z.boolean().optional(), disable_user_create_org: z.boolean().optional(),
allow_raw_resources: z.boolean().optional(), allow_raw_resources: z.boolean().optional(),
allow_base_domain_resources: z.boolean().optional() allow_base_domain_resources: z.boolean().optional(),
allow_local_sites: z.boolean().optional()
}) })
.optional() .optional()
}); });

View file

@ -7,7 +7,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
@ -24,11 +24,11 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role"; import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
@ -40,13 +40,13 @@ type CreateRoleFormProps = {
const formSchema = z.object({ const formSchema = z.object({
name: z.string({ message: "Name is required" }).max(32), name: z.string({ message: "Name is required" }).max(32),
description: z.string().max(255).optional(), description: z.string().max(255).optional()
}); });
export default function CreateRoleForm({ export default function CreateRoleForm({
open, open,
setOpen, setOpen,
afterCreate, afterCreate
}: CreateRoleFormProps) { }: CreateRoleFormProps) {
const { org } = useOrgContext(); const { org } = useOrgContext();
@ -58,8 +58,8 @@ export default function CreateRoleForm({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
name: "", name: "",
description: "", description: ""
}, }
}); });
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
@ -70,7 +70,7 @@ export default function CreateRoleForm({
`/org/${org?.org.orgId}/role`, `/org/${org?.org.orgId}/role`,
{ {
name: values.name, name: values.name,
description: values.description, description: values.description
} as CreateRoleBody } as CreateRoleBody
) )
.catch((e) => { .catch((e) => {
@ -80,7 +80,7 @@ export default function CreateRoleForm({
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while creating the role." "An error occurred while creating the role."
), )
}); });
}); });
@ -88,7 +88,7 @@ export default function CreateRoleForm({
toast({ toast({
variant: "default", variant: "default",
title: "Role created", title: "Role created",
description: "The role has been successfully created.", description: "The role has been successfully created."
}); });
if (open) { if (open) {
@ -135,9 +135,7 @@ export default function CreateRoleForm({
<FormItem> <FormItem>
<FormLabel>Role Name</FormLabel> <FormLabel>Role Name</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -150,9 +148,7 @@ export default function CreateRoleForm({
<FormItem> <FormItem>
<FormLabel>Description</FormLabel> <FormLabel>Description</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -162,6 +158,9 @@ export default function CreateRoleForm({
</Form> </Form>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="submit" type="submit"
form="create-role-form" form="create-role-form"
@ -170,9 +169,6 @@ export default function CreateRoleForm({
> >
Create Role Create Role
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -7,7 +7,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -23,7 +23,7 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
@ -32,10 +32,10 @@ import {
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { RoleRow } from "./RolesTable"; import { RoleRow } from "./RolesTable";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
@ -47,14 +47,14 @@ type CreateRoleFormProps = {
}; };
const formSchema = z.object({ const formSchema = z.object({
newRoleId: z.string({ message: "New role is required" }), newRoleId: z.string({ message: "New role is required" })
}); });
export default function DeleteRoleForm({ export default function DeleteRoleForm({
open, open,
roleToDelete, roleToDelete,
setOpen, setOpen,
afterDelete, afterDelete
}: CreateRoleFormProps) { }: CreateRoleFormProps) {
const { org } = useOrgContext(); const { org } = useOrgContext();
@ -66,9 +66,9 @@ export default function DeleteRoleForm({
useEffect(() => { useEffect(() => {
async function fetchRoles() { async function fetchRoles() {
const res = await api const res = await api
.get<AxiosResponse<ListRolesResponse>>( .get<
`/org/${org?.org.orgId}/roles` AxiosResponse<ListRolesResponse>
) >(`/org/${org?.org.orgId}/roles`)
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
toast({ toast({
@ -77,7 +77,7 @@ export default function DeleteRoleForm({
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while fetching the roles" "An error occurred while fetching the roles"
), )
}); });
}); });
@ -96,8 +96,8 @@ export default function DeleteRoleForm({
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
newRoleId: "", newRoleId: ""
}, }
}); });
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
@ -106,8 +106,8 @@ export default function DeleteRoleForm({
const res = await api const res = await api
.delete(`/role/${roleToDelete.roleId}`, { .delete(`/role/${roleToDelete.roleId}`, {
data: { data: {
roleId: values.newRoleId, roleId: values.newRoleId
}, }
}) })
.catch((e) => { .catch((e) => {
toast({ toast({
@ -116,7 +116,7 @@ export default function DeleteRoleForm({
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while removing the role." "An error occurred while removing the role."
), )
}); });
}); });
@ -124,7 +124,7 @@ export default function DeleteRoleForm({
toast({ toast({
variant: "default", variant: "default",
title: "Role removed", title: "Role removed",
description: "The role has been successfully removed.", description: "The role has been successfully removed."
}); });
if (open) { if (open) {
@ -214,6 +214,9 @@ export default function DeleteRoleForm({
</div> </div>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="submit" type="submit"
form="remove-role-form" form="remove-role-form"
@ -222,9 +225,6 @@ export default function DeleteRoleForm({
> >
Remove Role Remove Role
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -37,7 +37,7 @@ import {
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Checkbox } from "@app/components/ui/checkbox"; import { Checkbox } from "@app/components/ui/checkbox";
@ -194,9 +194,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -340,6 +338,9 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
</div> </div>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="submit" type="submit"
form="invite-user-form" form="invite-user-form"
@ -348,9 +349,6 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
> >
Create Invitation Create Invitation
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -185,7 +185,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
<Link <Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`} href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
> >
<Button variant={"outline"} className="ml-2"> <Button variant={"outlinePrimary"} className="ml-2">
Manage Manage
<ArrowRight className="ml-2 w-4 h-4" /> <ArrowRight className="ml-2 w-4 h-4" />
</Button> </Button>

View file

@ -64,7 +64,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
</Breadcrumb> </Breadcrumb>
</div> </div>
<div className="space-y-0.5 select-none mb-6"> <div className="space-y-0.5 mb-6">
<h2 className="text-2xl font-bold tracking-tight"> <h2 className="text-2xl font-bold tracking-tight">
User {user?.email} User {user?.email}
</h2> </h2>

View file

@ -1,6 +1,13 @@
import { Metadata } from "next"; import { Metadata } from "next";
import { TopbarNav } from "@app/components/TopbarNav"; import { TopbarNav } from "@app/components/TopbarNav";
import { Cog, Combine, Link, Settings, Users, Waypoints } from "lucide-react"; import {
Cog,
Combine,
LinkIcon,
Settings,
Users,
Waypoints
} from "lucide-react";
import { Header } from "@app/components/Header"; import { Header } from "@app/components/Header";
import { verifySession } from "@app/lib/auth/verifySession"; import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -11,6 +18,14 @@ import { authCookieHeader } from "@app/lib/api/cookies";
import { cache } from "react"; import { cache } from "react";
import { GetOrgUserResponse } from "@server/routers/user"; import { GetOrgUserResponse } from "@server/routers/user";
import UserProvider from "@app/providers/UserProvider"; import UserProvider from "@app/providers/UserProvider";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@ -38,7 +53,7 @@ const topNavItems = [
{ {
title: "Shareable Links", title: "Shareable Links",
href: "/{orgId}/settings/share-links", href: "/{orgId}/settings/share-links",
icon: <Link className="h-4 w-4" /> icon: <LinkIcon className="h-4 w-4" />
}, },
{ {
title: "General", title: "General",
@ -95,19 +110,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return ( return (
<> <>
<div className="w-full border-b bg-card select-none sm:px-0 px-3 fixed top-0 z-10"> <div className="w-full bg-card sm:px-0 px-3 fixed top-0 z-10">
<div className="container mx-auto flex flex-col content-between"> <div className="border-b">
<div className="my-4"> <div className="container mx-auto flex flex-col content-between">
<UserProvider user={user}> <div className="my-4">
<Header orgId={params.orgId} orgs={orgs} /> <UserProvider user={user}>
</UserProvider> <Header orgId={params.orgId} orgs={orgs} />
</UserProvider>
</div>
<TopbarNav items={topNavItems} orgId={params.orgId} />
</div> </div>
<TopbarNav items={topNavItems} orgId={params.orgId} />
</div> </div>
</div> </div>
<div className="container mx-auto sm:px-0 px-3 pt-[165px]"> <div className="container mx-auto sm:px-0 px-3 pt-[155px]">
{children} <div className="container mx-auto sm:px-0 px-3">
{children}
</div>
</div> </div>
</> </>
); );

View file

@ -66,6 +66,7 @@ import CopyTextBox from "@app/components/CopyTextBox";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import { Label } from "@app/components/ui/label"; import { Label } from "@app/components/ui/label";
import { ListDomainsResponse } from "@server/routers/domain"; import { ListDomainsResponse } from "@server/routers/domain";
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
const createResourceFormSchema = z const createResourceFormSchema = z
.object({ .object({
@ -140,6 +141,7 @@ export default function CreateResourceForm({
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">( const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
"subdomain" "subdomain"
); );
const [loadingPage, setLoadingPage] = useState(true);
const form = useForm<CreateResourceFormValues>({ const form = useForm<CreateResourceFormValues>({
resolver: zodResolver(createResourceFormSchema), resolver: zodResolver(createResourceFormSchema),
@ -215,8 +217,16 @@ export default function CreateResourceForm({
} }
}; };
fetchSites(); const load = async () => {
fetchDomains(); setLoadingPage(true);
await fetchSites();
await fetchDomains();
setLoadingPage(false);
};
load();
}, [open]); }, [open]);
async function onSubmit(data: CreateResourceFormValues) { async function onSubmit(data: CreateResourceFormValues) {
@ -282,236 +292,482 @@ export default function CreateResourceForm({
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
{!showSnippets && ( {loadingPage ? (
<Form {...form} key={formKey}> <LoaderPlaceholder height="500px" />
<form ) : (
onSubmit={form.handleSubmit(onSubmit)} <div>
className="space-y-4" {!showSnippets && (
id="create-resource-form" <Form {...form} key={formKey}>
> <form
{!env.flags.allowRawResources || ( onSubmit={form.handleSubmit(
<FormField onSubmit
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>
)} )}
/> className="space-y-4"
)} id="create-resource-form"
>
<FormField {!env.flags.allowRawResources || (
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
This is display name for the
resource.
</FormDescription>
</FormItem>
)}
/>
{form.watch("http") &&
env.flags.allowBaseDomainResources && (
<div>
<RadioGroup
className="flex space-x-4"
defaultValue={domainType}
onValueChange={(val) => {
setDomainType(
val as any
);
form.setValue(
"isBaseDomain",
val === "basedomain"
);
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="subdomain"
id="r1"
/>
<Label htmlFor="r1">
Subdomain
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="basedomain"
id="r2"
/>
<Label htmlFor="r2">
Base Domain
</Label>
</div>
</RadioGroup>
</div>
)}
{form.watch("http") && (
<>
{domainType === "subdomain" ? (
<div className="w-fill space-y-2">
{!env.flags
.allowBaseDomainResources && (
<FormLabel>
Subdomain
</FormLabel>
)}
<div className="flex">
<div className="w-full mr-1">
<FormField
control={
form.control
}
name="subdomain"
render={({
field
}) => (
<FormControl>
<Input
{...field}
className="text-right"
placeholder="Enter subdomain"
/>
</FormControl>
)}
/>
</div>
<div className="max-w-1/2">
<FormField
control={
form.control
}
name="domainId"
render={({
field
}) => (
<FormItem>
<Select
onValueChange={
field.onChange
}
value={
field.value
}
defaultValue={
field.value
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{baseDomains.map(
(
option
) => (
<SelectItem
key={
option.domainId
}
value={
option.domainId
}
>
.
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
) : (
<FormField <FormField
control={form.control} control={form.control}
name="domainId" name="http"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<Select <div className="space-y-0.5">
onValueChange={ <FormLabel className="text-base">
field.onChange HTTP
} Resource
defaultValue={ </FormLabel>
field.value <FormDescription>
} Toggle if
{...field} this is an
> HTTP
<FormControl> resource or
<SelectTrigger> a raw
<SelectValue /> TCP/UDP
</SelectTrigger> resource.
</FormControl> </FormDescription>
<SelectContent> </div>
{baseDomains.map( <FormControl>
( <Switch
option checked={
) => ( field.value
<SelectItem }
key={ onCheckedChange={
option.domainId field.onChange
} }
value={ />
option.domainId </FormControl>
}
>
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
)} )}
</>
)}
{!form.watch("http") && ( {!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"
render={({ field }) => (
<FormItem>
<FormLabel>
Name
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{form.watch("http") &&
env.flags
.allowBaseDomainResources && (
<FormField
control={form.control}
name="isBaseDomain"
render={({ field }) => (
<FormItem>
<FormLabel>
Domain Type
</FormLabel>
<Select
value={
domainType
}
onValueChange={(
val
) =>
setDomainType(
val ===
"basedomain"
? "basedomain"
: "subdomain"
)
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="subdomain">
Subdomain
</SelectItem>
<SelectItem value="basedomain">
Base
Domain
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{form.watch("http") && (
<>
{domainType ===
"subdomain" ? (
<div className="w-fill space-y-2">
<FormLabel>
Subdomain
</FormLabel>
<div className="flex">
<div className="w-1/2 mr-1">
<FormField
control={
form.control
}
name="subdomain"
render={({
field
}) => (
<FormItem>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="w-1/2">
<FormField
control={
form.control
}
name="domainId"
render={({
field
}) => (
<FormItem>
<Select
onValueChange={
field.onChange
}
value={
field.value
}
defaultValue={
field.value
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{baseDomains.map(
(
option
) => (
<SelectItem
key={
option.domainId
}
value={
option.domainId
}
>
.
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
) : (
<FormField
control={
form.control
}
name="domainId"
render={({
field
}) => (
<FormItem>
<FormLabel>
Base
Domain
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
{...field}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{baseDomains.map(
(
option
) => (
<SelectItem
key={
option.domainId
}
value={
option.domainId
}
>
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
{!form.watch("http") && (
<>
<FormField
control={form.control}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
Protocol
</FormLabel>
<Select
value={
field.value
}
onValueChange={
field.onChange
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
TCP
</SelectItem>
<SelectItem value="udp">
UDP
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>
Port Number
</FormLabel>
<FormControl>
<Input
type="number"
value={
field.value ??
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
? parseInt(
e
.target
.value
)
: null
)
}
/>
</FormControl>
<FormMessage />
<FormDescription>
The external
port number
to proxy
requests.
</FormDescription>
</FormItem>
)}
/>
</>
)}
<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>
)}
{showSnippets && (
<div>
<div className="flex items-start space-x-4 mb-6 last:mb-0">
<div className="flex-grow">
<h3 className="text-lg font-semibold mb-3">
Traefik: Add Entrypoints
</h3>
<CopyTextBox
text={`entryPoints:
${form.getValues("protocol")}-${form.getValues("proxyPort")}:
address: ":${form.getValues("proxyPort")}/${form.getValues("protocol")}"`}
wrapText={false}
/>
</div>
</div>
<div className="flex items-start space-x-4 mb-6 last:mb-0">
<div className="flex-grow">
<h3 className="text-lg font-semibold mb-3">
Gerbil: Expose Ports in
Docker Compose
</h3>
<CopyTextBox
text={`ports:
- ${form.getValues("proxyPort")}:${form.getValues("proxyPort")}${form.getValues("protocol") === "tcp" ? "" : "/" + form.getValues("protocol")}`}
wrapText={false}
/>
</div>
</div>
<Link <Link
className="text-sm text-primary flex items-center gap-1" className="text-sm text-primary flex items-center gap-1"
href="https://docs.fossorial.io/Getting%20Started/tcp-udp" href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
@ -519,233 +775,20 @@ export default function CreateResourceForm({
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<span> <span>
Learn how to configure TCP/UDP Make sure to follow the full
resources guide
</span> </span>
<SquareArrowOutUpRight size={14} /> <SquareArrowOutUpRight size={14} />
</Link> </Link>
)}
{!form.watch("http") && (
<>
<FormField
control={form.control}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
Protocol
</FormLabel>
<Select
value={field.value}
onValueChange={
field.onChange
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
TCP
</SelectItem>
<SelectItem value="udp">
UDP
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
<FormDescription>
The protocol to use
for the resource.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>
Port Number
</FormLabel>
<FormControl>
<Input
type="number"
value={
field.value ??
""
}
onChange={(e) =>
field.onChange(
e.target
.value
? parseInt(
e
.target
.value
)
: null
)
}
/>
</FormControl>
<FormMessage />
<FormDescription>
The port number to
proxy requests to
(required for
non-HTTP resources).
</FormDescription>
</FormItem>
)}
/>
</>
)}
<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>
)}
{showSnippets && (
<div>
<div className="flex items-start space-x-4 mb-6 last:mb-0">
<div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
1
</div> </div>
<div className="flex-grow"> )}
<h3 className="text-lg font-semibold mb-3">
Traefik: Add Entrypoints
</h3>
<CopyTextBox
text={`entryPoints:
${form.getValues("protocol")}-${form.getValues("proxyPort")}:
address: ":${form.getValues("proxyPort")}/${form.getValues("protocol")}"`}
wrapText={false}
/>
</div>
</div>
<div className="flex items-start space-x-4 mb-6 last:mb-0">
<div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
2
</div>
<div className="flex-grow">
<h3 className="text-lg font-semibold mb-3">
Gerbil: Expose Ports in Docker
Compose
</h3>
<CopyTextBox
text={`ports:
- ${form.getValues("proxyPort")}:${form.getValues("proxyPort")}${form.getValues("protocol") === "tcp" ? "" : "/" + form.getValues("protocol")}`}
wrapText={false}
/>
</div>
</div>
<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>
Make sure to follow the full guide
</span>
<SquareArrowOutUpRight size={14} />
</Link>
</div> </div>
)} )}
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
{!showSnippets && ( {!showSnippets && (
<Button <Button
type="submit" type="submit"
@ -765,10 +808,6 @@ export default function CreateResourceForm({
Go to Resource Go to Resource
</Button> </Button>
)} )}
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -233,7 +233,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
<Link <Link
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`} href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
> >
<Button variant={"outline"} className="ml-2"> <Button variant={"outlinePrimary"} className="ml-2">
Edit Edit
<ArrowRight className="ml-2 w-4 h-4" /> <ArrowRight className="ml-2 w-4 h-4" />
</Button> </Button>

View file

@ -8,7 +8,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
@ -24,22 +24,22 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { Resource } from "@server/db/schema"; import { Resource } from "@server/db/schema";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
const setPasswordFormSchema = z.object({ const setPasswordFormSchema = z.object({
password: z.string().min(4).max(100), password: z.string().min(4).max(100)
}); });
type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>; type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>;
const defaultValues: Partial<SetPasswordFormValues> = { const defaultValues: Partial<SetPasswordFormValues> = {
password: "", password: ""
}; };
type SetPasswordFormProps = { type SetPasswordFormProps = {
@ -53,7 +53,7 @@ export default function SetResourcePasswordForm({
open, open,
setOpen, setOpen,
resourceId, resourceId,
onSetPassword, onSetPassword
}: SetPasswordFormProps) { }: SetPasswordFormProps) {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
@ -61,7 +61,7 @@ export default function SetResourcePasswordForm({
const form = useForm<SetPasswordFormValues>({ const form = useForm<SetPasswordFormValues>({
resolver: zodResolver(setPasswordFormSchema), resolver: zodResolver(setPasswordFormSchema),
defaultValues, defaultValues
}); });
useEffect(() => { useEffect(() => {
@ -76,7 +76,7 @@ export default function SetResourcePasswordForm({
setLoading(true); setLoading(true);
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, { api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, {
password: data.password, password: data.password
}) })
.catch((e) => { .catch((e) => {
toast({ toast({
@ -85,14 +85,14 @@ export default function SetResourcePasswordForm({
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while setting the resource password" "An error occurred while setting the resource password"
), )
}); });
}) })
.then(() => { .then(() => {
toast({ toast({
title: "Resource password set", title: "Resource password set",
description: description:
"The resource password has been set successfully", "The resource password has been set successfully"
}); });
if (onSetPassword) { if (onSetPassword) {
@ -153,6 +153,9 @@ export default function SetResourcePasswordForm({
</Form> </Form>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="submit" type="submit"
form="set-password-form" form="set-password-form"
@ -161,9 +164,6 @@ export default function SetResourcePasswordForm({
> >
Enable Password Protection Enable Password Protection
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -8,7 +8,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
@ -24,27 +24,27 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { Resource } from "@server/db/schema"; import { Resource } from "@server/db/schema";
import { import {
InputOTP, InputOTP,
InputOTPGroup, InputOTPGroup,
InputOTPSlot, InputOTPSlot
} from "@app/components/ui/input-otp"; } from "@app/components/ui/input-otp";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
const setPincodeFormSchema = z.object({ const setPincodeFormSchema = z.object({
pincode: z.string().length(6), pincode: z.string().length(6)
}); });
type SetPincodeFormValues = z.infer<typeof setPincodeFormSchema>; type SetPincodeFormValues = z.infer<typeof setPincodeFormSchema>;
const defaultValues: Partial<SetPincodeFormValues> = { const defaultValues: Partial<SetPincodeFormValues> = {
pincode: "", pincode: ""
}; };
type SetPincodeFormProps = { type SetPincodeFormProps = {
@ -58,7 +58,7 @@ export default function SetResourcePincodeForm({
open, open,
setOpen, setOpen,
resourceId, resourceId,
onSetPincode, onSetPincode
}: SetPincodeFormProps) { }: SetPincodeFormProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -66,7 +66,7 @@ export default function SetResourcePincodeForm({
const form = useForm<SetPincodeFormValues>({ const form = useForm<SetPincodeFormValues>({
resolver: zodResolver(setPincodeFormSchema), resolver: zodResolver(setPincodeFormSchema),
defaultValues, defaultValues
}); });
useEffect(() => { useEffect(() => {
@ -81,7 +81,7 @@ export default function SetResourcePincodeForm({
setLoading(true); setLoading(true);
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, { api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, {
pincode: data.pincode, pincode: data.pincode
}) })
.catch((e) => { .catch((e) => {
toast({ toast({
@ -89,15 +89,15 @@ export default function SetResourcePincodeForm({
title: "Error setting resource PIN code", title: "Error setting resource PIN code",
description: formatAxiosError( description: formatAxiosError(
e, e,
"An error occurred while setting the resource PIN code", "An error occurred while setting the resource PIN code"
), )
}); });
}) })
.then(() => { .then(() => {
toast({ toast({
title: "Resource PIN code set", title: "Resource PIN code set",
description: description:
"The resource pincode has been set successfully", "The resource pincode has been set successfully"
}); });
if (onSetPincode) { if (onSetPincode) {
@ -181,6 +181,9 @@ export default function SetResourcePincodeForm({
</Form> </Form>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="submit" type="submit"
form="set-pincode-form" form="set-pincode-form"
@ -189,9 +192,6 @@ export default function SetResourcePincodeForm({
> >
Enable PIN Code Protection Enable PIN Code Protection
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -38,7 +38,8 @@ import {
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionFooter SettingsSectionFooter,
SettingsSectionForm
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { InfoPopup } from "@app/components/ui/info-popup"; import { InfoPopup } from "@app/components/ui/info-popup";
@ -438,6 +439,7 @@ export default function ResourceAuthenticationPage() {
setActiveRolesTagIndex setActiveRolesTagIndex
} }
placeholder="Select a role" placeholder="Select a role"
size="sm"
tags={ tags={
usersRolesForm.getValues() usersRolesForm.getValues()
.roles .roles
@ -466,14 +468,6 @@ export default function ResourceAuthenticationPage() {
true true
} }
sortTags={true} sortTags={true}
styleClasses={{
tag: {
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
},
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer:
"bg-transparent p-2"
}}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@ -504,6 +498,7 @@ export default function ResourceAuthenticationPage() {
usersRolesForm.getValues() usersRolesForm.getValues()
.users .users
} }
size="sm"
setTags={( setTags={(
newUsers newUsers
) => { ) => {
@ -528,14 +523,6 @@ export default function ResourceAuthenticationPage() {
true true
} }
sortTags={true} sortTags={true}
styleClasses={{
tag: {
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
},
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer:
"bg-transparent p-2"
}}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@ -582,7 +569,7 @@ export default function ResourceAuthenticationPage() {
</span> </span>
</div> </div>
<Button <Button
variant="outline" variant="outlinePrimary"
onClick={ onClick={
authInfo.password authInfo.password
? removeResourcePassword ? removeResourcePassword
@ -608,7 +595,7 @@ export default function ResourceAuthenticationPage() {
</span> </span>
</div> </div>
<Button <Button
variant="outline" variant="outlinePrimary"
onClick={ onClick={
authInfo.pincode authInfo.pincode
? removeResourcePincode ? removeResourcePincode
@ -664,6 +651,7 @@ export default function ResourceAuthenticationPage() {
activeTagIndex={ activeTagIndex={
activeEmailTagIndex activeEmailTagIndex
} }
size={"sm"}
validateTag={( validateTag={(
tag tag
) => { ) => {
@ -708,14 +696,6 @@ export default function ResourceAuthenticationPage() {
false false
} }
sortTags={true} sortTags={true}
styleClasses={{
tag: {
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
},
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer:
"bg-transparent p-2"
}}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>

View file

@ -241,10 +241,7 @@ export default function ReverseProxyTargets(props: {
>(`/resource/${params.resourceId}/target`, data); >(`/resource/${params.resourceId}/target`, data);
target.targetId = res.data.data.targetId; target.targetId = res.data.data.targetId;
} else if (target.updated) { } else if (target.updated) {
await api.post( await api.post(`/target/${target.targetId}`, data);
`/target/${target.targetId}`,
data
);
} }
setTargets([ setTargets([
@ -261,9 +258,7 @@ export default function ReverseProxyTargets(props: {
for (const targetId of targetsToRemove) { for (const targetId of targetsToRemove) {
await api.delete(`/target/${targetId}`); await api.delete(`/target/${targetId}`);
setTargets( setTargets(targets.filter((t) => t.targetId !== targetId));
targets.filter((t) => t.targetId !== targetId)
);
} }
toast({ toast({
@ -459,7 +454,8 @@ export default function ReverseProxyTargets(props: {
SSL Configuration SSL Configuration
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
Setup SSL to secure your connections with Let's Encrypt certificates Setup SSL to secure your connections with Let's
Encrypt certificates
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -490,7 +486,7 @@ export default function ReverseProxyTargets(props: {
onSubmit={addTargetForm.handleSubmit(addTarget)} onSubmit={addTargetForm.handleSubmit(addTarget)}
className="space-y-4" className="space-y-4"
> >
<div className="grid grid-cols-2 md:grid-cols-3 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
{resource.http && ( {resource.http && (
<FormField <FormField
control={addTargetForm.control} control={addTargetForm.control}
@ -545,18 +541,6 @@ export default function ReverseProxyTargets(props: {
<Input id="ip" {...field} /> <Input id="ip" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
{site?.type === "newt" ? (
<FormDescription>
This is the IP or hostname
of the target service on
your network.
</FormDescription>
) : site?.type === "wireguard" ? (
<FormDescription>
This is the IP of the
WireGuard peer.
</FormDescription>
) : null}
</FormItem> </FormItem>
)} )}
/> />
@ -575,26 +559,13 @@ export default function ReverseProxyTargets(props: {
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
{site?.type === "newt" ? (
<FormDescription>
This is the port of the
target service on your
network.
</FormDescription>
) : site?.type === "wireguard" ? (
<FormDescription>
This is the port exposed on
an address on the WireGuard
network.
</FormDescription>
) : null}
</FormItem> </FormItem>
)} )}
/> />
<Button type="submit" variant="outlinePrimary">
Add Target
</Button>
</div> </div>
<Button type="submit" variant="outline">
Add Target
</Button>
</form> </form>
</Form> </Form>

View file

@ -129,6 +129,7 @@ export default function GeneralForm() {
ListDomainsResponse["domains"] ListDomainsResponse["domains"]
>([]); >([]);
const [loadingPage, setLoadingPage] = useState(true);
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">( const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
resource.isBaseDomain ? "basedomain" : "subdomain" resource.isBaseDomain ? "basedomain" : "subdomain"
); );
@ -184,8 +185,14 @@ export default function GeneralForm() {
} }
}; };
fetchDomains(); const load = async () => {
fetchSites(); await fetchDomains();
await fetchSites();
setLoadingPage(false);
};
load();
}, []); }, []);
async function onSubmit(data: GeneralFormValues) { async function onSubmit(data: GeneralFormValues) {
@ -263,391 +270,399 @@ export default function GeneralForm() {
} }
return ( return (
<SettingsContainer> !loadingPage && (
<SettingsSection> <SettingsContainer>
<SettingsSectionHeader> <SettingsSection>
<SettingsSectionTitle> <SettingsSectionHeader>
General Settings <SettingsSectionTitle>
</SettingsSectionTitle> General Settings
<SettingsSectionDescription> </SettingsSectionTitle>
Configure the general settings for this resource <SettingsSectionDescription>
</SettingsSectionDescription> Configure the general settings for this resource
</SettingsSectionHeader> </SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form} key={formKey}> <Form {...form} key={formKey}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4" className="grid grid-cols-1 md:grid-cols-2 gap-4"
id="general-settings-form" id="general-settings-form"
> >
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
This is the display name of the
resource.
</FormDescription>
</FormItem>
)}
/>
{resource.http && (
<>
{env.flags.allowBaseDomainResources && (
<div>
<RadioGroup
className="flex space-x-4"
defaultValue={domainType}
onValueChange={(val) => {
setDomainType(
val as any
);
form.setValue(
"isBaseDomain",
val === "basedomain"
);
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="subdomain"
id="r1"
/>
<Label htmlFor="r1">
Subdomain
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="basedomain"
id="r2"
/>
<Label htmlFor="r2">
Base Domain
</Label>
</div>
</RadioGroup>
</div>
)}
{domainType === "subdomain" ? (
<div className="w-fill space-y-2">
{!env.flags
.allowBaseDomainResources && (
<FormLabel>
Subdomain
</FormLabel>
)}
<div className="flex">
<div className="w-full mr-1">
<FormField
control={
form.control
}
name="subdomain"
render={({
field
}) => (
<FormItem>
<FormControl>
<Input
{...field}
className="text-right"
placeholder="Enter subdomain"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="max-w-1/2">
<FormField
control={
form.control
}
name="domainId"
render={({
field
}) => (
<FormItem>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
value={
field.value
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{baseDomains.map(
(
option
) => (
<SelectItem
key={
option.domainId
}
value={
option.domainId
}
>
.
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
) : (
<FormField
control={form.control}
name="domainId"
render={({ field }) => (
<FormItem>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value ||
baseDomains[0]
?.domainId
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{baseDomains.map(
(
option
) => (
<SelectItem
key={
option.domainId
}
value={
option.domainId
}
>
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
{!resource.http && (
<FormField <FormField
control={form.control} control={form.control}
name="proxyPort" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>Name</FormLabel>
Port Number
</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
type="number"
value={
field.value ?? ""
}
onChange={(e) =>
field.onChange(
e.target.value
? parseInt(
e
.target
.value
)
: null
)
}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription>
This is the port that will
be used to access the
resource.
</FormDescription>
</FormItem> </FormItem>
)} )}
/> />
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter> {resource.http && (
<Button <>
type="submit" {env.flags
loading={saveLoading} .allowBaseDomainResources && (
disabled={saveLoading} <FormField
form="general-settings-form" control={form.control}
> name="isBaseDomain"
Save Settings render={({ field }) => (
</Button> <FormItem>
</SettingsSectionFooter> <FormLabel>
</SettingsSection> Domain Type
</FormLabel>
<Select
value={
domainType
}
onValueChange={(
val
) =>
setDomainType(
val ===
"basedomain"
? "basedomain"
: "subdomain"
)
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="subdomain">
Subdomain
</SelectItem>
<SelectItem value="basedomain">
Base
Domain
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<SettingsSection> <div className="col-span-2">
<SettingsSectionHeader> {domainType === "subdomain" ? (
<SettingsSectionTitle> <div className="w-fill space-y-2">
Transfer Resource <FormLabel>
</SettingsSectionTitle> Subdomain
<SettingsSectionDescription> </FormLabel>
Transfer this resource to a different site <div className="flex">
</SettingsSectionDescription> <div className="w-1/2 mr-1">
</SettingsSectionHeader> <FormField
control={
<SettingsSectionBody> form.control
<SettingsSectionForm> }
<Form {...transferForm}> name="subdomain"
<form render={({
onSubmit={transferForm.handleSubmit(onTransfer)} field
className="space-y-4" }) => (
id="transfer-form" <FormItem>
> <FormControl>
<FormField <Input
control={transferForm.control} {...field}
name="siteId" />
render={({ field }) => ( </FormControl>
<FormItem> <FormMessage />
<FormLabel> </FormItem>
Destination Site )}
</FormLabel> />
<Popover </div>
open={open} <div className="w-1/2">
onOpenChange={setOpen} <FormField
> control={
<PopoverTrigger asChild> form.control
<FormControl> }
<Button name="domainId"
variant="outline" render={({
role="combobox" field
className={cn( }) => (
"w-full justify-between", <FormItem>
!field.value && <Select
"text-muted-foreground" onValueChange={
)} field.onChange
> }
{field.value defaultValue={
? 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="w-full p-0">
<Command>
<CommandInput
placeholder="Search sites"
className="h-9"
/>
<CommandEmpty>
No sites found.
</CommandEmpty>
<CommandGroup>
{sites.map(
(site) => (
<CommandItem
value={`${site.name}:${site.siteId}`}
key={
site.siteId
}
onSelect={() => {
transferForm.setValue(
"siteId",
site.siteId
);
setOpen(
false
);
}}
>
{
site.name
}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
site.siteId ===
field.value field.value
? "opacity-100" }
: "opacity-0" value={
)} field.value
/> }
</CommandItem> >
) <FormControl>
)} <SelectTrigger>
</CommandGroup> <SelectValue />
</Command> </SelectTrigger>
</PopoverContent> </FormControl>
</Popover> <SelectContent>
<FormMessage /> {baseDomains.map(
</FormItem> (
option
) => (
<SelectItem
key={
option.domainId
}
value={
option.domainId
}
>
.
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
) : (
<FormField
control={form.control}
name="domainId"
render={({ field }) => (
<FormItem>
<FormLabel>
Base Domain
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value ||
baseDomains[0]
?.domainId
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{baseDomains.map(
(
option
) => (
<SelectItem
key={
option.domainId
}
value={
option.domainId
}
>
{
option.baseDomain
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</>
)} )}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter> {!resource.http && (
<Button <FormField
type="submit" control={form.control}
loading={transferLoading} name="proxyPort"
disabled={transferLoading} render={({ field }) => (
form="transfer-form" <FormItem>
> <FormLabel>
Transfer Resource Port Number
</Button> </FormLabel>
</SettingsSectionFooter> <FormControl>
</SettingsSection> <Input
</SettingsContainer> type="number"
value={
field.value ??
""
}
onChange={(e) =>
field.onChange(
e.target
.value
? parseInt(
e
.target
.value
)
: null
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={saveLoading}
disabled={saveLoading}
form="general-settings-form"
>
Save Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Transfer Resource
</SettingsSectionTitle>
<SettingsSectionDescription>
Transfer this resource to a different site
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...transferForm}>
<form
onSubmit={transferForm.handleSubmit(
onTransfer
)}
className="space-y-4"
id="transfer-form"
>
<FormField
control={transferForm.control}
name="siteId"
render={({ field }) => (
<FormItem>
<FormLabel>
Destination Site
</FormLabel>
<Popover
open={open}
onOpenChange={setOpen}
>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full 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="w-full p-0">
<Command>
<CommandInput
placeholder="Search sites"
className="h-9"
/>
<CommandEmpty>
No sites found.
</CommandEmpty>
<CommandGroup>
{sites.map(
(site) => (
<CommandItem
value={`${site.name}:${site.siteId}`}
key={
site.siteId
}
onSelect={() => {
transferForm.setValue(
"siteId",
site.siteId
);
setOpen(
false
);
}}
>
{
site.name
}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
site.siteId ===
field.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
)
)}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={transferLoading}
disabled={transferLoading}
form="transfer-form"
>
Transfer Resource
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
)
); );
} }

View file

@ -94,7 +94,7 @@ enum RuleAction {
enum RuleMatch { enum RuleMatch {
PATH = "Path", PATH = "Path",
IP = "IP", IP = "IP",
CIDR = "IP Range", CIDR = "IP Range"
} }
export default function ResourceRules(props: { export default function ResourceRules(props: {
@ -623,7 +623,7 @@ export default function ResourceRules(props: {
onSubmit={addRuleForm.handleSubmit(addRule)} onSubmit={addRuleForm.handleSubmit(addRule)}
className="space-y-4" className="space-y-4"
> >
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
<FormField <FormField
control={addRuleForm.control} control={addRuleForm.control}
name="action" name="action"
@ -711,14 +711,14 @@ export default function ResourceRules(props: {
</FormItem> </FormItem>
)} )}
/> />
<Button
type="submit"
variant="outlinePrimary"
disabled={!rulesEnabled}
>
Add Rule
</Button>
</div> </div>
<Button
type="submit"
variant="outline"
disabled={!rulesEnabled}
>
Add Rule
</Button>
</form> </form>
</Form> </Form>
<TableContainer> <TableContainer>

View file

@ -152,13 +152,15 @@ export default function CreateShareLinkForm({
if (res?.status === 200) { if (res?.status === 200) {
setResources( setResources(
res.data.data.resources.filter((r) => { res.data.data.resources
return r.http; .filter((r) => {
}).map((r) => ({ return r.http;
resourceId: r.resourceId, })
name: r.name, .map((r) => ({
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/` resourceId: r.resourceId,
})) name: r.name,
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`
}))
); );
} }
} }
@ -274,7 +276,7 @@ export default function CreateShareLinkForm({
name="resourceId" name="resourceId"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-col"> <FormItem className="flex flex-col">
<FormLabel className="mb-2"> <FormLabel>
Resource Resource
</FormLabel> </FormLabel>
<Popover> <Popover>
@ -318,9 +320,7 @@ export default function CreateShareLinkForm({
r r
) => ( ) => (
<CommandItem <CommandItem
value={ value={`${r.name}:${r.resourceId}`}
`${r.name}:${r.resourceId}`
}
key={ key={
r.resourceId r.resourceId
} }
@ -369,13 +369,11 @@ export default function CreateShareLinkForm({
name="title" name="title"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<Label> <FormLabel>
Title (optional) Title (optional)
</Label> </FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -383,66 +381,68 @@ export default function CreateShareLinkForm({
/> />
<div className="space-y-4"> <div className="space-y-4">
<Label>Expire In</Label> <div className="space-y-2">
<div className="grid grid-cols-2 gap-4 mt-2"> <FormLabel>Expire In</FormLabel>
<FormField <div className="grid grid-cols-2 gap-4">
control={form.control} <FormField
name="timeUnit" control={form.control}
render={({ field }) => ( name="timeUnit"
<FormItem> render={({ field }) => (
<Select <FormItem>
onValueChange={ <Select
field.onChange onValueChange={
} field.onChange
defaultValue={field.value.toString()} }
> defaultValue={field.value.toString()}
<FormControl> >
<SelectTrigger> <FormControl>
<SelectValue placeholder="Select duration" /> <SelectTrigger>
</SelectTrigger> <SelectValue placeholder="Select duration" />
</FormControl> </SelectTrigger>
<SelectContent> </FormControl>
{timeUnits.map( <SelectContent>
( {timeUnits.map(
option (
) => ( option
<SelectItem ) => (
key={ <SelectItem
option.unit key={
} option.unit
value={ }
option.unit value={
} option.unit
> }
{ >
option.name {
} option.name
</SelectItem> }
) </SelectItem>
)} )
</SelectContent> )}
</Select> </SelectContent>
<FormMessage /> </Select>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
/>
<FormField <FormField
control={form.control} control={form.control}
name="timeValue" name="timeValue"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input <Input
type="number" type="number"
min={1} min={1}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -552,6 +552,9 @@ export default function CreateShareLinkForm({
</div> </div>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="button" type="button"
onClick={form.handleSubmit(onSubmit)} onClick={form.handleSubmit(onSubmit)}
@ -560,9 +563,6 @@ export default function CreateShareLinkForm({
> >
Create Link Create Link
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -273,7 +273,21 @@ export default function ShareLinksTable({
} }
return "Never"; return "Never";
} }
},
{
id: "delete",
cell: ({ row }) => (
<div className="flex items-center justify-end space-x-2">
<Button
variant="outlinePrimary"
onClick={() => deleteSharelink(row.original.accessTokenId)}
>
Delete
</Button>
</div>
)
} }
]; ];
return ( return (

View file

@ -41,6 +41,7 @@ import Link from "next/link";
import { import {
ArrowUpRight, ArrowUpRight,
ChevronsUpDown, ChevronsUpDown,
Loader2,
SquareArrowOutUpRight SquareArrowOutUpRight
} from "lucide-react"; } from "lucide-react";
import { import {
@ -48,6 +49,7 @@ import {
CollapsibleContent, CollapsibleContent,
CollapsibleTrigger CollapsibleTrigger
} from "@app/components/ui/collapsible"; } from "@app/components/ui/collapsible";
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
const createSiteFormSchema = z.object({ const createSiteFormSchema = z.object({
name: z name: z
@ -97,6 +99,8 @@ export default function CreateSiteForm({
const [siteDefaults, setSiteDefaults] = const [siteDefaults, setSiteDefaults] =
useState<PickSiteDefaultsResponse | null>(null); useState<PickSiteDefaultsResponse | null>(null);
const [loadingPage, setLoadingPage] = useState(true);
const handleCheckboxChange = (checked: boolean) => { const handleCheckboxChange = (checked: boolean) => {
// setChecked?.(checked); // setChecked?.(checked);
setIsChecked(checked); setIsChecked(checked);
@ -121,27 +125,35 @@ export default function CreateSiteForm({
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
// reset all values const load = async () => {
setLoading?.(false); setLoadingPage(true);
setIsLoading(false); // reset all values
form.reset(); setLoading?.(false);
setChecked?.(false); setIsLoading(false);
setKeypair(null); form.reset();
setSiteDefaults(null); setChecked?.(false);
setKeypair(null);
setSiteDefaults(null);
const generatedKeypair = generateKeypair(); const generatedKeypair = generateKeypair();
setKeypair(generatedKeypair); setKeypair(generatedKeypair);
api.get(`/org/${orgId}/pick-site-defaults`) await api
.catch((e) => { .get(`/org/${orgId}/pick-site-defaults`)
// update the default value of the form to be local method .catch((e) => {
form.setValue("method", "local"); // update the default value of the form to be local method
}) form.setValue("method", "local");
.then((res) => { })
if (res && res.status === 200) { .then((res) => {
setSiteDefaults(res.data.data); if (res && res.status === 200) {
} setSiteDefaults(res.data.data);
}); }
});
setLoadingPage(false);
};
load();
}, [open]); }, [open]);
async function onSubmit(data: CreateSiteFormValues) { async function onSubmit(data: CreateSiteFormValues) {
@ -257,7 +269,9 @@ PersistentKeepalive = 5`
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`; const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
return ( return loadingPage ? (
<LoaderPlaceholder height="300px"/>
) : (
<div className="space-y-4"> <div className="space-y-4">
<Form {...form}> <Form {...form}>
<form <form
@ -276,8 +290,7 @@ PersistentKeepalive = 5`
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
This is the the display name for the This is the the display name for the site.
site.
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}

View file

@ -58,6 +58,9 @@ export default function CreateSiteFormModal({
</div> </div>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="submit" type="submit"
form="create-site-form" form="create-site-form"
@ -69,9 +72,6 @@ export default function CreateSiteFormModal({
> >
Create Site Create Site
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -268,7 +268,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<Link <Link
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`} href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
> >
<Button variant={"outline"} className="ml-2"> <Button variant={"outlinePrimary"} className="ml-2">
Edit Edit
<ArrowRight className="ml-2 w-4 h-4" /> <ArrowRight className="ml-2 w-4 h-4" />
</Button> </Button>

View file

@ -68,7 +68,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
<SiteProvider site={site}> <SiteProvider site={site}>
<SidebarSettings sidebarNavItems={sidebarNavItems}> <SidebarSettings sidebarNavItems={sidebarNavItems}>
<div className="mb-8"> <div className="mb-4">
<SiteInfoCard /> <SiteInfoCard />
</div> </div>
{children} {children}

View file

@ -22,7 +22,7 @@
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%; --destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 85%; --border: 20 5.9% 85%;
--input: 20 5.9% 85%; --input: 20 5.9% 80%;
--ring: 24.6 95% 53.1%; --ring: 24.6 95% 53.1%;
--radius: 0.75rem; --radius: 0.75rem;
--chart-1: 12 76% 61%; --chart-1: 12 76% 61%;
@ -50,7 +50,7 @@
--destructive: 0 72.2% 50.6%; --destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%; --destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 25.0%; --border: 12 6.5% 25.0%;
--input: 12 6.5% 25.0%; --input: 12 6.5% 30.0%;
--ring: 20.5 90.2% 48.2%; --ring: 20.5 90.2% 48.2%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;

View file

@ -37,11 +37,11 @@ export default async function RootLayout({
> >
<EnvProvider env={pullEnv()}> <EnvProvider env={pullEnv()}>
{/* Main content */} {/* Main content */}
<div className="flex-grow">{children}</div> <div className="flex-grow pb-3 md:pb-0">{children}</div>
{/* Footer */} {/* Footer */}
<footer className="w-full mt-12 py-3 mb-6 px-4"> <footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600 select-none"> <div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600">
<div className="flex items-center space-x-2 whitespace-nowrap"> <div className="flex items-center space-x-2 whitespace-nowrap">
<span>Pangolin</span> <span>Pangolin</span>
</div> </div>

View file

@ -7,7 +7,7 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { import {
@ -15,14 +15,14 @@ import {
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { import {
InviteUserBody, InviteUserBody,
InviteUserResponse, InviteUserResponse,
ListUsersResponse, ListUsersResponse
} from "@server/routers/user"; } from "@server/routers/user";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import React, { useState } from "react"; import React, { useState } from "react";
@ -37,7 +37,7 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaFooter, CredenzaFooter,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { Description } from "@radix-ui/react-toast"; import { Description } from "@radix-ui/react-toast";
@ -61,7 +61,7 @@ export default function InviteUserForm({
title, title,
onConfirm, onConfirm,
buttonText, buttonText,
dialog, dialog
}: InviteUserFormProps) { }: InviteUserFormProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -69,15 +69,15 @@ export default function InviteUserForm({
const formSchema = z.object({ const formSchema = z.object({
string: z.string().refine((val) => val === string, { string: z.string().refine((val) => val === string, {
message: "Invalid confirmation", message: "Invalid confirmation"
}), })
}); });
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
string: "", string: ""
}, }
}); });
function reset() { function reset() {
@ -128,6 +128,9 @@ export default function InviteUserForm({
</Form> </Form>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
<Button <Button
type="submit" type="submit"
form="confirm-delete-form" form="confirm-delete-form"
@ -136,9 +139,6 @@ export default function InviteUserForm({
> >
{buttonText} {buttonText}
</Button> </Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -155,7 +155,7 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
// ); // );
return ( return (
<div className={cn("px-0 mb-4", className)} {...props}> <div className={cn("px-0 mb-4 space-y-4", className)} {...props}>
{children} {children}
</div> </div>
); );

View file

@ -29,10 +29,8 @@ import {
CredenzaTitle CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { CheckCircle2 } from "lucide-react"; import { CheckCircle2 } from "lucide-react";
const disableSchema = z.object({ const disableSchema = z.object({
@ -152,36 +150,7 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
Authenticator Code Authenticator Code
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<InputOTP <Input {...field} />
maxLength={6}
{...field}
pattern={
REGEXP_ONLY_DIGITS_AND_CHARS
}
>
<InputOTPGroup>
<InputOTPSlot
index={0}
/>
<InputOTPSlot
index={1}
/>
<InputOTPSlot
index={2}
/>
</InputOTPGroup>
<InputOTPGroup>
<InputOTPSlot
index={3}
/>
<InputOTPSlot
index={4}
/>
<InputOTPSlot
index={5}
/>
</InputOTPGroup>
</InputOTP>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -210,6 +179,9 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
)} )}
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
{step === "password" && ( {step === "password" && (
<Button <Button
type="submit" type="submit"
@ -220,9 +192,6 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
Disable 2FA Disable 2FA
</Button> </Button>
)} )}
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -36,7 +36,7 @@ import {
CredenzaTitle CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api";; import { formatAxiosError } from "@app/lib/api";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
@ -222,7 +222,10 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
<QRCodeCanvas value={secretUri} size={200} /> <QRCodeCanvas value={secretUri} size={200} />
</div> </div>
<div className="max-w-md mx-auto"> <div className="max-w-md mx-auto">
<CopyTextBox text={secretUri} wrapText={false} /> <CopyTextBox
text={secretUri}
wrapText={false}
/>
</div> </div>
<Form {...confirmForm}> <Form {...confirmForm}>
@ -279,6 +282,9 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
)} )}
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
{(step === 1 || step === 2) && ( {(step === 1 || step === 2) && (
<Button <Button
type="button" type="button"
@ -295,9 +301,6 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
Submit Submit
</Button> </Button>
)} )}
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -0,0 +1,21 @@
"use client";
import React from "react";
import { Loader2 } from "lucide-react"; // Ensure you have lucide-react installed
interface LoaderProps {
height?: string;
}
const LoaderPlaceholder: React.FC<LoaderProps> = ({ height = "100px" }) => {
return (
<div
className="flex items-center justify-center w-full"
style={{ height }}
>
<Loader2 className="animate-spin" />
</div>
);
};
export default LoaderPlaceholder;

View file

@ -7,7 +7,7 @@ export function SettingsSection({ children }: { children: React.ReactNode }) {
} }
export function SettingsSectionHeader({ children }: { children: React.ReactNode }) { export function SettingsSectionHeader({ children }: { children: React.ReactNode }) {
return <div className="space-y-0.5 pb-8">{children}</div> return <div className="space-y-0.5 pb-6">{children}</div>
} }
export function SettingsSectionForm({ children }: { children: React.ReactNode }) { export function SettingsSectionForm({ children }: { children: React.ReactNode }) {

View file

@ -11,7 +11,7 @@ export default function SettingsSectionTitle({
}: SettingsSectionTitleProps) { }: SettingsSectionTitleProps) {
return ( return (
<div <div
className={`space-y-0.5 select-none ${!size || size === "2xl" ? "mb-8 md:mb-8" : ""}`} className={`space-y-0.5 ${!size || size === "2xl" ? "mb-8 md:mb-8" : ""}`}
> >
<h2 <h2
className={`text-${ className={`text-${

View file

@ -490,7 +490,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
<div className="w-full"> <div className="w-full">
<div <div
className={cn( className={cn(
`flex flex-row flex-wrap items-center gap-2 p-2 w-full rounded-md border-2 border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`, `flex flex-row flex-wrap items-center gap-1.5 p-1.5 w-full rounded-md border-2 border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 bg-transparent`,
styleClasses?.inlineTagsContainer styleClasses?.inlineTagsContainer
)} )}
> >
@ -644,7 +644,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
) : ( ) : (
<div <div
className={cn( className={cn(
`flex flex-row flex-wrap items-center p-2 gap-2 h-fit w-full bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`, `flex flex-row flex-wrap items-center p-1.5 gap-1.5 h-fit w-full bg-transparent text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`,
styleClasses?.inlineTagsContainer styleClasses?.inlineTagsContainer
)} )}
> >

View file

@ -22,7 +22,7 @@ export const tagVariants = cva(
"bg-destructive border-destructive text-destructive-foreground hover:bg-destructive/90 disabled:cursor-not-allowed disabled:opacity-50" "bg-destructive border-destructive text-destructive-foreground hover:bg-destructive/90 disabled:cursor-not-allowed disabled:opacity-50"
}, },
size: { size: {
sm: "text-xs h-7", sm: "text-xs h-6",
md: "text-sm h-8", md: "text-sm h-8",
lg: "text-base h-9", lg: "text-base h-9",
xl: "text-lg h-10" xl: "text-lg h-10"
@ -67,7 +67,7 @@ export const tagVariants = cva(
variant: "default", variant: "default",
size: "md", size: "md",
shape: "default", shape: "default",
borderStyle: "default", borderStyle: "none",
interaction: "nonClickable", interaction: "nonClickable",
animation: "fadeIn", animation: "fadeIn",
textStyle: "normal" textStyle: "normal"
@ -144,7 +144,7 @@ export const Tag: React.FC<TagProps> = ({
}} }}
disabled={disabled} disabled={disabled}
className={cn( className={cn(
`py-1 px-3 h-full hover:bg-transparent`, `p-1 h-full hover:bg-transparent`,
tagClasses?.closeButton tagClasses?.closeButton
)} )}
> >

View file

@ -16,6 +16,8 @@ const buttonVariants = cva(
"bg-destructive text-destructive-foreground hover:bg-destructive/90", "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: outline:
"border-2 border-input bg-card hover:bg-accent hover:text-accent-foreground", "border-2 border-input bg-card hover:bg-accent hover:text-accent-foreground",
outlinePrimary:
"border-2 border-primary bg-card hover:bg-primary/10 text-primary",
secondary: secondary:
"bg-secondary border border-input border-2 text-secondary-foreground hover:bg-secondary/80", "bg-secondary border border-input border-2 text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",

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-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card 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-[35%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card 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}

View file

@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md", "relative flex h-10 w-10 items-center justify-center border-y-2 border-r-2 border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l-2 last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background", isActive && "z-10 ring-2 ring-ring ring-offset-background",
className className
)} )}

View file

@ -77,7 +77,7 @@ const TableHead = React.forwardRef<
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", "h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className className
)} )}
{...props} {...props}

View file

@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva( const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{ {
variants: { variants: {
variant: { variant: {