major ui tweaks and refactoring

This commit is contained in:
Milo Schwartz
2025-01-04 20:22:01 -05:00
parent 51bf5c1408
commit 64158a823b
91 changed files with 1791 additions and 1246 deletions

View File

@@ -16,6 +16,7 @@ import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
@@ -89,7 +90,7 @@ export function ResourcesDataTable<TData, TValue>({
<Plus className="mr-2 h-4 w-4" /> Add Resource
</Button>
</div>
<div className="border rounded-md">
<TableContainer>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -141,7 +142,7 @@ export function ResourcesDataTable<TData, TValue>({
)}
</TableBody>
</Table>
</div>
</TableContainer>
<div className="mt-4">
<DataTablePagination table={table} />
</div>

View File

@@ -0,0 +1,68 @@
"use client";
import React, { useState, useEffect } from "react";
import { Server, Lock, Key, Users, X, ArrowRight } from "lucide-react"; // Replace with actual imports
import { Card, CardContent } from "@app/components/ui/card";
import { Button } from "@app/components/ui/button";
export const ResourcesSplashCard = () => {
const [isDismissed, setIsDismissed] = useState(false);
const key = "resources-splash-dismissed";
useEffect(() => {
const dismissed = localStorage.getItem(key);
if (dismissed === "true") {
setIsDismissed(true);
}
}, []);
const handleDismiss = () => {
setIsDismissed(true);
localStorage.setItem(key, "true");
};
if (isDismissed) {
return null;
}
return (
<Card className="w-full mx-auto overflow-hidden mb-8 hidden md:block relative">
<button
onClick={handleDismiss}
className="absolute top-2 right-2 p-2"
aria-label="Dismiss"
>
<X className="w-5 h-5" />
</button>
<CardContent className="grid gap-6 p-6">
<div className="space-y-4">
<h3 className="text-xl font-semibold flex items-center gap-2">
<Server className="text-blue-500" />
Resources
</h3>
<p className="text-sm">
Resources are proxies to applications running on your private network. Create a resource for any HTTP or HTTPS app on your private network.
Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.
</p>
<ul className="text-sm text-muted-foreground space-y-2">
<li className="flex items-center gap-2">
<Lock className="text-green-500 w-4 h-4" />
Secure connectivity with WireGuard encryption
</li>
<li className="flex items-center gap-2">
<Key className="text-yellow-500 w-4 h-4" />
Configure multiple authentication methods
</li>
<li className="flex items-center gap-2">
<Users className="text-purple-500 w-4 h-4" />
User and role-based access control
</li>
</ul>
</div>
</CardContent>
</Card>
);
};
export default ResourcesSplashCard;

View File

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

View File

@@ -60,9 +60,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff className="w-4 h-4" />
<span>
This resource is not protected with any
auth method. Anyone can access this
resource.
Anyone can access this resource.
</span>
</div>
)}

View File

@@ -28,16 +28,26 @@ import {
FormMessage
} from "@app/components/ui/form";
import { TagInput } from "emblor";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
// 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 SetResourcePasswordForm from "./SetResourcePasswordForm";
import { Separator } from "@app/components/ui/separator";
import SetResourcePincodeForm from "./SetResourcePincodeForm";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import {
SettingsContainer,
SettingsSection,
SettingsSectionTitle,
SettingsSectionHeader,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
SettingsSectionFooter
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
const UsersRolesFormSchema = z.object({
roles: z.array(
@@ -382,328 +392,80 @@ export default function ResourceAuthenticationPage() {
/>
)}
<div className="space-y-12">
<section className="space-y-4 lg:max-w-2xl">
<SettingsSectionTitle
title="Users & Roles"
description="Configure which users and roles can visit this resource"
size="1xl"
/>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Users & Roles
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure which users and roles can visit this
resource
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SwitchInput
id="sso-toggle"
label="Use Platform SSO"
description="Existing users will only have to login once for all resources that have this enabled."
defaultChecked={resource.sso}
onCheckedChange={(val) => setSsoEnabled(val)}
/>
<div>
<div className="flex items-center space-x-2 mb-2">
<Switch
id="sso-toggle"
defaultChecked={resource.sso}
onCheckedChange={(val) => setSsoEnabled(val)}
/>
<Label htmlFor="sso-toggle">Use Platform SSO</Label>
</div>
<span className="text-muted-foreground text-sm">
Existing users will only have to login once for all
resources that have this enabled.
</span>
</div>
<Form {...usersRolesForm}>
<form
onSubmit={usersRolesForm.handleSubmit(
onSubmitUsersRoles
)}
className="space-y-4"
>
{ssoEnabled && (
<>
<FormField
control={usersRolesForm.control}
name="roles"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>Roles</FormLabel>
<FormControl>
{/* @ts-ignore */}
<TagInput
{...field}
activeTagIndex={
activeRolesTagIndex
}
setActiveTagIndex={
setActiveRolesTagIndex
}
placeholder="Enter a role"
tags={
usersRolesForm.getValues()
.roles
}
setTags={(newRoles) => {
usersRolesForm.setValue(
"roles",
newRoles as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={
true
}
autocompleteOptions={
allRoles
}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
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>
These roles will be able to
access this resource. Admins
can always access this
resource.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={usersRolesForm.control}
name="users"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>Users</FormLabel>
<FormControl>
{/* @ts-ignore */}
<TagInput
{...field}
activeTagIndex={
activeUsersTagIndex
}
setActiveTagIndex={
setActiveUsersTagIndex
}
placeholder="Enter a user"
tags={
usersRolesForm.getValues()
.users
}
setTags={(newUsers) => {
usersRolesForm.setValue(
"users",
newUsers as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={
true
}
autocompleteOptions={
allUsers
}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
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>
)}
/>
</>
)}
<Button
type="submit"
loading={loadingSaveUsersRoles}
disabled={loadingSaveUsersRoles}
<Form {...usersRolesForm}>
<form
onSubmit={usersRolesForm.handleSubmit(
onSubmitUsersRoles
)}
id="users-roles-form"
className="space-y-4"
>
Save Users & Roles
</Button>
</form>
</Form>
</section>
<Separator />
<section className="space-y-4 lg:max-w-2xl">
<SettingsSectionTitle
title="Authentication Methods"
description="Allow access to the resource via additional auth methods"
size="1xl"
/>
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between space-x-4">
<div
className={`flex items-center text-${!authInfo.password ? "red" : "green"}-500 space-x-2`}
>
<Key />
<span>
Password Protection{" "}
{authInfo?.password
? "Enabled"
: "Disabled"}
</span>
</div>
{authInfo?.password ? (
<Button
variant="gray"
type="button"
loading={loadingRemoveResourcePassword}
disabled={loadingRemoveResourcePassword}
onClick={removeResourcePassword}
>
Remove Password
</Button>
) : (
<Button
variant="gray"
type="button"
onClick={() => setIsSetPasswordOpen(true)}
>
Add Password
</Button>
)}
</div>
<div className="flex items-center justify-between space-x-4">
<div
className={`flex items-center text-${!authInfo.pincode ? "red" : "green"}-500 space-x-2`}
>
<Binary />
<span>
PIN Code Protection{" "}
{authInfo?.pincode ? "Enabled" : "Disabled"}
</span>
</div>
{authInfo?.pincode ? (
<Button
variant="gray"
type="button"
loading={loadingRemoveResourcePincode}
disabled={loadingRemoveResourcePincode}
onClick={removeResourcePincode}
>
Remove PIN Code
</Button>
) : (
<Button
variant="gray"
type="button"
onClick={() => setIsSetPincodeOpen(true)}
>
Add PIN Code
</Button>
)}
</div>
</div>
</section>
<Separator />
<section className="space-y-4 lg:max-w-2xl">
{env.EMAIL_ENABLED === "true" && (
<>
<div>
<div className="flex items-center space-x-2 mb-2">
<Switch
id="whitelist-toggle"
defaultChecked={
resource.emailWhitelistEnabled
}
onCheckedChange={(val) =>
setWhitelistEnabled(val)
}
/>
<Label htmlFor="whitelist-toggle">
Email Whitelist
</Label>
</div>
<span className="text-muted-foreground text-sm">
Enable resource whitelist to require
email-based authentication (one-time
passwords) for resource access.
</span>
</div>
{whitelistEnabled && (
<Form {...whitelistForm}>
<form className="space-y-4">
{ssoEnabled && (
<>
<FormField
control={whitelistForm.control}
name="emails"
control={usersRolesForm.control}
name="roles"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>
Whitelisted Emails
</FormLabel>
<FormLabel>Roles</FormLabel>
<FormControl>
{/* @ts-ignore */}
<TagInput
{...field}
activeTagIndex={
activeEmailTagIndex
activeRolesTagIndex
}
validateTag={(
tag
) => {
return z
.string()
.email()
.safeParse(
tag
).success;
}}
setActiveTagIndex={
setActiveEmailTagIndex
setActiveRolesTagIndex
}
placeholder="Enter an email"
placeholder="Enter a role"
tags={
whitelistForm.getValues()
.emails
usersRolesForm.getValues()
.roles
}
setTags={(
newRoles
) => {
whitelistForm.setValue(
"emails",
usersRolesForm.setValue(
"roles",
newRoles as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={
true
}
autocompleteOptions={
allRoles
}
allowDuplicates={
false
}
restrictTagsToAutocompleteOptions={
true
}
sortTags={true}
styleClasses={{
tag: {
@@ -715,24 +477,271 @@ export default function ResourceAuthenticationPage() {
}}
/>
</FormControl>
<FormDescription>
These roles will be able
to access this resource.
Admins can always access
this resource.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
)}
<FormField
control={usersRolesForm.control}
name="users"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>Users</FormLabel>
<FormControl>
{/* @ts-ignore */}
<TagInput
{...field}
activeTagIndex={
activeUsersTagIndex
}
setActiveTagIndex={
setActiveUsersTagIndex
}
placeholder="Enter a user"
tags={
usersRolesForm.getValues()
.users
}
setTags={(
newUsers
) => {
usersRolesForm.setValue(
"users",
newUsers as [
Tag,
...Tag[]
]
);
}}
enableAutocomplete={
true
}
autocompleteOptions={
allUsers
}
allowDuplicates={
false
}
restrictTagsToAutocompleteOptions={
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>
)}
/>
</>
)}
</form>
</Form>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={loadingSaveUsersRoles}
disabled={loadingSaveUsersRoles}
form="users-roles-form"
>
Save Users & Roles
</Button>
</SettingsSectionFooter>
</SettingsSection>
<Button
loading={loadingSaveWhitelist}
disabled={loadingSaveWhitelist}
onClick={saveWhitelist}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Authentication Methods
</SettingsSectionTitle>
<SettingsSectionDescription>
Allow access to the resource via additional auth
methods
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{/* Password Protection */}
<div className="flex items-center justify-between">
<div
className={`flex items-center text-${!authInfo.password ? "neutral" : "green"}-500 space-x-2`}
>
Save Whitelist
<Key />
<span>
Password Protection{" "}
{authInfo.password ? "Enabled" : "Disabled"}
</span>
</div>
<Button
variant="outline"
onClick={
authInfo.password
? removeResourcePassword
: () => setIsSetPasswordOpen(true)
}
loading={loadingRemoveResourcePassword}
>
{authInfo.password
? "Remove Password"
: "Add Password"}
</Button>
</>
)}
</section>
</div>
</div>
{/* PIN Code Protection */}
<div className="flex items-center justify-between">
<div
className={`flex items-center text-${!authInfo.pincode ? "neutral" : "green"}-500 space-x-2`}
>
<Binary />
<span>
PIN Code Protection{" "}
{authInfo.pincode ? "Enabled" : "Disabled"}
</span>
</div>
<Button
variant="outline"
onClick={
authInfo.pincode
? removeResourcePincode
: () => setIsSetPincodeOpen(true)
}
loading={loadingRemoveResourcePincode}
>
{authInfo.pincode
? "Remove PIN Code"
: "Add PIN Code"}
</Button>
</div>
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
One-time Passwords
</SettingsSectionTitle>
<SettingsSectionDescription>
Require email-based authentication for resource
access
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{env.email.emailEnabled && (
<>
<SwitchInput
id="whitelist-toggle"
label="Email Whitelist"
defaultChecked={
resource.emailWhitelistEnabled
}
onCheckedChange={setWhitelistEnabled}
/>
{whitelistEnabled && (
<Form {...whitelistForm}>
<form id="whitelist-form">
<FormField
control={whitelistForm.control}
name="emails"
render={({ field }) => (
<FormItem>
<FormLabel>
Whitelisted Emails
</FormLabel>
<FormControl>
{/* @ts-ignore */}
{/* @ts-ignore */}
<TagInput
{...field}
activeTagIndex={
activeEmailTagIndex
}
validateTag={(
tag
) => {
return z
.string()
.email()
.safeParse(
tag
)
.success;
}}
setActiveTagIndex={
setActiveEmailTagIndex
}
placeholder="Enter an email"
tags={
whitelistForm.getValues()
.emails
}
setTags={(
newRoles
) => {
whitelistForm.setValue(
"emails",
newRoles as [
Tag,
...Tag[]
]
);
}}
allowDuplicates={
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>
</FormItem>
)}
/>
</form>
</Form>
)}
</>
)}
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
onClick={saveWhitelist}
form="whitelist-form"
loading={loadingSaveWhitelist}
>
Save Whitelist
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
</>
);
}

View File

@@ -40,28 +40,34 @@ import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow
} from "@app/components/ui/table";
import { useToast } from "@app/hooks/useToast";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { ArrayElement } from "@server/types/ArrayElement";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";;
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { createApiClient } from "@app/lib/api";
import { GetSiteResponse } from "@server/routers/site";
import {
SettingsContainer,
SettingsSection,
SettingsSectionHeader,
SettingsSectionTitle,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
SettingsSectionFooter
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
const addTargetSchema = z.object({
ip: z.string().ip(),
method: z.string(),
port: z
.string()
.refine((val) => !isNaN(Number(val)), {
message: "Port must be a number"
})
.transform((val) => Number(val))
port: z.coerce.number().int().positive()
// protocol: z.string(),
});
@@ -99,7 +105,7 @@ export default function ReverseProxyTargets(props: {
defaultValues: {
ip: "",
method: "http",
port: "80"
port: 80
// protocol: "TCP",
}
});
@@ -154,7 +160,7 @@ export default function ReverseProxyTargets(props: {
fetchSite();
}, []);
async function addTarget(data: AddTargetFormValues) {
async function addTarget(data: z.infer<typeof addTargetSchema>) {
// Check if target with same IP, port and method already exists
const isDuplicate = targets.some(
(target) =>
@@ -218,16 +224,10 @@ export default function ReverseProxyTargets(props: {
);
}
async function saveAll() {
async function saveTargets() {
try {
setLoading(true);
const res = await api.post(`/resource/${params.resourceId}`, {
ssl: sslEnabled
});
updateResource({ ssl: sslEnabled });
for (let target of targets) {
const data = {
ip: target.ip,
@@ -269,8 +269,8 @@ export default function ReverseProxyTargets(props: {
}
toast({
title: "Resource updated",
description: "Resource and targets updated successfully"
title: "Targets updated",
description: "Targets updated successfully"
});
setTargetsToRemove([]);
@@ -289,6 +289,20 @@ export default function ReverseProxyTargets(props: {
setLoading(false);
}
async function saveSsl(val: boolean) {
const res = await api.post(`/resource/${params.resourceId}`, {
ssl: val
});
setSslEnabled(val);
updateResource({ ssl: sslEnabled });
toast({
title: "SSL Configuration",
description: "SSL configuration updated successfully"
});
}
const columns: ColumnDef<LocalTarget>[] = [
{
accessorKey: "method",
@@ -410,239 +424,180 @@ export default function ReverseProxyTargets(props: {
}
return (
<>
<div className="space-y-12">
<section className="space-y-4">
<SettingsSectionTitle
title="SSL"
description="Setup SSL to secure your connections with LetsEncrypt certificates"
size="1xl"
<SettingsContainer>
{/* SSL Section */}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
SSL Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Setup SSL to secure your connections with LetsEncrypt
certificates
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SwitchInput
id="ssl-toggle"
label="Enable SSL (https)"
defaultChecked={resource.ssl}
onCheckedChange={async (val) => {
await saveSsl(val);
}}
/>
</SettingsSectionBody>
</SettingsSection>
<div className="flex items-center space-x-2">
<Switch
id="ssl-toggle"
defaultChecked={resource.ssl}
onCheckedChange={(val) => setSslEnabled(val)}
/>
<Label htmlFor="ssl-toggle">Enable SSL (https)</Label>
</div>
</section>
<hr />
<section className="space-y-4">
<SettingsSectionTitle
title="Targets"
description="Setup targets to route traffic to your services"
size="1xl"
/>
<div className="space-y-4">
<Form {...addTargetForm}>
<form
onSubmit={addTargetForm.handleSubmit(
addTarget as any
)}
className="space-y-4"
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<FormField
control={addTargetForm.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(
value
) => {
addTargetForm.setValue(
"method",
value
);
}}
>
<SelectTrigger id="method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
</SelectContent>
</Select>
</FormControl>
{/* <FormDescription> */}
{/* Choose the method for how */}
{/* the target is accessed. */}
{/* </FormDescription> */}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addTargetForm.control}
name="ip"
render={({ field }) => (
<FormItem>
<FormLabel>
IP Address
</FormLabel>
<FormControl>
<Input id="ip" {...field} />
</FormControl>
{/* <FormDescription> */}
{/* Use the IP of the resource on your private network if using Newt, or the peer IP if using raw WireGuard. */}
{/* </FormDescription> */}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addTargetForm.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input
id="port"
type="number"
{...field}
required
/>
</FormControl>
{/* <FormDescription> */}
{/* Specify the port number for */}
{/* the target. */}
{/* </FormDescription> */}
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
{/* Targets Section */}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Target Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Setup targets to route traffic to your services
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Form {...addTargetForm}>
<form
onSubmit={addTargetForm.handleSubmit(addTarget)}
className="space-y-4"
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<FormField
control={addTargetForm.control}
name="protocol"
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Protocol</FormLabel>
<FormLabel>Method</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
addTargetForm.setValue(
"protocol",
"method",
value
);
}}
>
<SelectTrigger id="protocol">
<SelectValue placeholder="Select protocol" />
<SelectTrigger id="method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="UDP">
UDP
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="TCP">
TCP
<SelectItem value="https">
https
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
Select the protocol used by the
target
</FormDescription>
<FormMessage />
</FormItem>
)}
/> */}
</div>
<Button type="submit" variant="gray">
Add Target
</Button>
</form>
</Form>
<div className="rounded-md border">
<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>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No targets. Add a target using
the form.
</TableCell>
</TableRow>
/>
<FormField
control={addTargetForm.control}
name="ip"
render={({ field }) => (
<FormItem>
<FormLabel>IP Address</FormLabel>
<FormControl>
<Input id="ip" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
</TableBody>
</Table>
</div>
/>
<FormField
control={addTargetForm.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input
id="port"
type="number"
{...field}
required
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" variant="outline">
Add Target
</Button>
</form>
</Form>
<Button
onClick={saveAll}
loading={loading}
disabled={loading}
>
Save Changes
</Button>
</div>
</section>
</div>
</>
<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>
))}
</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>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
onClick={saveTargets}
loading={loading}
disabled={loading}
>
Save Targets
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
);
}

View File

@@ -11,7 +11,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage,
FormMessage
} from "@/components/ui/form";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { Input } from "@/components/ui/input";
@@ -21,13 +21,13 @@ import {
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandList
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
PopoverTrigger
} from "@/components/ui/popover";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { ListSitesResponse } from "@server/routers/site";
@@ -37,7 +37,16 @@ import { useParams, useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { GetResourceAuthInfoResponse } from "@server/routers/resource";
import { useToast } from "@app/hooks/useToast";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
SettingsContainer,
SettingsSection,
SettingsSectionHeader,
SettingsSectionTitle,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
SettingsSectionFooter
} from "@app/components/Settings";
import { useOrgContext } from "@app/hooks/useOrgContext";
import CustomDomainInput from "../CustomDomainInput";
import ResourceInfoBox from "../ResourceInfoBox";
@@ -47,7 +56,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
const GeneralFormSchema = z.object({
name: z.string(),
subdomain: subdomainSchema,
subdomain: subdomainSchema
// siteId: z.number(),
});
@@ -72,10 +81,10 @@ export default function GeneralForm() {
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: resource.name,
subdomain: resource.subdomain,
subdomain: resource.subdomain
// siteId: resource.siteId!,
},
mode: "onChange",
mode: "onChange"
});
useEffect(() => {
@@ -95,7 +104,7 @@ export default function GeneralForm() {
`resource/${resource?.resourceId}`,
{
name: data.name,
subdomain: data.subdomain,
subdomain: data.subdomain
// siteId: data.siteId,
}
)
@@ -106,13 +115,13 @@ export default function GeneralForm() {
description: formatAxiosError(
e,
"An error occurred while updating the resource"
),
)
});
})
.then(() => {
toast({
title: "Resource updated",
description: "The resource has been updated successfully",
description: "The resource has been updated successfully"
});
updateResource({ name: data.name, subdomain: data.subdomain });
@@ -123,153 +132,85 @@ export default function GeneralForm() {
}
return (
<>
<div className="space-y-12 lg:max-w-2xl">
<section className="space-y-4">
<SettingsSectionTitle
title="General Settings"
description="Configure the general settings for this resource"
size="1xl"
/>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
General Settings
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the general settings for this resource
</SettingsSectionDescription>
</SettingsSectionHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<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>
)}
/>
<FormField
control={form.control}
name="subdomain"
render={({ field }) => (
<FormItem>
<FormLabel>Subdomain</FormLabel>
<FormControl>
<CustomDomainInput
value={field.value}
domainSuffix={domainSuffix}
placeholder="Enter subdomain"
onChange={(value) =>
form.setValue(
"subdomain",
value
)
}
/>
</FormControl>
<FormDescription>
This is the subdomain that will be
used to access the resource.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Site</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[350px] 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-[350px] p-0">
<Command>
<CommandInput placeholder="Search sites" />
<CommandList>
<CommandEmpty>
No sites found.
</CommandEmpty>
<CommandGroup>
{sites.map((site) => (
<CommandItem
value={
site.name
}
key={
site.siteId
}
onSelect={() => {
form.setValue(
"siteId",
site.siteId
);
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
field.value
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the site that will be used in
the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/> */}
<Button
type="submit"
loading={saveLoading}
disabled={saveLoading}
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
Save Changes
</Button>
</form>
</Form>
</section>
</div>
</>
<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>
)}
/>
<FormField
control={form.control}
name="subdomain"
render={({ field }) => (
<FormItem>
<FormLabel>Subdomain</FormLabel>
<FormControl>
<CustomDomainInput
value={field.value}
domainSuffix={domainSuffix}
placeholder="Enter subdomain"
onChange={(value) =>
form.setValue(
"subdomain",
value
)
}
/>
</FormControl>
<FormDescription>
This is the subdomain 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>
</SettingsContainer>
);
}

View File

@@ -8,6 +8,7 @@ import { redirect } from "next/navigation";
import { cache } from "react";
import { GetOrgResponse } from "@server/routers/org";
import OrgProvider from "@app/providers/OrgProvider";
import ResourcesSplashCard from "./ResourcesSplashCard";
type ResourcesPageProps = {
params: Promise<{ orgId: string }>;
@@ -62,6 +63,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
return (
<>
<ResourcesSplashCard />
<SettingsSectionTitle
title="Manage Resources"
description="Create secure proxies to your private applications"