mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-25 06:16:40 +00:00
Merge branch 'main' into holepunch
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
@@ -24,11 +24,11 @@ import {
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
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 { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
@@ -40,13 +40,13 @@ type CreateRoleFormProps = {
|
||||
|
||||
const formSchema = z.object({
|
||||
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({
|
||||
open,
|
||||
setOpen,
|
||||
afterCreate,
|
||||
afterCreate
|
||||
}: CreateRoleFormProps) {
|
||||
const { org } = useOrgContext();
|
||||
|
||||
@@ -58,8 +58,8 @@ export default function CreateRoleForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
},
|
||||
description: ""
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
@@ -70,7 +70,7 @@ export default function CreateRoleForm({
|
||||
`/org/${org?.org.orgId}/role`,
|
||||
{
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
description: values.description
|
||||
} as CreateRoleBody
|
||||
)
|
||||
.catch((e) => {
|
||||
@@ -80,7 +80,7 @@ export default function CreateRoleForm({
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while creating the role."
|
||||
),
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function CreateRoleForm({
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Role created",
|
||||
description: "The role has been successfully created.",
|
||||
description: "The role has been successfully created."
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -135,10 +135,7 @@ export default function CreateRoleForm({
|
||||
<FormItem>
|
||||
<FormLabel>Role Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter name for the role"
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -151,10 +148,7 @@ export default function CreateRoleForm({
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Describe the role"
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -164,6 +158,9 @@ export default function CreateRoleForm({
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="create-role-form"
|
||||
@@ -172,9 +169,6 @@ export default function CreateRoleForm({
|
||||
>
|
||||
Create Role
|
||||
</Button>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { ListRolesResponse } from "@server/routers/role";
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { RoleRow } from "./RolesTable";
|
||||
import { formatAxiosError } from "@app/lib/api";;
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
@@ -47,14 +47,14 @@ type CreateRoleFormProps = {
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
newRoleId: z.string({ message: "New role is required" }),
|
||||
newRoleId: z.string({ message: "New role is required" })
|
||||
});
|
||||
|
||||
export default function DeleteRoleForm({
|
||||
open,
|
||||
roleToDelete,
|
||||
setOpen,
|
||||
afterDelete,
|
||||
afterDelete
|
||||
}: CreateRoleFormProps) {
|
||||
const { org } = useOrgContext();
|
||||
|
||||
@@ -66,9 +66,9 @@ export default function DeleteRoleForm({
|
||||
useEffect(() => {
|
||||
async function fetchRoles() {
|
||||
const res = await api
|
||||
.get<AxiosResponse<ListRolesResponse>>(
|
||||
`/org/${org?.org.orgId}/roles`
|
||||
)
|
||||
.get<
|
||||
AxiosResponse<ListRolesResponse>
|
||||
>(`/org/${org?.org.orgId}/roles`)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
@@ -77,7 +77,7 @@ export default function DeleteRoleForm({
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the roles"
|
||||
),
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,8 +96,8 @@ export default function DeleteRoleForm({
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
newRoleId: "",
|
||||
},
|
||||
newRoleId: ""
|
||||
}
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
@@ -106,8 +106,8 @@ export default function DeleteRoleForm({
|
||||
const res = await api
|
||||
.delete(`/role/${roleToDelete.roleId}`, {
|
||||
data: {
|
||||
roleId: values.newRoleId,
|
||||
},
|
||||
roleId: values.newRoleId
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
@@ -116,7 +116,7 @@ export default function DeleteRoleForm({
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while removing the role."
|
||||
),
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function DeleteRoleForm({
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Role removed",
|
||||
description: "The role has been successfully removed.",
|
||||
description: "The role has been successfully removed."
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -214,6 +214,9 @@ export default function DeleteRoleForm({
|
||||
</div>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="remove-role-form"
|
||||
@@ -222,9 +225,6 @@ export default function DeleteRoleForm({
|
||||
>
|
||||
Remove Role
|
||||
</Button>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from "@app/components/Credenza";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
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 { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
@@ -194,10 +194,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter an email"
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -341,6 +338,9 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||
</div>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="invite-user-form"
|
||||
@@ -349,9 +349,6 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||
>
|
||||
Create Invitation
|
||||
</Button>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button variant={"outline"} className="ml-2">
|
||||
<Button variant={"outlinePrimary"} className="ml-2">
|
||||
Manage
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -64,7 +64,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
</Breadcrumb>
|
||||
</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">
|
||||
User {user?.email}
|
||||
</h2>
|
||||
@@ -73,7 +73,6 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
|
||||
<SidebarSettings
|
||||
sidebarNavItems={sidebarNavItems}
|
||||
limitWidth={true}
|
||||
>
|
||||
{children}
|
||||
</SidebarSettings>
|
||||
|
||||
@@ -210,11 +210,11 @@ export default function GeneralPage() {
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
This is the display name of the
|
||||
org
|
||||
organization.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -238,7 +238,6 @@ export default function GeneralPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Danger Zone
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Metadata } from "next";
|
||||
import { TopbarNav } from "@app/components/TopbarNav";
|
||||
import { Cog, Combine, Laptop, Link, Settings, Users, Waypoints, Workflow } from "lucide-react";
|
||||
import {
|
||||
Combine,
|
||||
LinkIcon,
|
||||
Settings,
|
||||
Users,
|
||||
Waypoints
|
||||
} from "lucide-react";
|
||||
import { Header } from "@app/components/Header";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -43,7 +49,7 @@ const topNavItems = [
|
||||
{
|
||||
title: "Shareable Links",
|
||||
href: "/{orgId}/settings/share-links",
|
||||
icon: <Link className="h-4 w-4" />
|
||||
icon: <LinkIcon className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "General",
|
||||
@@ -100,19 +106,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full border-b bg-card select-none sm:px-0 px-3 fixed top-0 z-10">
|
||||
<div className="container mx-auto flex flex-col content-between">
|
||||
<div className="my-4">
|
||||
<UserProvider user={user}>
|
||||
<Header orgId={params.orgId} orgs={orgs} />
|
||||
</UserProvider>
|
||||
<div className="w-full bg-card sm:px-0 px-3 fixed top-0 z-10">
|
||||
<div className="border-b">
|
||||
<div className="container mx-auto flex flex-col content-between">
|
||||
<div className="my-4">
|
||||
<UserProvider user={user}>
|
||||
<Header orgId={params.orgId} orgs={orgs} />
|
||||
</UserProvider>
|
||||
</div>
|
||||
<TopbarNav items={topNavItems} orgId={params.orgId} />
|
||||
</div>
|
||||
<TopbarNav items={topNavItems} orgId={params.orgId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto sm:px-0 px-3 pt-[165px]">
|
||||
{children}
|
||||
<div className="container mx-auto sm:px-0 px-3 pt-[155px]">
|
||||
<div className="container mx-auto sm:px-0 px-3">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -233,7 +233,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||
<Link
|
||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||
>
|
||||
<Button variant={"outline"} className="ml-2">
|
||||
<Button variant={"outlinePrimary"} className="ml-2">
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -2,27 +2,68 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
|
||||
interface DomainOption {
|
||||
baseDomain: string;
|
||||
domainId: string;
|
||||
}
|
||||
|
||||
interface CustomDomainInputProps {
|
||||
domainSuffix: string;
|
||||
domainOptions: DomainOption[];
|
||||
selectedDomainId?: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
onChange?: (value: string, selectedDomainId: string) => void;
|
||||
}
|
||||
|
||||
export default function CustomDomainInput({
|
||||
domainSuffix,
|
||||
placeholder = "Enter subdomain",
|
||||
domainOptions,
|
||||
selectedDomainId,
|
||||
placeholder = "Subdomain",
|
||||
value: defaultValue,
|
||||
onChange
|
||||
}: CustomDomainInputProps) {
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
const [selectedDomain, setSelectedDomain] = React.useState<DomainOption>();
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
React.useEffect(() => {
|
||||
if (domainOptions.length) {
|
||||
if (selectedDomainId) {
|
||||
const selectedDomainOption = domainOptions.find(
|
||||
(option) => option.domainId === selectedDomainId
|
||||
);
|
||||
setSelectedDomain(selectedDomainOption || domainOptions[0]);
|
||||
} else {
|
||||
setSelectedDomain(domainOptions[0]);
|
||||
}
|
||||
}
|
||||
}, [domainOptions]);
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!selectedDomain) {
|
||||
return;
|
||||
}
|
||||
const newValue = event.target.value;
|
||||
setValue(newValue);
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
onChange(newValue, selectedDomain.domainId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDomainChange = (domainId: string) => {
|
||||
const newSelectedDomain =
|
||||
domainOptions.find((option) => option.domainId === domainId) ||
|
||||
domainOptions[0];
|
||||
setSelectedDomain(newSelectedDomain);
|
||||
if (onChange) {
|
||||
onChange(value, newSelectedDomain.domainId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,12 +74,28 @@ export default function CustomDomainInput({
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className="rounded-r-none w-full"
|
||||
onChange={handleInputChange}
|
||||
className="w-1/2 mr-1 text-right"
|
||||
/>
|
||||
<div className="max-w-1/2 flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-muted-foreground">
|
||||
<span className="text-sm truncate">.{domainSuffix}</span>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={handleDomainChange}
|
||||
value={selectedDomain?.domainId}
|
||||
defaultValue={selectedDomain?.domainId}
|
||||
>
|
||||
<SelectTrigger className="w-1/2 pr-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{domainOptions.map((option) => (
|
||||
<SelectItem
|
||||
key={option.domainId}
|
||||
value={option.domainId}
|
||||
>
|
||||
.{option.baseDomain}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { ArrowRight, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { Separator } from "@app/components/ui/separator";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
@@ -13,21 +11,14 @@ import {
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import Link from "next/link";
|
||||
|
||||
type ResourceInfoBoxType = {};
|
||||
|
||||
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const { org } = useOrgContext();
|
||||
const { resource, authInfo } = useResourceContext();
|
||||
|
||||
let fullUrl = `${resource.ssl ? "https" : "http"}://`;
|
||||
if (resource.isBaseDomain) {
|
||||
fullUrl = fullUrl + org.org.domain;
|
||||
} else {
|
||||
fullUrl = fullUrl + `${resource.subdomain}.${org.org.domain}`;
|
||||
}
|
||||
let fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
@@ -36,7 +27,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
Resource Information
|
||||
</AlertTitle>
|
||||
<AlertDescription className="mt-4">
|
||||
<InfoSections>
|
||||
<InfoSections cols={3}>
|
||||
{resource.http ? (
|
||||
<>
|
||||
<InfoSection>
|
||||
@@ -50,22 +41,16 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
authInfo.whitelist ? (
|
||||
<div className="flex items-start space-x-2 text-green-500">
|
||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||
<span>
|
||||
This resource is protected with
|
||||
at least one auth method.
|
||||
</span>
|
||||
<span>Protected</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 text-yellow-500">
|
||||
<ShieldOff className="w-4 h-4" />
|
||||
<span>
|
||||
Anyone can access this resource.
|
||||
</span>
|
||||
<span>Not Protected</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<Separator orientation="vertical" />
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>URL</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
@@ -75,6 +60,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Site</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{resource.siteName}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -86,7 +77,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<Separator orientation="vertical" />
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Port</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
@@ -24,22 +24,22 @@ import {
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { formatAxiosError } from "@app/lib/api";;
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Resource } from "@server/db/schema";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
const setPasswordFormSchema = z.object({
|
||||
password: z.string().min(4).max(100),
|
||||
password: z.string().min(4).max(100)
|
||||
});
|
||||
|
||||
type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>;
|
||||
|
||||
const defaultValues: Partial<SetPasswordFormValues> = {
|
||||
password: "",
|
||||
password: ""
|
||||
};
|
||||
|
||||
type SetPasswordFormProps = {
|
||||
@@ -53,7 +53,7 @@ export default function SetResourcePasswordForm({
|
||||
open,
|
||||
setOpen,
|
||||
resourceId,
|
||||
onSetPassword,
|
||||
onSetPassword
|
||||
}: SetPasswordFormProps) {
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function SetResourcePasswordForm({
|
||||
|
||||
const form = useForm<SetPasswordFormValues>({
|
||||
resolver: zodResolver(setPasswordFormSchema),
|
||||
defaultValues,
|
||||
defaultValues
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,7 +76,7 @@ export default function SetResourcePasswordForm({
|
||||
setLoading(true);
|
||||
|
||||
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, {
|
||||
password: data.password,
|
||||
password: data.password
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
@@ -85,14 +85,14 @@ export default function SetResourcePasswordForm({
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while setting the resource password"
|
||||
),
|
||||
)
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Resource password set",
|
||||
description:
|
||||
"The resource password has been set successfully",
|
||||
"The resource password has been set successfully"
|
||||
});
|
||||
|
||||
if (onSetPassword) {
|
||||
@@ -136,17 +136,16 @@ export default function SetResourcePasswordForm({
|
||||
<Input
|
||||
autoComplete="off"
|
||||
type="password"
|
||||
placeholder="Your secure password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
Users will be able to access
|
||||
this resource by entering this
|
||||
password. It must be at least 4
|
||||
characters long.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -154,6 +153,9 @@ export default function SetResourcePasswordForm({
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="set-password-form"
|
||||
@@ -162,9 +164,6 @@ export default function SetResourcePasswordForm({
|
||||
>
|
||||
Enable Password Protection
|
||||
</Button>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
@@ -24,27 +24,27 @@ import {
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { formatAxiosError } from "@app/lib/api";;
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Resource } from "@server/db/schema";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
InputOTPSlot
|
||||
} from "@app/components/ui/input-otp";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
const setPincodeFormSchema = z.object({
|
||||
pincode: z.string().length(6),
|
||||
pincode: z.string().length(6)
|
||||
});
|
||||
|
||||
type SetPincodeFormValues = z.infer<typeof setPincodeFormSchema>;
|
||||
|
||||
const defaultValues: Partial<SetPincodeFormValues> = {
|
||||
pincode: "",
|
||||
pincode: ""
|
||||
};
|
||||
|
||||
type SetPincodeFormProps = {
|
||||
@@ -58,7 +58,7 @@ export default function SetResourcePincodeForm({
|
||||
open,
|
||||
setOpen,
|
||||
resourceId,
|
||||
onSetPincode,
|
||||
onSetPincode
|
||||
}: SetPincodeFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function SetResourcePincodeForm({
|
||||
|
||||
const form = useForm<SetPincodeFormValues>({
|
||||
resolver: zodResolver(setPincodeFormSchema),
|
||||
defaultValues,
|
||||
defaultValues
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -81,7 +81,7 @@ export default function SetResourcePincodeForm({
|
||||
setLoading(true);
|
||||
|
||||
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, {
|
||||
pincode: data.pincode,
|
||||
pincode: data.pincode
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
@@ -89,15 +89,15 @@ export default function SetResourcePincodeForm({
|
||||
title: "Error setting resource PIN code",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while setting the resource PIN code",
|
||||
),
|
||||
"An error occurred while setting the resource PIN code"
|
||||
)
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Resource PIN code set",
|
||||
description:
|
||||
"The resource pincode has been set successfully",
|
||||
"The resource pincode has been set successfully"
|
||||
});
|
||||
|
||||
if (onSetPincode) {
|
||||
@@ -167,13 +167,13 @@ export default function SetResourcePincodeForm({
|
||||
</InputOTP>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
Users will be able to access
|
||||
this resource by entering this
|
||||
PIN code. It must be at least 6
|
||||
digits long.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -181,6 +181,9 @@ export default function SetResourcePincodeForm({
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="set-pincode-form"
|
||||
@@ -189,9 +192,6 @@ export default function SetResourcePincodeForm({
|
||||
>
|
||||
Enable PIN Code Protection
|
||||
</Button>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -8,14 +8,12 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import {
|
||||
GetResourceAuthInfoResponse,
|
||||
GetResourceWhitelistResponse,
|
||||
ListResourceRolesResponse,
|
||||
ListResourceUsersResponse
|
||||
} from "@server/routers/resource";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { set, z } from "zod";
|
||||
import { Tag } from "emblor";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
@@ -27,12 +25,8 @@ import {
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { TagInput } from "emblor";
|
||||
// import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { ListUsersResponse } from "@server/routers/user";
|
||||
import { Switch } from "@app/components/ui/switch";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { Binary, Key, ShieldCheck } from "lucide-react";
|
||||
import { Binary, Key } from "lucide-react";
|
||||
import SetResourcePasswordForm from "./SetResourcePasswordForm";
|
||||
import SetResourcePincodeForm from "./SetResourcePincodeForm";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
@@ -44,11 +38,12 @@ import {
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionFooter
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionForm
|
||||
} from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const UsersRolesFormSchema = z.object({
|
||||
@@ -413,7 +408,7 @@ export default function ResourceAuthenticationPage() {
|
||||
<SwitchInput
|
||||
id="sso-toggle"
|
||||
label="Use Platform SSO"
|
||||
description="Existing users will only have to login once for all resources that have this enabled."
|
||||
description="Existing users will only have to log in once for all resources that have this enabled."
|
||||
defaultChecked={resource.sso}
|
||||
onCheckedChange={(val) => setSsoEnabled(val)}
|
||||
/>
|
||||
@@ -435,7 +430,6 @@ export default function ResourceAuthenticationPage() {
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
@@ -444,7 +438,8 @@ export default function ResourceAuthenticationPage() {
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder="Enter a role"
|
||||
placeholder="Select a role"
|
||||
size="sm"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.roles
|
||||
@@ -473,23 +468,13 @@ export default function ResourceAuthenticationPage() {
|
||||
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>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
These roles will be able
|
||||
to access this resource.
|
||||
Admins can always access
|
||||
this resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -500,7 +485,6 @@ export default function ResourceAuthenticationPage() {
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Users</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
@@ -509,11 +493,12 @@ export default function ResourceAuthenticationPage() {
|
||||
setActiveTagIndex={
|
||||
setActiveUsersTagIndex
|
||||
}
|
||||
placeholder="Enter a user"
|
||||
placeholder="Select a user"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.users
|
||||
}
|
||||
size="sm"
|
||||
setTags={(
|
||||
newUsers
|
||||
) => {
|
||||
@@ -538,25 +523,8 @@ export default function ResourceAuthenticationPage() {
|
||||
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>
|
||||
<FormDescription>
|
||||
Users added here will be
|
||||
able to access this
|
||||
resource. A user will
|
||||
always have access to a
|
||||
resource if they have a
|
||||
role that has access to
|
||||
it.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -601,7 +569,7 @@ export default function ResourceAuthenticationPage() {
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="outlinePrimary"
|
||||
onClick={
|
||||
authInfo.password
|
||||
? removeResourcePassword
|
||||
@@ -627,7 +595,7 @@ export default function ResourceAuthenticationPage() {
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="outlinePrimary"
|
||||
onClick={
|
||||
authInfo.pincode
|
||||
? removeResourcePincode
|
||||
@@ -683,6 +651,7 @@ export default function ResourceAuthenticationPage() {
|
||||
activeTagIndex={
|
||||
activeEmailTagIndex
|
||||
}
|
||||
size={"sm"}
|
||||
validateTag={(
|
||||
tag
|
||||
) => {
|
||||
@@ -727,18 +696,12 @@ export default function ResourceAuthenticationPage() {
|
||||
false
|
||||
}
|
||||
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>
|
||||
<FormDescription>
|
||||
Press enter to add an email after typing it in the input field.
|
||||
Press enter to add an
|
||||
email after typing it in
|
||||
the input field.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
@@ -103,8 +104,8 @@ export default function ReverseProxyTargets(props: {
|
||||
resolver: zodResolver(addTargetSchema),
|
||||
defaultValues: {
|
||||
ip: "",
|
||||
method: resource.http ? "http" : null
|
||||
// protocol: "TCP",
|
||||
method: resource.http ? "http" : null,
|
||||
port: "" as any as number
|
||||
} as z.infer<typeof addTargetSchema>
|
||||
});
|
||||
|
||||
@@ -199,7 +200,11 @@ export default function ReverseProxyTargets(props: {
|
||||
};
|
||||
|
||||
setTargets([...targets, newTarget]);
|
||||
addTargetForm.reset();
|
||||
addTargetForm.reset({
|
||||
ip: "",
|
||||
method: resource.http ? "http" : null,
|
||||
port: "" as any as number
|
||||
});
|
||||
}
|
||||
|
||||
const removeTarget = (targetId: number) => {
|
||||
@@ -241,10 +246,7 @@ export default function ReverseProxyTargets(props: {
|
||||
>(`/resource/${params.resourceId}/target`, data);
|
||||
target.targetId = res.data.data.targetId;
|
||||
} else if (target.updated) {
|
||||
await api.post(
|
||||
`/target/${target.targetId}`,
|
||||
data
|
||||
);
|
||||
await api.post(`/target/${target.targetId}`, data);
|
||||
}
|
||||
|
||||
setTargets([
|
||||
@@ -261,9 +263,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
for (const targetId of targetsToRemove) {
|
||||
await api.delete(`/target/${targetId}`);
|
||||
setTargets(
|
||||
targets.filter((t) => t.targetId !== targetId)
|
||||
);
|
||||
setTargets(targets.filter((t) => t.targetId !== targetId));
|
||||
}
|
||||
|
||||
toast({
|
||||
@@ -421,6 +421,7 @@ export default function ReverseProxyTargets(props: {
|
||||
<SelectContent>
|
||||
<SelectItem value="http">http</SelectItem>
|
||||
<SelectItem value="https">https</SelectItem>
|
||||
<SelectItem value="h2c">h2c</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
@@ -436,7 +437,13 @@ export default function ReverseProxyTargets(props: {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel()
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: 1000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (pageLoading) {
|
||||
@@ -452,8 +459,7 @@ export default function ReverseProxyTargets(props: {
|
||||
SSL Configuration
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Setup SSL to secure your connections with
|
||||
LetsEncrypt certificates
|
||||
Set up SSL to secure your connections with certificates
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -475,7 +481,7 @@ export default function ReverseProxyTargets(props: {
|
||||
Target Configuration
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Setup targets to route traffic to your services
|
||||
Set up targets to route traffic to your services
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -484,7 +490,7 @@ export default function ReverseProxyTargets(props: {
|
||||
onSubmit={addTargetForm.handleSubmit(addTarget)}
|
||||
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-start">
|
||||
{resource.http && (
|
||||
<FormField
|
||||
control={addTargetForm.control}
|
||||
@@ -517,6 +523,9 @@ export default function ReverseProxyTargets(props: {
|
||||
<SelectItem value="https">
|
||||
https
|
||||
</SelectItem>
|
||||
<SelectItem value="h2c">
|
||||
h2c
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -536,18 +545,6 @@ export default function ReverseProxyTargets(props: {
|
||||
<Input id="ip" {...field} />
|
||||
</FormControl>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
@@ -566,83 +563,68 @@ export default function ReverseProxyTargets(props: {
|
||||
/>
|
||||
</FormControl>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlinePrimary"
|
||||
className="mt-8"
|
||||
>
|
||||
Add Target
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="submit" variant="outline">
|
||||
Add Target
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column
|
||||
.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No targets. Add a target using the
|
||||
form.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Adding more than one target above will enable load
|
||||
balancing.
|
||||
</p>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No targets. Add a target using the form.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
<TableCaption>
|
||||
Adding more than one target above will enable load
|
||||
balancing.
|
||||
</TableCaption>
|
||||
</Table>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
|
||||
@@ -33,7 +33,6 @@ import { useEffect, useState } from "react";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { GetResourceAuthInfoResponse } from "@server/routers/resource";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import {
|
||||
SettingsContainer,
|
||||
@@ -53,6 +52,15 @@ import { subdomainSchema } from "@server/lib/schemas";
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { ListDomainsResponse } from "@server/routers/domain";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||
|
||||
const GeneralFormSchema = z
|
||||
.object({
|
||||
@@ -60,7 +68,8 @@ const GeneralFormSchema = z
|
||||
name: z.string().min(1).max(255),
|
||||
proxyPort: z.number().optional(),
|
||||
http: z.boolean(),
|
||||
isBaseDomain: z.boolean().optional()
|
||||
isBaseDomain: z.boolean().optional(),
|
||||
domainId: z.string().optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -100,6 +109,7 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
type TransferFormValues = z.infer<typeof TransferFormSchema>;
|
||||
|
||||
export default function GeneralForm() {
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
const params = useParams();
|
||||
const { resource, updateResource } = useResourceContext();
|
||||
const { org } = useOrgContext();
|
||||
@@ -113,10 +123,13 @@ export default function GeneralForm() {
|
||||
|
||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [domainSuffix, setDomainSuffix] = useState(org.org.domain);
|
||||
const [transferLoading, setTransferLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [baseDomains, setBaseDomains] = useState<
|
||||
ListDomainsResponse["domains"]
|
||||
>([]);
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState(true);
|
||||
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
|
||||
resource.isBaseDomain ? "basedomain" : "subdomain"
|
||||
);
|
||||
@@ -128,7 +141,8 @@ export default function GeneralForm() {
|
||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||
proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
|
||||
http: resource.http,
|
||||
isBaseDomain: resource.isBaseDomain ? true : false
|
||||
isBaseDomain: resource.isBaseDomain ? true : false,
|
||||
domainId: resource.domainId || undefined
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -147,19 +161,54 @@ export default function GeneralForm() {
|
||||
);
|
||||
setSites(res.data.data.sites);
|
||||
};
|
||||
fetchSites();
|
||||
|
||||
const fetchDomains = async () => {
|
||||
const res = await api
|
||||
.get<
|
||||
AxiosResponse<ListDomainsResponse>
|
||||
>(`/org/${orgId}/domains/`)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error fetching domains",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred when fetching the domains"
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
const domains = res.data.data.domains;
|
||||
setBaseDomains(domains);
|
||||
setFormKey((key) => key + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
await fetchDomains();
|
||||
await fetchSites();
|
||||
|
||||
setLoadingPage(false);
|
||||
};
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function onSubmit(data: GeneralFormValues) {
|
||||
setSaveLoading(true);
|
||||
|
||||
const res = await api
|
||||
.post(`resource/${resource?.resourceId}`, {
|
||||
name: data.name,
|
||||
subdomain: data.subdomain,
|
||||
proxyPort: data.proxyPort,
|
||||
isBaseDomain: data.isBaseDomain
|
||||
})
|
||||
.post<AxiosResponse<UpdateResourceResponse>>(
|
||||
`resource/${resource?.resourceId}`,
|
||||
{
|
||||
name: data.name,
|
||||
subdomain: data.http ? data.subdomain : undefined,
|
||||
proxyPort: data.proxyPort,
|
||||
isBaseDomain: data.http ? data.isBaseDomain : undefined,
|
||||
domainId: data.http ? data.domainId : undefined
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
@@ -177,12 +226,17 @@ export default function GeneralForm() {
|
||||
description: "The resource has been updated successfully"
|
||||
});
|
||||
|
||||
const resource = res.data.data;
|
||||
|
||||
updateResource({
|
||||
name: data.name,
|
||||
subdomain: data.subdomain,
|
||||
proxyPort: data.proxyPort,
|
||||
isBaseDomain: data.isBaseDomain
|
||||
isBaseDomain: data.isBaseDomain,
|
||||
fullDomain: resource.fullDomain
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
}
|
||||
setSaveLoading(false);
|
||||
}
|
||||
@@ -211,323 +265,415 @@ export default function GeneralForm() {
|
||||
description: "The resource has been transferred successfully"
|
||||
});
|
||||
router.refresh();
|
||||
|
||||
updateResource({
|
||||
siteName:
|
||||
sites.find((site) => site.siteId === data.siteId)?.name ||
|
||||
""
|
||||
});
|
||||
}
|
||||
setTransferLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
General Settings
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure the general settings for this resource
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
!loadingPage && (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
General Settings
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure the general settings for this resource
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the display name of the
|
||||
resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</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>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subdomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
{!env.flags
|
||||
.allowBaseDomainResources && (
|
||||
<FormLabel>
|
||||
Subdomain
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
{domainType ===
|
||||
"subdomain" ? (
|
||||
<FormControl>
|
||||
<CustomDomainInput
|
||||
value={
|
||||
field.value ||
|
||||
""
|
||||
}
|
||||
domainSuffix={
|
||||
domainSuffix
|
||||
}
|
||||
placeholder="Enter subdomain"
|
||||
onChange={(
|
||||
value
|
||||
) =>
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<FormControl>
|
||||
<Input
|
||||
value={
|
||||
domainSuffix
|
||||
}
|
||||
readOnly
|
||||
disabled
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
<FormDescription>
|
||||
This is the subdomain
|
||||
that will be used to
|
||||
access the resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!resource.http && (
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form} key={formKey}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Port Number
|
||||
</FormLabel>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter port number"
|
||||
value={
|
||||
field.value ?? ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the port that will
|
||||
be used to access the
|
||||
resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={saveLoading}
|
||||
disabled={saveLoading}
|
||||
form="general-settings-form"
|
||||
>
|
||||
Save Settings
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
{resource.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"
|
||||
);
|
||||
form.setValue(
|
||||
"isBaseDomain",
|
||||
val ===
|
||||
"basedomain"
|
||||
? true
|
||||
: false
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="subdomain">
|
||||
Subdomain
|
||||
</SelectItem>
|
||||
<SelectItem value="basedomain">
|
||||
Base
|
||||
Domain
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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 className="flex flex-col">
|
||||
<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 ===
|
||||
<div className="col-span-2">
|
||||
{domainType === "subdomain" ? (
|
||||
<div className="w-fill space-y-2">
|
||||
<FormLabel>
|
||||
Subdomain
|
||||
</FormLabel>
|
||||
<div className="flex">
|
||||
<div className="w-1/2">
|
||||
<FormField
|
||||
control={
|
||||
form.control
|
||||
}
|
||||
name="subdomain"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="border-r-0 rounded-r-none"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<FormField
|
||||
control={
|
||||
form.control
|
||||
}
|
||||
name="domainId"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
defaultValue={
|
||||
field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Select the new site to transfer
|
||||
this resource to.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
}
|
||||
value={
|
||||
field.value
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="rounded-l-none">
|
||||
<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 ||
|
||||
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>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={transferLoading}
|
||||
disabled={transferLoading}
|
||||
form="transfer-form"
|
||||
variant="destructive"
|
||||
>
|
||||
Transfer Resource
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
{!resource.http && (
|
||||
<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 />
|
||||
</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" />
|
||||
<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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,9 +130,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
<OrgProvider org={org}>
|
||||
<ResourceProvider resource={resource} authInfo={authInfo}>
|
||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||
<div className="mb-8">
|
||||
<ResourceInfoBox />
|
||||
</div>
|
||||
<ResourceInfoBox />
|
||||
{children}
|
||||
</SidebarSettings>
|
||||
</ResourceProvider>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
@@ -92,9 +93,9 @@ enum RuleAction {
|
||||
}
|
||||
|
||||
enum RuleMatch {
|
||||
PATH = "Path",
|
||||
IP = "IP",
|
||||
CIDR = "IP Range",
|
||||
PATH = "Path"
|
||||
CIDR = "IP Range"
|
||||
}
|
||||
|
||||
export default function ResourceRules(props: {
|
||||
@@ -469,9 +470,9 @@ export default function ResourceRules(props: {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="PATH">{RuleMatch.PATH}</SelectItem>
|
||||
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
|
||||
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
|
||||
<SelectItem value="PATH">{RuleMatch.PATH}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
@@ -524,7 +525,13 @@ export default function ResourceRules(props: {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel()
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: 1000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (pageLoading) {
|
||||
@@ -617,7 +624,7 @@ export default function ResourceRules(props: {
|
||||
onSubmit={addRuleForm.handleSubmit(addRule)}
|
||||
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
|
||||
control={addRuleForm.control}
|
||||
name="action"
|
||||
@@ -665,17 +672,17 @@ export default function ResourceRules(props: {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{resource.http && (
|
||||
<SelectItem value="PATH">
|
||||
{RuleMatch.PATH}
|
||||
</SelectItem>
|
||||
)}
|
||||
<SelectItem value="IP">
|
||||
{RuleMatch.IP}
|
||||
</SelectItem>
|
||||
<SelectItem value="CIDR">
|
||||
{RuleMatch.CIDR}
|
||||
</SelectItem>
|
||||
{resource.http && (
|
||||
<SelectItem value="PATH">
|
||||
{RuleMatch.PATH}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
@@ -705,68 +712,63 @@ export default function ResourceRules(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlinePrimary"
|
||||
disabled={!rulesEnabled}
|
||||
>
|
||||
Add Rule
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
disabled={!rulesEnabled}
|
||||
>
|
||||
Add Rule
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column
|
||||
.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No rules. Add a rule using the form.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Rules are evaluated by priority in ascending order.
|
||||
</p>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No rules. Add a rule using the form.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
<TableCaption>
|
||||
Rules are evaluated by priority in ascending order.
|
||||
</TableCaption>
|
||||
</Table>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
|
||||
@@ -107,7 +107,12 @@ export default function CreateShareLinkForm({
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [resources, setResources] = useState<
|
||||
{ resourceId: number; name: string; resourceUrl: string }[]
|
||||
{
|
||||
resourceId: number;
|
||||
name: string;
|
||||
resourceUrl: string;
|
||||
siteName: string | null;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const timeUnits = [
|
||||
@@ -152,13 +157,16 @@ export default function CreateShareLinkForm({
|
||||
|
||||
if (res?.status === 200) {
|
||||
setResources(
|
||||
res.data.data.resources.filter((r) => {
|
||||
return r.http;
|
||||
}).map((r) => ({
|
||||
resourceId: r.resourceId,
|
||||
name: r.name,
|
||||
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`
|
||||
}))
|
||||
res.data.data.resources
|
||||
.filter((r) => {
|
||||
return r.http;
|
||||
})
|
||||
.map((r) => ({
|
||||
resourceId: r.resourceId,
|
||||
name: r.name,
|
||||
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`,
|
||||
siteName: r.siteName
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -229,19 +237,28 @@ export default function CreateShareLinkForm({
|
||||
token.accessToken
|
||||
);
|
||||
setDirectLink(directLink);
|
||||
|
||||
const resource = resources.find((r) => r.resourceId === values.resourceId);
|
||||
|
||||
onCreated?.({
|
||||
accessTokenId: token.accessTokenId,
|
||||
resourceId: token.resourceId,
|
||||
resourceName: values.resourceName,
|
||||
title: token.title,
|
||||
createdAt: token.createdAt,
|
||||
expiresAt: token.expiresAt
|
||||
expiresAt: token.expiresAt,
|
||||
siteName: resource?.siteName || null,
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function getSelectedResourceName(id: number) {
|
||||
const resource = resources.find((r) => r.resourceId === id);
|
||||
return `${resource?.name} ${resource?.siteName ? `(${resource.siteName})` : ""}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Credenza
|
||||
@@ -274,7 +291,7 @@ export default function CreateShareLinkForm({
|
||||
name="resourceId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="mb-2">
|
||||
<FormLabel>
|
||||
Resource
|
||||
</FormLabel>
|
||||
<Popover>
|
||||
@@ -290,14 +307,9 @@ export default function CreateShareLinkForm({
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? resources.find(
|
||||
(
|
||||
r
|
||||
) =>
|
||||
r.resourceId ===
|
||||
field.value
|
||||
? getSelectedResourceName(
|
||||
field.value
|
||||
)
|
||||
?.name
|
||||
: "Select resource"}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -305,7 +317,7 @@ export default function CreateShareLinkForm({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search resources..." />
|
||||
<CommandInput placeholder="Search resources" />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
No
|
||||
@@ -318,9 +330,7 @@ export default function CreateShareLinkForm({
|
||||
r
|
||||
) => (
|
||||
<CommandItem
|
||||
value={
|
||||
`${r.name}:${r.resourceId}`
|
||||
}
|
||||
value={`${r.name}:${r.resourceId}`}
|
||||
key={
|
||||
r.resourceId
|
||||
}
|
||||
@@ -348,9 +358,7 @@ export default function CreateShareLinkForm({
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{
|
||||
r.name
|
||||
}
|
||||
{`${r.name} ${r.siteName ? `(${r.siteName})` : ""}`}
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
@@ -369,14 +377,11 @@ export default function CreateShareLinkForm({
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>
|
||||
<FormLabel>
|
||||
Title (optional)
|
||||
</Label>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter title"
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -384,67 +389,68 @@ export default function CreateShareLinkForm({
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Label>Expire In</Label>
|
||||
<div className="grid grid-cols-2 gap-4 mt-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeUnit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
defaultValue={field.value.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{timeUnits.map(
|
||||
(
|
||||
option
|
||||
) => (
|
||||
<SelectItem
|
||||
key={
|
||||
option.unit
|
||||
}
|
||||
value={
|
||||
option.unit
|
||||
}
|
||||
>
|
||||
{
|
||||
option.name
|
||||
}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Expire In</FormLabel>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeUnit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
defaultValue={field.value.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{timeUnits.map(
|
||||
(
|
||||
option
|
||||
) => (
|
||||
<SelectItem
|
||||
key={
|
||||
option.unit
|
||||
}
|
||||
value={
|
||||
option.unit
|
||||
}
|
||||
>
|
||||
{
|
||||
option.name
|
||||
}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Enter duration"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -554,6 +560,9 @@ export default function CreateShareLinkForm({
|
||||
</div>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
@@ -562,9 +571,6 @@ export default function CreateShareLinkForm({
|
||||
>
|
||||
Create Link
|
||||
</Button>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -41,6 +41,7 @@ export type ShareLinkRow = {
|
||||
title: string | null;
|
||||
createdAt: number;
|
||||
expiresAt: number | null;
|
||||
siteName: string | null;
|
||||
};
|
||||
|
||||
type ShareLinksTableProps = {
|
||||
@@ -145,7 +146,8 @@ export default function ShareLinksTable({
|
||||
return (
|
||||
<Link href={`/${orgId}/settings/resources/${r.resourceId}`}>
|
||||
<Button variant="outline">
|
||||
{r.resourceName}
|
||||
{r.resourceName}{" "}
|
||||
{r.siteName ? `(${r.siteName})` : ""}
|
||||
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -273,6 +275,21 @@ export default function ShareLinksTable({
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import Link from "next/link";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
ChevronsUpDown,
|
||||
Loader2,
|
||||
SquareArrowOutUpRight
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@app/components/ui/collapsible";
|
||||
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
|
||||
|
||||
const createSiteFormSchema = z.object({
|
||||
name: z
|
||||
@@ -97,6 +99,8 @@ export default function CreateSiteForm({
|
||||
const [siteDefaults, setSiteDefaults] =
|
||||
useState<PickSiteDefaultsResponse | null>(null);
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState(true);
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
// setChecked?.(checked);
|
||||
setIsChecked(checked);
|
||||
@@ -121,27 +125,36 @@ export default function CreateSiteForm({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
// reset all values
|
||||
setLoading?.(false);
|
||||
setIsLoading(false);
|
||||
form.reset();
|
||||
setChecked?.(false);
|
||||
setKeypair(null);
|
||||
setSiteDefaults(null);
|
||||
const load = async () => {
|
||||
setLoadingPage(true);
|
||||
// reset all values
|
||||
setLoading?.(false);
|
||||
setIsLoading(false);
|
||||
form.reset();
|
||||
setChecked?.(false);
|
||||
setKeypair(null);
|
||||
setSiteDefaults(null);
|
||||
|
||||
const generatedKeypair = generateKeypair();
|
||||
setKeypair(generatedKeypair);
|
||||
const generatedKeypair = generateKeypair();
|
||||
setKeypair(generatedKeypair);
|
||||
|
||||
api.get(`/org/${orgId}/pick-site-defaults`)
|
||||
.catch((e) => {
|
||||
// update the default value of the form to be local method
|
||||
form.setValue("method", "local");
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.status === 200) {
|
||||
setSiteDefaults(res.data.data);
|
||||
}
|
||||
});
|
||||
await api
|
||||
.get(`/org/${orgId}/pick-site-defaults`)
|
||||
.catch((e) => {
|
||||
// update the default value of the form to be local method
|
||||
form.setValue("method", "local");
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.status === 200) {
|
||||
setSiteDefaults(res.data.data);
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
setLoadingPage(false);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [open]);
|
||||
|
||||
async function onSubmit(data: CreateSiteFormValues) {
|
||||
@@ -257,7 +270,9 @@ PersistentKeepalive = 5`
|
||||
|
||||
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">
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -272,17 +287,12 @@ PersistentKeepalive = 5`
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="Site name"
|
||||
{...field}
|
||||
/>
|
||||
<Input autoComplete="off" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the name that will be displayed for
|
||||
this site.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
This is the the display name for the site.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -319,10 +329,10 @@ PersistentKeepalive = 5`
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
This is how you will expose connections.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -335,7 +345,6 @@ PersistentKeepalive = 5`
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>
|
||||
{" "}
|
||||
Learn how to install Newt on your system
|
||||
</span>
|
||||
<SquareArrowOutUpRight size={14} />
|
||||
@@ -354,7 +363,7 @@ PersistentKeepalive = 5`
|
||||
) : form.watch("method") === "wireguard" &&
|
||||
isLoading ? (
|
||||
<p>Loading WireGuard configuration...</p>
|
||||
) : form.watch("method") === "newt" ? (
|
||||
) : form.watch("method") === "newt" && siteDefaults ? (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<Collapsible
|
||||
@@ -362,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
|
||||
@@ -376,8 +389,8 @@ PersistentKeepalive = 5`
|
||||
className="p-0 flex items-center justify-between w-full"
|
||||
>
|
||||
<h4 className="text-sm font-semibold">
|
||||
Expand for Docker Deployment
|
||||
Details
|
||||
Expand for Docker
|
||||
Deployment Details
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
@@ -409,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>
|
||||
|
||||
@@ -58,6 +58,9 @@ export default function CreateSiteFormModal({
|
||||
</div>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="create-site-form"
|
||||
@@ -69,9 +72,6 @@ export default function CreateSiteFormModal({
|
||||
>
|
||||
Create Site
|
||||
</Button>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -268,7 +268,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
<Link
|
||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||
>
|
||||
<Button variant={"outline"} className="ml-2">
|
||||
<Button variant={"outlinePrimary"} className="ml-2">
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import { Separator } from "@app/components/ui/separator";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
@@ -33,7 +32,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">Site Information</AlertTitle>
|
||||
<AlertDescription className="mt-4">
|
||||
<InfoSections>
|
||||
<InfoSections cols={2}>
|
||||
{(site.type == "newt" || site.type == "wireguard") && (
|
||||
<>
|
||||
<InfoSection>
|
||||
@@ -52,8 +51,6 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
|
||||
<Separator orientation="vertical" />
|
||||
</>
|
||||
)}
|
||||
<InfoSection>
|
||||
|
||||
@@ -33,7 +33,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useState } from "react";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string()
|
||||
name: z.string().nonempty("Name is required")
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -114,11 +114,11 @@ export default function GeneralPage() {
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
This is the display name of the
|
||||
site
|
||||
site.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -68,9 +68,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
<SiteProvider site={site}>
|
||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||
<div className="mb-8">
|
||||
<SiteInfoCard />
|
||||
</div>
|
||||
<SiteInfoCard />
|
||||
{children}
|
||||
</SidebarSettings>
|
||||
</SiteProvider>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default async function Page(props: {
|
||||
Looks like you've been invited!
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
To accept the invite, you must login or create an
|
||||
To accept the invite, you must log in or create an
|
||||
account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ import { Loader2 } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "../../../components/ui/alert";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { formatAxiosError } from "@app/lib/api";;
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
||||
@@ -182,7 +182,7 @@ export default function ResetPasswordForm({
|
||||
return;
|
||||
}
|
||||
|
||||
setSuccessMessage("Password reset successfully! Back to login...");
|
||||
setSuccessMessage("Password reset successfully! Back to log in...");
|
||||
|
||||
setTimeout(() => {
|
||||
if (redirect) {
|
||||
@@ -223,16 +223,13 @@ export default function ResetPasswordForm({
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter your email"
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
We'll send a password reset
|
||||
code to this email address.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -255,7 +252,6 @@ export default function ResetPasswordForm({
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Email"
|
||||
{...field}
|
||||
disabled
|
||||
/>
|
||||
@@ -276,12 +272,15 @@ export default function ResetPasswordForm({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter reset code sent to your email"
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
Check your email for the
|
||||
reset code.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -298,7 +297,6 @@ export default function ResetPasswordForm({
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -317,7 +315,6 @@ export default function ResetPasswordForm({
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -349,7 +346,9 @@ export default function ResetPasswordForm({
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
{...field}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
pattern={
|
||||
REGEXP_ONLY_DIGITS_AND_CHARS
|
||||
}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot
|
||||
|
||||
@@ -263,7 +263,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
}
|
||||
|
||||
if (isAllowed) {
|
||||
window.location.href = props.redirect;
|
||||
// window.location.href = props.redirect;
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +449,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter password"
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
@@ -517,7 +517,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter email"
|
||||
type="email"
|
||||
{...field}
|
||||
/>
|
||||
@@ -576,7 +575,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter OTP"
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function SignupForm({
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Email" {...field} />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -160,7 +160,6 @@ export default function SignupForm({
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -177,7 +176,6 @@ export default function SignupForm({
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default async function Page(props: {
|
||||
Looks like you've been invited!
|
||||
</h2>
|
||||
<p className="text-center">
|
||||
To accept the invite, you must login or create an
|
||||
To accept the invite, you must log in or create an
|
||||
account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -145,7 +145,6 @@ export default function VerifyEmailForm({
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Email"
|
||||
{...field}
|
||||
disabled
|
||||
/>
|
||||
@@ -196,12 +195,11 @@ export default function VerifyEmailForm({
|
||||
</InputOTP>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
We sent a verification code to your
|
||||
email address. Please enter the code
|
||||
to verify your email address.
|
||||
email address.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--border: 20 5.9% 85%;
|
||||
--input: 20 5.9% 85%;
|
||||
--border: 20 5.9% 80%;
|
||||
--input: 20 5.9% 75%;
|
||||
--ring: 24.6 95% 53.1%;
|
||||
--radius: 0.75rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
@@ -49,8 +49,8 @@
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--border: 12 6.5% 25.0%;
|
||||
--input: 12 6.5% 25.0%;
|
||||
--border: 12 6.5% 30.0%;
|
||||
--input: 12 6.5% 35.0%;
|
||||
--ring: 20.5 90.2% 48.2%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
|
||||
@@ -37,11 +37,11 @@ export default async function RootLayout({
|
||||
>
|
||||
<EnvProvider env={pullEnv()}>
|
||||
{/* Main content */}
|
||||
<div className="flex-grow">{children}</div>
|
||||
<div className="flex-grow pb-3 md:pb-0">{children}</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="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">
|
||||
<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">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<span>Pangolin</span>
|
||||
</div>
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function StepperForm() {
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Setup New Organization</CardTitle>
|
||||
<CardTitle>New Organization</CardTitle>
|
||||
<CardDescription>
|
||||
Create your organization, site, and resources
|
||||
</CardDescription>
|
||||
@@ -200,7 +200,6 @@ export default function StepperForm() {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Name your new organization"
|
||||
type="text"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
@@ -242,7 +241,6 @@ export default function StepperForm() {
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter unique organization ID"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
Reference in New Issue
Block a user