disable eslint - new colors, and slimmer buttons/inputs??

This commit is contained in:
Milo Schwartz 2024-11-22 22:09:40 -05:00
parent bf04deb038
commit 5388c5d5b4
No known key found for this signature in database
20 changed files with 788 additions and 553 deletions

View file

@ -1,6 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

View file

@ -88,8 +88,6 @@
"drizzle-kit": "0.24.2",
"esbuild": "0.20.1",
"esbuild-node-externals": "1.13.0",
"eslint": "^8",
"eslint-config-next": "15.0.1",
"postcss": "^8",
"react-email": "3.0.1",
"tailwindcss": "^3.4.1",

View file

@ -90,7 +90,7 @@ export async function verifyResourceSession(
return allowed(res);
}
const redirectUrl = `${config.app.base_url}/auth/resource/${resource.resourceId}/login?redirect=${originalRequestURL}`;
const redirectUrl = `${config.app.base_url}/${resource.orgId}/auth/resource/${resource.resourceId}?redirect=${originalRequestURL}`;
if (sso && sessions.session) {
const { session, user } = await validateSessionToken(

View file

@ -15,7 +15,7 @@ const setResourceAuthMethodsParamsSchema = z.object({
const setResourceAuthMethodsBodySchema = z
.object({
password: z.string().nullish(),
password: z.string().min(4).max(100).nullable(),
})
.strict();

View file

@ -0,0 +1,205 @@
"use client";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { LockIcon, KeyIcon, UserIcon, Binary, Key, User } from "lucide-react";
const pinSchema = z.object({
pin: z
.string()
.length(6, { message: "PIN must be exactly 6 digits" })
.regex(/^\d+$/, { message: "PIN must only contain numbers" }),
});
const passwordSchema = z.object({
email: z.string().email({ message: "Please enter a valid email address" }),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters long" }),
});
export default function ResourceAuthPortal() {
const [activeTab, setActiveTab] = useState("pin");
const pinForm = useForm<z.infer<typeof pinSchema>>({
resolver: zodResolver(pinSchema),
defaultValues: {
pin: "",
},
});
const passwordForm = useForm<z.infer<typeof passwordSchema>>({
resolver: zodResolver(passwordSchema),
defaultValues: {
email: "",
password: "",
},
});
const onPinSubmit = (values: z.infer<typeof pinSchema>) => {
console.log("PIN authentication", values);
// Implement PIN authentication logic here
};
const onPasswordSubmit = (values: z.infer<typeof passwordSchema>) => {
console.log("Password authentication", values);
// Implement password authentication logic here
};
const handleSSOAuth = () => {
console.log("SSO authentication");
// Implement SSO authentication logic here
};
return (
<div className="w-full max-w-md mx-auto">
<Card>
<CardHeader>
<CardTitle>Welcome Back</CardTitle>
<CardDescription>
Choose your preferred login method
</CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="pin">
<Binary className="w-4 h-4 mr-1" /> PIN
</TabsTrigger>
<TabsTrigger value="password">
<Key className="w-4 h-4 mr-1" /> Password
</TabsTrigger>
<TabsTrigger value="sso">
<User className="w-4 h-4 mr-1" /> SSO
</TabsTrigger>
</TabsList>
<TabsContent value="pin">
<Form {...pinForm}>
<form
onSubmit={pinForm.handleSubmit(onPinSubmit)}
className="space-y-4"
>
<FormField
control={pinForm.control}
name="pin"
render={({ field }) => (
<FormItem>
<FormLabel>Enter PIN</FormLabel>
<FormControl>
<Input
placeholder="Enter your 6-digit PIN"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
<KeyIcon className="w-4 h-4 mr-2" />
Login with PIN
</Button>
</form>
</Form>
</TabsContent>
<TabsContent value="password">
<Form {...passwordForm}>
<form
onSubmit={passwordForm.handleSubmit(
onPasswordSubmit
)}
className="space-y-4"
>
<FormField
control={passwordForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="Enter your email"
type="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={passwordForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Enter your password"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
<LockIcon className="w-4 h-4 mr-2" />
Login with Password
</Button>
</form>
</Form>
</TabsContent>
<TabsContent value="sso">
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Click the button below to login with your
organization's SSO provider.
</p>
<Button
onClick={handleSSOAuth}
className="w-full"
>
<UserIcon className="w-4 h-4 mr-2" />
Login with SSO
</Button>
</div>
</TabsContent>
</Tabs>
</CardContent>
<CardFooter className="flex justify-center">
<p className="text-sm text-muted-foreground">
Don't have an account?{" "}
<a href="#" className="underline">
Sign up
</a>
</p>
</CardFooter>
</Card>
</div>
);
}

View file

@ -0,0 +1,17 @@
import ResourceAuthPortal from "./components/ResourceAuthPortal";
export default async function ResourceAuthPage(props: {
params: Promise<{ resourceId: number; orgId: string }>;
}) {
const params = await props.params;
console.log(params);
return (
<>
<div className="p-3 md:mt-32">
<ResourceAuthPortal />
</div>
</>
);
}

View file

@ -32,22 +32,22 @@ 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 { Input } from "@app/components/ui/input";
import { ShieldCheck } from "lucide-react";
import SetResourcePasswordForm from "./components/SetResourcePasswordForm";
import { Separator } from "@app/components/ui/separator";
const UsersRolesFormSchema = z.object({
roles: z.array(
z.object({
id: z.string(),
text: z.string(),
})
}),
),
users: z.array(
z.object({
id: z.string(),
text: z.string(),
})
}),
),
});
@ -60,10 +60,10 @@ export default function ResourceAuthenticationPage() {
const [pageLoading, setPageLoading] = useState(true);
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>(
[]
[],
);
const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>(
[]
[],
);
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
number | null
@ -73,7 +73,7 @@ export default function ResourceAuthenticationPage() {
>(null);
const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
const [blockAccess, setBlockAccess] = useState(resource.blockAccess);
// const [blockAccess, setBlockAccess] = useState(resource.blockAccess);
const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false);
const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] =
@ -96,16 +96,16 @@ export default function ResourceAuthenticationPage() {
resourceUsersResponse,
] = await Promise.all([
api.get<AxiosResponse<ListRolesResponse>>(
`/org/${org?.org.orgId}/roles`
`/org/${org?.org.orgId}/roles`,
),
api.get<AxiosResponse<ListResourceRolesResponse>>(
`/resource/${resource.resourceId}/roles`
`/resource/${resource.resourceId}/roles`,
),
api.get<AxiosResponse<ListUsersResponse>>(
`/org/${org?.org.orgId}/users`
`/org/${org?.org.orgId}/users`,
),
api.get<AxiosResponse<ListResourceUsersResponse>>(
`/resource/${resource.resourceId}/users`
`/resource/${resource.resourceId}/users`,
),
]);
@ -115,7 +115,7 @@ export default function ResourceAuthenticationPage() {
id: role.roleId.toString(),
text: role.name,
}))
.filter((role) => role.text !== "Admin")
.filter((role) => role.text !== "Admin"),
);
usersRolesForm.setValue(
@ -125,14 +125,14 @@ export default function ResourceAuthenticationPage() {
id: i.roleId.toString(),
text: i.name,
}))
.filter((role) => role.text !== "Admin")
.filter((role) => role.text !== "Admin"),
);
setAllUsers(
usersResponse.data.data.users.map((user) => ({
id: user.id.toString(),
text: user.email,
}))
})),
);
usersRolesForm.setValue(
@ -140,7 +140,7 @@ export default function ResourceAuthenticationPage() {
resourceUsersResponse.data.data.users.map((i) => ({
id: i.userId.toString(),
text: i.email,
}))
})),
);
setPageLoading(false);
@ -151,7 +151,7 @@ export default function ResourceAuthenticationPage() {
title: "Failed to fetch data",
description: formatAxiosError(
e,
"An error occurred while fetching the data"
"An error occurred while fetching the data",
),
});
}
@ -161,7 +161,7 @@ export default function ResourceAuthenticationPage() {
}, []);
async function onSubmitUsersRoles(
data: z.infer<typeof UsersRolesFormSchema>
data: z.infer<typeof UsersRolesFormSchema>,
) {
try {
setLoadingSaveUsersRoles(true);
@ -175,7 +175,6 @@ export default function ResourceAuthenticationPage() {
}),
api.post(`/resource/${resource.resourceId}`, {
sso: ssoEnabled,
blockAccess,
}),
];
@ -200,7 +199,7 @@ export default function ResourceAuthenticationPage() {
title: "Failed to set roles",
description: formatAxiosError(
e,
"An error occurred while setting the roles"
"An error occurred while setting the roles",
),
});
} finally {
@ -231,7 +230,7 @@ export default function ResourceAuthenticationPage() {
title: "Error removing resource password",
description: formatAxiosError(
e,
"An error occurred while removing the resource password"
"An error occurred while removing the resource password",
),
});
})
@ -258,168 +257,170 @@ export default function ResourceAuthenticationPage() {
/>
)}
<div className="space-y-6 lg:max-w-2xl">
{/* <div>
<div className="flex items-center space-x-2 mb-2">
<Switch
id="block-toggle"
defaultChecked={resource.blockAccess}
onCheckedChange={(val) => setBlockAccess(val)}
/>
<Label htmlFor="block-toggle">Block Access</Label>
<div className="space-y-12 lg:max-w-2xl">
<section className="space-y-6">
<SettingsSectionTitle
title="Users & Roles"
description="Configure who can visit this resource (only applicable if SSO is used)"
size="1xl"
/>
<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">Allow SSO</Label>
</div>
<span className="text-muted-foreground text-sm">
Users will be able to access the resource if they're
logged into the dashboard and have access to the
resource. Users will only have to login once for all
resources that have SSO enabled.
</span>
</div>
<span className="text-muted-foreground text-sm">
When enabled, this will prevent anyone from accessing
the resource including SSO users.
</span>
</div> */}
<SettingsSectionTitle
title="Users & Roles"
description="Configure who can visit this resource (only applicable if SSO is used)"
size="1xl"
/>
<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">Allow SSO</Label>
</div>
<span className="text-muted-foreground text-sm">
Users will be able to access the resource if they're
logged into the dashboard and have access to the
resource. Users will only have to login once for all
resources that have SSO enabled.
</span>
</div>
<Form {...usersRolesForm}>
<form
onSubmit={usersRolesForm.handleSubmit(
onSubmitUsersRoles
)}
className="space-y-6"
>
<FormField
control={usersRolesForm.control}
name="roles"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>Roles</FormLabel>
<FormControl>
<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 p-2",
},
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer:
"bg-transparent",
}}
/>
</FormControl>
<FormDescription>
Users with these roles will be able to
access this resource. Admins can always
access this resource.
</FormDescription>
<FormMessage />
</FormItem>
<Form {...usersRolesForm}>
<form
onSubmit={usersRolesForm.handleSubmit(
onSubmitUsersRoles,
)}
/>
<FormField
control={usersRolesForm.control}
name="users"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>Users</FormLabel>
<FormControl>
<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 p-2",
},
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer:
"bg-transparent",
}}
/>
</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}
className="space-y-6"
>
Save Users & Roles
</Button>
</form>
</Form>
<FormField
control={usersRolesForm.control}
name="roles"
render={({ field }) => (
<FormItem className="flex flex-col items-start">
<FormLabel>Roles</FormLabel>
<FormControl>
<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: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer:
"bg-transparent",
}}
/>
</FormControl>
<FormDescription>
Users with 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>
<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: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
inlineTagsContainer:
"bg-transparent",
}}
/>
</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}
>
Save Users & Roles
</Button>
</form>
</Form>
</section>
<SettingsSectionTitle
title="Authentication Methods"
description="You can also allow users to access the resource via the below methods"
size="1xl"
/>
<Separator />
<section className="space-y-6">
<SettingsSectionTitle
title="Authentication Methods"
description="You can also allow users to access the resource via the below methods"
size="1xl"
/>
<div>
{authInfo?.password ? (
<div className="flex items-center space-x-4">
<div className="flex items-center text-green-500 space-x-2">
@ -447,7 +448,7 @@ export default function ResourceAuthenticationPage() {
</Button>
</div>
)}
</div>
</section>
</div>
</>
);

View file

@ -39,45 +39,66 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
};
return (
<Card>
<Card className="shadow-none">
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
Resource Information
</AlertTitle>
<AlertDescription className="mt-3">
<p className="mb-2">
The current full URL for this resource is:
</p>
<div className="flex items-center space-x-2 bg-muted p-2 rounded-md">
<LinkIcon className="h-4 w-4" />
<a
href={fullUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-mono flex-grow hover:underline truncate"
>
{fullUrl}
</a>
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
className="ml-2"
type="button"
>
{copied ? (
<CheckIcon className="h-4 w-4 text-green-500" />
<div className="space-y-3">
<div>
{authInfo.password ||
authInfo.pincode ||
authInfo.sso ? (
<div className="flex items-center space-x-2 text-green-500">
<ShieldCheck />
<span>
This resource is protected with at least
one auth method.
</span>
</div>
) : (
<CopyIcon className="h-4 w-4" />
<div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff />
<span>
This resource is not protected with any
auth method. Anyone can access this
resource.
</span>
</div>
)}
<span className="ml-2">
{copied ? "Copied!" : "Copy"}
</span>
</Button>
</div>
</div>
{/* <p className="mt-3">
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md">
<LinkIcon className="h-4 w-4" />
<a
href={fullUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-mono flex-grow hover:underline truncate"
>
{fullUrl}
</a>
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
className="ml-2"
type="button"
>
{copied ? (
<CheckIcon className="h-4 w-4 text-green-500" />
) : (
<CopyIcon className="h-4 w-4" />
)}
<span className="ml-2">
{copied ? "Copied!" : "Copy"}
</span>
</Button>
</div>
{/* <p className="mt-3">
To create a proxy to your private services,{" "}
<Link
href={`/${org.org.orgId}/settings/resources/${resource.resourceId}/connectivity`}
@ -87,27 +108,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</Link>{" "}
to this resource
</p> */}
<div className="mt-3">
{authInfo.password ||
authInfo.pincode ||
authInfo.sso ? (
<div className="flex items-center space-x-2 text-green-500">
<ShieldCheck />
<span>
This resource is protected with at least one
auth method
</span>
</div>
) : (
<div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff />
<span>
This resource is not protected with any auth
method. Anyone can access this resource.
</span>
</div>
)}
</div>
</AlertDescription>
</Alert>

View file

@ -51,6 +51,7 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
import { ArrayElement } from "@server/types/ArrayElement";
import { Dot } from "lucide-react";
import { formatAxiosError } from "@app/lib/utils";
import { Separator } from "@radix-ui/react-separator";
const addTargetSchema = z.object({
ip: z.string().ip(),
@ -104,7 +105,7 @@ export default function ReverseProxyTargets(props: {
const fetchSites = async () => {
try {
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
`/resource/${params.resourceId}/targets`
`/resource/${params.resourceId}/targets`,
);
if (res.status === 200) {
@ -117,7 +118,7 @@ export default function ReverseProxyTargets(props: {
title: "Failed to fetch targets",
description: formatAxiosError(
err,
"An error occurred while fetching targets"
"An error occurred while fetching targets",
),
});
} finally {
@ -155,8 +156,8 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? { ...target, ...data, updated: true }
: target
)
: target,
),
);
}
@ -186,7 +187,7 @@ export default function ReverseProxyTargets(props: {
} else if (target.updated) {
const res = await api.post(
`/target/${target.targetId}`,
data
data,
);
}
@ -204,7 +205,7 @@ export default function ReverseProxyTargets(props: {
for (const targetId of targetsToRemove) {
await api.delete(`/target/${targetId}`);
setTargets(
targets.filter((target) => target.targetId !== targetId)
targets.filter((target) => target.targetId !== targetId),
);
}
@ -221,7 +222,7 @@ export default function ReverseProxyTargets(props: {
title: "Operation failed",
description: formatAxiosError(
err,
"An error occurred during the save operation"
"An error occurred during the save operation",
),
});
}
@ -346,110 +347,123 @@ export default function ReverseProxyTargets(props: {
}
return (
<div>
<div className="space-y-6">
<SettingsSectionTitle
title="SSL"
description="Setup SSL to secure your connections with LetsEncrypt certificates"
size="1xl"
/>
<div className="flex items-center space-x-2">
<Switch
id="ssl-toggle"
defaultChecked={resource.ssl}
onCheckedChange={(val) => setSslEnabled(val)}
<>
<div className="space-y-12">
<section className="space-y-6 lg:max-w-2xl">
<SettingsSectionTitle
title="SSL"
description="Setup SSL to secure your connections with LetsEncrypt certificates"
size="1xl"
/>
<Label htmlFor="ssl-toggle">Enable SSL (https)</Label>
</div>
<SettingsSectionTitle
title="Targets"
description="Setup targets to route traffic to your services"
size="1xl"
/>
<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>
<Form {...addTargetForm}>
<form
onSubmit={addTargetForm.handleSubmit(addTarget as any)}
>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<FormField
control={addTargetForm.control}
name="ip"
render={({ field }) => (
<FormItem>
<FormLabel>IP Address</FormLabel>
<FormControl>
<Input id="ip" {...field} />
</FormControl>
<FormDescription>
Enter the IP address of the target
</FormDescription>
<FormMessage />
</FormItem>
<hr className="lg:max-w-2xl" />
<section className="space-y-6">
<SettingsSectionTitle
title="Targets"
description="Setup targets to route traffic to your services"
size="1xl"
/>
<div className="space-y-6">
<Form {...addTargetForm}>
<form
onSubmit={addTargetForm.handleSubmit(
addTarget as any,
)}
/>
<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="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
>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<FormField
control={addTargetForm.control}
name="ip"
render={({ field }) => (
<FormItem>
<FormLabel>
IP Address
</FormLabel>
<FormControl>
<Input id="ip" {...field} />
</FormControl>
<FormDescription>
Enter the IP address of the
target.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<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="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
control={addTargetForm.control}
name="protocol"
render={({ field }) => (
@ -486,64 +500,85 @@ export default function ReverseProxyTargets(props: {
</FormItem>
)}
/> */}
</div>
<Button type="submit" variant="gray">
Add Target
</Button>
</form>
</Form>
</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()
<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>
),
)}
</TableCell>
</TableRow>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No targets. Add a target using the form.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</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>
</div>
<Button onClick={saveAll} loading={loading} disabled={loading}>
Save Changes
</Button>
<Button
onClick={saveAll}
loading={loading}
disabled={loading}
>
Save Changes
</Button>
</div>
</section>
</div>
</div>
</>
);
}

View file

@ -118,69 +118,65 @@ export default function GeneralForm() {
return (
<>
<div className="lg:max-w-2xl space-y-6">
<SettingsSectionTitle
title="General Settings"
description="Configure the general settings for this resource"
size="1xl"
/>
<div className="lg:max-w-2xl space-y-12">
<section className="space-y-6">
<SettingsSectionTitle
title="General Settings"
description="Configure the general settings for this resource"
size="1xl"
/>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<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>
)}
/>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<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>
)}
/>
<SettingsSectionTitle
title="Domain"
description="Define the domain that users will use to access this resource"
size="1xl"
/>
<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
<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 }) => (
@ -257,15 +253,16 @@ export default function GeneralForm() {
</FormItem>
)}
/> */}
<Button
type="submit"
loading={saveLoading}
disabled={saveLoading}
>
Save Changes
</Button>
</form>
</Form>
<Button
type="submit"
loading={saveLoading}
disabled={saveLoading}
>
Save Changes
</Button>
</form>
</Form>
</section>
</div>
</>
);

View file

@ -116,7 +116,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
sidebarNavItems={sidebarNavItems}
limitWidth={false}
>
<div className="mb-8">
<div className="mb-8 lg:max-w-2xl">
<ResourceInfoBox />
</div>
{children}

View file

@ -206,6 +206,7 @@ sh get-docker.sh`;
<FormLabel>Name</FormLabel>
<FormControl>
<Input
autoComplete="off"
placeholder="Site name"
{...field}
/>

View file

@ -2,83 +2,65 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 211.58 18.45% 20.2%;
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24.6 95% 53.1%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 24.6 95% 53.1%;
--radius: 0.75rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
--primary: 14.59 24.83% 29.22%;
--primary-foreground: 0 0% 100%;
--card: 20 15.79% 96.27%;
--card-foreground: 0 0% 0%;
--popover: 0 0% 100%;
--popover-foreground: 30 28.57% 2.75%;
--secondary: 25 16.07% 43.92%;
--secondary-foreground: 0 0% 100%;
--muted: 20 15.79% 96.27%;
--muted-foreground: 0 0% 34.12%;
--accent: 0 0% 86.67%;
--accent-foreground: 24 23.81% 4.12%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 12 6.67% 85.29%;
--input: 12 6.67% 85.29%;
--ring: 24.71 31.29% 31.96%;
--chart-1: 23.64 23.74% 27.25%;
--chart-2: 23.57 14.43% 38.04%;
--chart-3: 22.86 8.71% 52.75%;
--chart-4: 23.33 8.82% 60%;
--chart-5: 24 8.98% 67.25%;
--radius: 0.35rem;
}
.dark {
--background: 0 0% 11.76%;
--foreground: 204 6.67% 85.29%;
--primary: 14.21 25.68% 29.02%;
--primary-foreground: 228 13.51% 92.75%;
--card: 0 0% 9.41%;
--card-foreground: 204 6.67% 85.29%;
--popover: 0 0% 11.76%;
--popover-foreground: 24 9.09% 89.22%;
--secondary: 12.63 15.97% 23.33%;
--secondary-foreground: 0 0% 100%;
--muted: 0 0% 9.41%;
--muted-foreground: 212.73 5.31% 59.41%;
--accent: 0 2.17% 18.04%;
--accent-foreground: 24 9.09% 89.22%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 240 2.86% 27.45%;
--input: 240 2.86% 27.45%;
--ring: 23.64 23.74% 27.25%;
--chart-1: 23.64 23.74% 27.25%;
--chart-2: 23.57 23.73% 23.14%;
--chart-3: 24.55 24.44% 17.65%;
--chart-4: 23.33 23.68% 14.9%;
--chart-5: 24 23.81% 12.35%;
--radius: 0.35rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 20.5 90.2% 48.2%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 20.5 90.2% 48.2%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;

View file

@ -1,6 +1,6 @@
import type { Metadata } from "next";
import "./globals.css";
import { Fira_Sans, Inter, Noto_Sans_Mono, Roboto_Mono } from "next/font/google";
import { IBM_Plex_Sans, Work_Sans } from "next/font/google";
import { Toaster } from "@/components/ui/toaster";
import { ThemeProvider } from "@app/providers/ThemeProvider";
@ -9,8 +9,11 @@ export const metadata: Metadata = {
description: "",
};
const font = Inter({ subsets: ["latin"] });
// const font = Inter({ subsets: ["latin"] });
// const font = Noto_Sans_Mono({ subsets: ["latin"] });
const font = Work_Sans({ subsets: ["latin"] });
// const font = Space_Grotesk({subsets: ["latin"]})
// const font = IBM_Plex_Sans({subsets: ["latin"], weight: "400"})
export default async function RootLayout({
children,

View file

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

View file

@ -81,7 +81,7 @@ export function SidebarNav({
</div>
<nav
className={cn(
"hidden lg:flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
"hidden lg:flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-3",
disabled && "opacity-50 pointer-events-none",
className
)}

View file

@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full rounded-lg p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {

View file

@ -23,10 +23,10 @@ const buttonVariants = cva(
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {

View file

@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}

View file

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