standardize header, save all button for targets, fix update site on resource

This commit is contained in:
Milo Schwartz
2024-11-13 20:08:05 -05:00
parent cf3cf4d827
commit 44b932937f
33 changed files with 577 additions and 397 deletions

View File

@@ -29,6 +29,7 @@ import {
} from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/utils";
type CreateRoleFormProps = {
open: boolean;
@@ -74,9 +75,10 @@ export default function CreateRoleForm({
toast({
variant: "destructive",
title: "Failed to create role",
description:
e.response?.data?.message ||
"An error occurred while creating the role.",
description: formatAxiosError(
e,
"An error occurred while creating the role."
),
});
});

View File

@@ -36,6 +36,7 @@ import {
SelectValue,
} from "@app/components/ui/select";
import { RoleRow } from "./RolesTable";
import { formatAxiosError } from "@app/lib/utils";
type CreateRoleFormProps = {
open: boolean;
@@ -71,9 +72,10 @@ export default function DeleteRoleForm({
toast({
variant: "destructive",
title: "Failed to fetch roles",
description:
e.message ||
"An error occurred while fetching the roles",
description: formatAxiosError(
e,
"An error occurred while fetching the roles"
),
});
});
@@ -109,9 +111,10 @@ export default function DeleteRoleForm({
toast({
variant: "destructive",
title: "Failed to remove role",
description:
e.response?.data?.message ||
"An error occurred while removing the role.",
description: formatAxiosError(
e,
"An error occurred while removing the role."
),
});
});

View File

@@ -28,6 +28,8 @@ import { ListRolesResponse } from "@server/routers/role";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { useParams } from "next/navigation";
import { Button } from "@app/components/ui/button";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { formatAxiosError } from "@app/lib/utils";
const formSchema = z.object({
email: z.string().email({ message: "Please enter a valid email" }),
@@ -60,9 +62,10 @@ export default function AccessControlsPage() {
toast({
variant: "destructive",
title: "Failed to fetch roles",
description:
e.message ||
"An error occurred while fetching the roles",
description: formatAxiosError(
e,
"An error occurred while fetching the roles"
),
});
});
@@ -87,9 +90,10 @@ export default function AccessControlsPage() {
toast({
variant: "destructive",
title: "Failed to add user to role",
description:
e.response?.data?.message ||
"An error occurred while adding user to the role.",
description: formatAxiosError(
e,
"An error occurred while adding user to the role."
),
});
});
@@ -106,14 +110,11 @@ export default function AccessControlsPage() {
return (
<>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
Access Controls
</h2>
<p className="text-muted-foreground">
Manage what this user can access and do in the organization
</p>
</div>
<SettingsSectionTitle
title="Access Controls"
description="Manage what this user can access and do in the organization"
size="1xl"
/>
<Form {...form}>
<form

View File

@@ -53,7 +53,8 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
className="text-muted-foreground hover:underline"
>
<div className="flex flex-row items-center gap-1">
<ArrowLeft /> <span>All Users</span>
<ArrowLeft className="w-4 h-4" />{" "}
<span>All Users</span>
</div>
</Link>
</div>

View File

@@ -38,6 +38,7 @@ import {
} from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { ListRolesResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/utils";
type InviteUserFormProps = {
open: boolean;
@@ -94,9 +95,10 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
toast({
variant: "destructive",
title: "Failed to fetch roles",
description:
e.message ||
"An error occurred while fetching the roles",
description: formatAxiosError(
e,
"An error occurred while fetching the roles"
),
});
});
@@ -128,9 +130,10 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
toast({
variant: "destructive",
title: "Failed to invite user",
description:
e.response?.data?.message ||
"An error occurred while inviting the user.",
description: formatAxiosError(
e,
"An error occurred while inviting the user"
),
});
});

View File

@@ -19,6 +19,7 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
import { useToast } from "@app/hooks/useToast";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { formatAxiosError } from "@app/lib/utils";
export type UserRow = {
id: string;
@@ -162,9 +163,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
toast({
variant: "destructive",
title: "Failed to remove user",
description:
e.message ??
"An error occurred while removing the user.",
description: formatAxiosError(
e,
"An error occurred while removing the user."
),
});
});

View File

@@ -21,6 +21,7 @@ import {
SelectValue,
} from "@app/components/ui/select";
import { useToast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/utils";
import { ListOrgsResponse } from "@server/routers/org";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -51,6 +52,7 @@ export default function Header({ email, orgName, name, orgs }: HeaderProps) {
console.error("Error logging out", e);
toast({
title: "Error logging out",
description: formatAxiosError(e, "Error logging out"),
});
})
.then(() => {

View File

@@ -1,5 +1,6 @@
import { internal } from "@app/api";
import { authCookieHeader } from "@app/api/cookies";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { SidebarSettings } from "@app/components/SidebarSettings";
import { verifySession } from "@app/lib/auth/verifySession";
import OrgProvider from "@app/providers/OrgProvider";
@@ -67,14 +68,12 @@ export default async function GeneralSettingsPage({
<>
<OrgProvider org={org}>
<OrgUserProvider orgUser={orgUser}>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
General
</h2>
<p className="text-muted-foreground">
Configure your organization's general settings
</p>
</div>
<SettingsSectionTitle
title="General"
description="Configure your organization's general settings"
size="1xl"
/>
<SidebarSettings sidebarNavItems={sidebarNavItems}>
{children}
</SidebarSettings>

View File

@@ -1,9 +1,7 @@
"use client";
import { useEffect, useState, use } from "react";
import { Trash2, Server, Globe, Cpu } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@@ -14,13 +12,12 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import api from "@app/api";
import { AxiosResponse } from "axios";
import { ListTargetsResponse } from "@server/routers/target/listTargets";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { set, z } from "zod";
import {
Form,
FormControl,
@@ -50,16 +47,14 @@ import {
} from "@app/components/ui/table";
import { useToast } from "@app/hooks/useToast";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Target } from "@server/db/schema";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { ArrayElement } from "@server/types/ArrayElement";
import { Dot } from "lucide-react";
import { formatAxiosError } from "@app/lib/utils";
import { escape } from "querystring";
const addTargetSchema = z.object({
ip: z
.string()
.regex(
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
"Invalid IP address format"
),
ip: z.string().ip(),
method: z.string(),
port: z
.string()
@@ -72,6 +67,11 @@ const addTargetSchema = z.object({
type AddTargetFormValues = z.infer<typeof addTargetSchema>;
type LocalTarget = ArrayElement<ListTargetsResponse["targets"]> & {
new?: boolean;
updated?: boolean;
};
export default function ReverseProxyTargets(props: {
params: Promise<{ resourceId: number }>;
}) {
@@ -80,7 +80,9 @@ export default function ReverseProxyTargets(props: {
const { toast } = useToast();
const { resource, updateResource } = useResourceContext();
const [targets, setTargets] = useState<ListTargetsResponse["targets"]>([]);
const [targets, setTargets] = useState<LocalTarget[]>([]);
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
const [sslEnabled, setSslEnabled] = useState(resource.ssl);
const addTargetForm = useForm({
resolver: zodResolver(addTargetSchema),
@@ -103,9 +105,10 @@ export default function ReverseProxyTargets(props: {
toast({
variant: "destructive",
title: "Failed to fetch targets",
description:
err.message ||
"An error occurred while fetching targets",
description: formatAxiosError(
err,
"An error occurred while fetching targets"
),
});
});
@@ -117,68 +120,145 @@ export default function ReverseProxyTargets(props: {
}, []);
async function addTarget(data: AddTargetFormValues) {
const res = await api
.put<AxiosResponse<CreateTargetResponse>>(
`/resource/${params.resourceId}/target`,
{
...data,
resourceId: undefined,
}
const newTarget: LocalTarget = {
...data,
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: resource.resourceId,
};
setTargets([...targets, newTarget]);
addTargetForm.reset();
}
const removeTarget = (targetId: number) => {
setTargets([
...targets.filter((target) => target.targetId !== targetId),
]);
if (!targets.find((target) => target.targetId === targetId)?.new) {
setTargetsToRemove([...targetsToRemove, targetId]);
}
};
async function updateTarget(targetId: number, data: Partial<LocalTarget>) {
setTargets(
targets.map((target) =>
target.targetId === targetId
? { ...target, ...data, updated: true }
: target
)
);
}
async function saveAll() {
const res = await api
.post(`/resource/${params.resourceId}`, { ssl: sslEnabled })
.catch((err) => {
console.error(err);
toast({
variant: "destructive",
title: "Failed to add target",
description:
err.message || "An error occurred while adding target",
title: "Failed to update resource",
description: formatAxiosError(
err,
"Failed to update resource"
),
});
});
if (res && res.status === 201) {
setTargets([...targets, res.data.data]);
addTargetForm.reset();
}
}
const removeTarget = (targetId: number) => {
api.delete(`/target/${targetId}`)
.catch((err) => {
console.error(err);
})
.then((res) => {
setTargets(
targets.filter((target) => target.targetId !== targetId)
);
.then(() => {
updateResource({ ssl: sslEnabled });
});
};
async function updateTarget(targetId: number, data: Partial<Target>) {
setTargets(
targets.map((target) =>
target.targetId === targetId ? { ...target, ...data } : target
)
);
for (const target of targets) {
const data = {
ip: target.ip,
port: target.port,
method: target.method,
protocol: target.protocol,
enabled: target.enabled,
};
const res = await api.post(`/target/${targetId}`, data).catch((err) => {
console.error(err);
toast({
variant: "destructive",
title: "Failed to update target",
description:
err.message || "An error occurred while updating target",
});
if (target.new) {
await api
.put<AxiosResponse<CreateTargetResponse>>(
`/resource/${params.resourceId}/target`,
data
)
.then((res) => {
setTargets(
targets.map((t) => {
if (
t.new &&
t.targetId === res.data.data.targetId
) {
return {
...t,
new: false,
};
}
return t;
})
);
})
.catch((err) => {
console.error(err);
toast({
variant: "destructive",
title: "Failed to add target",
description: formatAxiosError(
err,
"Failed to add target"
),
});
});
} else if (target.updated) {
const res = await api
.post(`/target/${target.targetId}`, data)
.catch((err) => {
console.error(err);
toast({
variant: "destructive",
title: "Failed to update target",
description: formatAxiosError(
err,
"Failed to update target"
),
});
});
}
}
for (const targetId of targetsToRemove) {
await api
.delete(`/target/${targetId}`)
.catch((err) => {
console.error(err);
toast({
variant: "destructive",
title: "Failed to remove target",
description: formatAxiosError(
err,
"Failed to remove target"
),
});
})
.then((res) => {
setTargets(
targets.filter((target) => target.targetId !== targetId)
);
});
}
toast({
title: "Resource updated",
description: "Resource and targets updated successfully",
});
if (res && res.status === 200) {
toast({
title: "Target updated",
description: "The target has been updated successfully",
});
}
setTargetsToRemove([]);
}
const columns: ColumnDef<ListTargetsResponse["targets"][0]>[] = [
const columns: ColumnDef<LocalTarget>[] = [
{
accessorKey: "ip",
header: "IP Address",
@@ -259,12 +339,17 @@ export default function ReverseProxyTargets(props: {
{
id: "actions",
cell: ({ row }) => (
<Button
variant="outline"
onClick={() => removeTarget(row.original.targetId)}
>
Delete
</Button>
<>
<div className="flex items-center justify-end space-x-2">
{row.original.new && <Dot />}
<Button
variant="outline"
onClick={() => removeTarget(row.original.targetId)}
>
Delete
</Button>
</div>
</>
),
},
];
@@ -286,10 +371,15 @@ export default function ReverseProxyTargets(props: {
<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" />
<Switch
id="ssl-toggle"
defaultChecked={resource.ssl}
onCheckedChange={(val) => setSslEnabled(val)}
/>
<Label htmlFor="ssl-toggle">Enable SSL (https)</Label>
</div>
</div>
@@ -298,6 +388,7 @@ export default function ReverseProxyTargets(props: {
<SettingsSectionTitle
title="Targets"
description="Setup targets to route traffic to your services"
size="1xl"
/>
<Form {...addTargetForm}>
@@ -355,8 +446,8 @@ export default function ReverseProxyTargets(props: {
</Select>
</FormControl>
<FormDescription>
Choose the method for the target
connection
Choose the method for how the
target is accessed
</FormDescription>
<FormMessage />
</FormItem>
@@ -422,7 +513,9 @@ export default function ReverseProxyTargets(props: {
)}
/>
</div>
<Button type="submit">Add Target</Button>
<Button type="submit" variant="gray">
Add Target
</Button>
</form>
</Form>
</div>
@@ -466,7 +559,7 @@ export default function ReverseProxyTargets(props: {
colSpan={columns.length}
className="h-24 text-center"
>
No results.
No targets. Add a target using the form.
</TableCell>
</TableRow>
)}
@@ -474,6 +567,10 @@ export default function ReverseProxyTargets(props: {
</Table>
</div>
</div>
<div className="mt-8">
<Button onClick={saveAll}>Save Changes</Button>
</div>
</div>
);
}

View File

@@ -2,7 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { cn } from "@/lib/utils";
import { cn, formatAxiosError } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -38,6 +38,7 @@ import { useParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { GetResourceResponse } from "@server/routers/resource";
import { useToast } from "@app/hooks/useToast";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
const GeneralFormSchema = z.object({
name: z.string(),
@@ -48,64 +49,71 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
export default function GeneralForm() {
const params = useParams();
const orgId = params.orgId;
const { resource, updateResource } = useResourceContext();
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const { toast } = useToast();
const { resource, updateResource } = useResourceContext();
const orgId = params.orgId;
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const [saveLoading, setSaveLoading] = useState(false);
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: resource?.name,
siteId: resource?.siteId,
name: resource.name,
siteId: resource.siteId!,
},
mode: "onChange",
});
useEffect(() => {
if (typeof window !== "undefined") {
const fetchSites = async () => {
const res = await api.get<AxiosResponse<ListSitesResponse>>(
`/org/${orgId}/sites/`
);
setSites(res.data.data.sites);
};
fetchSites();
}
const fetchSites = async () => {
const res = await api.get<AxiosResponse<ListSitesResponse>>(
`/org/${orgId}/sites/`
);
setSites(res.data.data.sites);
};
fetchSites();
}, []);
async function onSubmit(data: GeneralFormValues) {
setSaveLoading(true);
updateResource({ name: data.name, siteId: data.siteId });
await api
.post<AxiosResponse<GetResourceResponse>>(
`resource/${resource?.resourceId}`,
{
name: data.name,
siteId: data.siteId,
}
)
api.post<AxiosResponse<GetResourceResponse>>(
`resource/${resource?.resourceId}`,
{
name: data.name,
siteId: data.siteId,
}
)
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update resource",
description:
e.response?.data?.message ||
"An error occurred while updating the resource",
description: formatAxiosError(
e,
"An error occurred while updating the resource"
),
});
});
})
.then(() => {
toast({
title: "Resource updated",
description: "The resource has been updated successfully",
});
})
.finally(() => setSaveLoading(false));
}
return (
<>
<div className="lg:max-w-2xl">
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
General Settings
</h2>
<p className="text-muted-foreground">
Configure the general settings for this resource
</p>
</div>
<SettingsSectionTitle
title="General Settings"
description="Configure the general settings for this resource"
size="1xl"
/>
<Form {...form}>
<form
@@ -160,10 +168,10 @@ export default function GeneralForm() {
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0">
<Command>
<CommandInput placeholder="Search site..." />
<CommandInput placeholder="Search sites" />
<CommandList>
<CommandEmpty>
No site found.
No sites found.
</CommandEmpty>
<CommandGroup>
{sites.map((site) => (
@@ -206,7 +214,13 @@ export default function GeneralForm() {
</FormItem>
)}
/>
<Button type="submit">Update Resource</Button>
<Button
type="submit"
loading={saveLoading}
disabled={saveLoading}
>
Save Changes
</Button>
</form>
</Form>
</div>

View File

@@ -7,6 +7,7 @@ import { authCookieHeader } from "@app/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
interface ResourceLayoutProps {
children: React.ReactNode;
@@ -53,19 +54,16 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
className="text-muted-foreground hover:underline"
>
<div className="flex flex-row items-center gap-1">
<ArrowLeft /> <span>All Resources</span>
<ArrowLeft className="w-4 h-4" />{" "}
<span>All Resources</span>
</div>
</Link>
</div>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
{resource?.name + " Settings"}
</h2>
<p className="text-muted-foreground">
Configure the settings on your resource
</p>
</div>
<SettingsSectionTitle
title={`${resource?.name} Settings`}
description="Configure the settings on your resource"
/>
<ResourceProvider resource={resource}>
<SidebarSettings

View File

@@ -29,7 +29,7 @@ import {
} from "@app/components/Credenza";
import { useParams, useRouter } from "next/navigation";
import { ListSitesResponse } from "@server/routers/site";
import { cn } from "@app/lib/utils";
import { cn, formatAxiosError } from "@app/lib/utils";
import { CheckIcon } from "lucide-react";
import {
Popover,
@@ -123,7 +123,12 @@ export default function CreateResourceForm({
)
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating resource",
description: formatAxiosError(
e,
"An error occurred when creating the resource"
),
});
});

View File

@@ -17,6 +17,8 @@ import CreateResourceForm from "./CreateResourceForm";
import { useState } from "react";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { set } from "zod";
import { formatAxiosError } from "@app/lib/utils";
import { useToast } from "@app/hooks/useToast";
export type ResourceRow = {
id: number;
@@ -34,6 +36,8 @@ type ResourcesTableProps = {
export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const router = useRouter();
const { toast } = useToast();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedResource, setSelectedResource] =
@@ -43,6 +47,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
api.delete(`/resource/${resourceId}`)
.catch((e) => {
console.error("Error deleting resource", e);
toast({
variant: "destructive",
title: "Error deleting resource",
description: formatAxiosError(e, "Error deleting resource"),
});
})
.then(() => {
router.refresh();

View File

@@ -3,6 +3,7 @@ import { authCookieHeader } from "@app/api/cookies";
import ResourcesTable, { ResourceRow } from "./components/ResourcesTable";
import { AxiosResponse } from "axios";
import { ListResourcesResponse } from "@server/routers/resource";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
type ResourcesPageProps = {
params: Promise<{ orgId: string }>;
@@ -33,14 +34,10 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
return (
<>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
Manage Resources
</h2>
<p className="text-muted-foreground">
Create secure proxies to your private applications.
</p>
</div>
<SettingsSectionTitle
title="Manage Resources"
description="Create secure proxies to your private applications"
/>
<ResourcesTable resources={resourceRows} orgId={params.orgId} />
</>

View File

@@ -18,6 +18,8 @@ import { useForm } from "react-hook-form";
import api from "@app/api";
import { useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { formatAxiosError } from "@app/lib/utils";
const GeneralFormSchema = z.object({
name: z.string(),
@@ -41,18 +43,19 @@ export default function GeneralPage() {
async function onSubmit(data: GeneralFormValues) {
await api
.post(`/site/${site?.siteId}`, {
name: data.name,
})
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
description:
e.message ||
"An error occurred while updating the site.",
.post(`/site/${site?.siteId}`, {
name: data.name,
})
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
description: formatAxiosError(
e,
"An error occurred while updating the site."
),
});
});
});
updateSite({ name: data.name });
@@ -61,14 +64,11 @@ export default function GeneralPage() {
return (
<>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
General Settings
</h2>
<p className="text-muted-foreground">
Configure the general settings for this site
</p>
</div>
<SettingsSectionTitle
title="General Settings"
description="Configure the general settings for this site"
size="1xl"
/>
<Form {...form}>
<form

View File

@@ -7,6 +7,7 @@ import { authCookieHeader } from "@app/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
interface SettingsLayoutProps {
children: React.ReactNode;
@@ -44,19 +45,15 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
className="text-muted-foreground hover:underline"
>
<div className="flex flex-row items-center gap-1">
<ArrowLeft /> <span>All Sites</span>
<ArrowLeft className="w-4 h-4" /> <span>All Sites</span>
</div>
</Link>
</div>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
{site?.name + " Settings"}
</h2>
<p className="text-muted-foreground">
Configure the settings on your site
</p>
</div>
<SettingsSectionTitle
title={`${site?.name} Settings`}
description="Configure the settings on your site"
/>
<SiteProvider site={site}>
<SidebarSettings

View File

@@ -40,6 +40,7 @@ import {
SelectTrigger,
SelectValue,
} from "@app/components/ui/select";
import { formatAxiosError } from "@app/lib/utils";
const method = [
{ label: "Wireguard", value: "wg" },
@@ -108,7 +109,9 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
api.get(`/org/${orgId}/pick-site-defaults`)
.catch((e) => {
toast({
variant: "destructive",
title: "Error picking site defaults",
description: formatAxiosError(e),
});
})
.then((res) => {
@@ -130,7 +133,9 @@ export default function CreateSiteForm({ open, setOpen }: CreateSiteFormProps) {
})
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating site",
description: formatAxiosError(e),
});
});

View File

@@ -17,6 +17,8 @@ import { AxiosResponse } from "axios";
import { useState } from "react";
import CreateSiteForm from "./CreateSiteForm";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useToast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/utils";
export type SiteRow = {
id: number;
@@ -35,6 +37,8 @@ type SitesTableProps = {
export default function SitesTable({ sites, orgId }: SitesTableProps) {
const router = useRouter();
const { toast } = useToast();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
@@ -48,6 +52,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
api.delete(`/site/${siteId}`)
.catch((e) => {
console.error("Error deleting site", e);
toast({
variant: "destructive",
title: "Error deleting site",
description: formatAxiosError(e, "Error deleting site"),
});
})
.then(() => {
router.refresh();

View File

@@ -3,6 +3,7 @@ import { authCookieHeader } from "@app/api/cookies";
import { ListSitesResponse } from "@server/routers/site";
import { AxiosResponse } from "axios";
import SitesTable, { SiteRow } from "./components/SitesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
type SitesPageProps = {
params: Promise<{ orgId: string }>;
@@ -34,14 +35,10 @@ export default async function SitesPage(props: SitesPageProps) {
return (
<>
<div className="space-y-0.5 select-none mb-6">
<h2 className="text-2xl font-bold tracking-tight">
Manage Sites
</h2>
<p className="text-muted-foreground">
Manage your existing sites here or create a new one.
</p>
</div>
<SettingsSectionTitle
title="Manage Sites"
description="Manage your existing sites here or create a new one."
/>
<SitesTable sites={siteRows} orgId={params.orgId} />
</>

View File

@@ -26,6 +26,7 @@ import { LoginResponse } from "@server/routers/auth";
import { api } from "@app/api";
import { useRouter } from "next/navigation";
import { AxiosResponse } from "axios";
import { formatAxiosError } from "@app/lib/utils";
type LoginFormProps = {
redirect?: string;
@@ -65,8 +66,7 @@ export default function LoginForm({ redirect }: LoginFormProps) {
.catch((e) => {
console.error(e);
setError(
e.response?.data?.message ||
"An error occurred while logging in",
formatAxiosError(e, "An error occurred while logging in")
);
});
@@ -146,7 +146,11 @@ export default function LoginForm({ redirect }: LoginFormProps) {
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button type="submit" className="w-full" loading={loading}>
<Button
type="submit"
className="w-full"
loading={loading}
>
Login
</Button>
</form>

View File

@@ -27,6 +27,7 @@ import { api } from "@app/api";
import { useRouter } from "next/navigation";
import { passwordSchema } from "@server/auth/passwordSchema";
import { AxiosResponse } from "axios";
import { formatAxiosError } from "@app/lib/utils";
type SignupFormProps = {
redirect?: string;
@@ -70,8 +71,7 @@ export default function SignupForm({ redirect }: SignupFormProps) {
.catch((e) => {
console.error(e);
setError(
e.response?.data?.message ||
"An error occurred while signing up",
formatAxiosError(e, "An error occurred while signing up")
);
});

View File

@@ -34,6 +34,7 @@ import { Loader2 } from "lucide-react";
import { Alert, AlertDescription } from "../../../components/ui/alert";
import { useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
import { formatAxiosError } from "@app/lib/utils";
const FormSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
@@ -76,14 +77,14 @@ export default function VerifyEmailForm({
code: data.pin,
})
.catch((e) => {
setError(e.response?.data?.message || "An error occurred");
setError(formatAxiosError(e, "An error occurred"));
console.error("Failed to verify email:", e);
});
if (res && res.data?.data?.valid) {
setError(null);
setSuccessMessage(
"Email successfully verified! Redirecting you...",
"Email successfully verified! Redirecting you..."
);
setTimeout(() => {
if (redirect && redirect.includes("http")) {
@@ -103,7 +104,7 @@ export default function VerifyEmailForm({
setIsResending(true);
const res = await api.post("/auth/verify-email/request").catch((e) => {
setError(e.response?.data?.message || "An error occurred");
setError(formatAxiosError(e, "An error occurred"));
console.error("Failed to resend verification code:", e);
});

View File

@@ -5,6 +5,7 @@ import { AcceptInviteResponse } from "@server/routers/user";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import InviteStatusCard from "./InviteStatusCard";
import { formatAxiosError } from "@app/lib/utils";
export default async function InvitePage(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -47,8 +48,7 @@ export default async function InvitePage(props: {
await authCookieHeader()
)
.catch((e) => {
error = e.response?.data?.message;
console.log(error);
console.error(e);
});
if (res && res.status === 200) {

View File

@@ -15,6 +15,7 @@ import {
CardTitle,
} from "@app/components/ui/card";
import CopyTextBox from "@app/components/CopyTextBox";
import { formatAxiosError } from "@app/lib/utils";
type Step = "org" | "site" | "resources";
@@ -43,7 +44,7 @@ export default function StepperForm() {
const debouncedCheckOrgIdAvailability = useCallback(
debounce(checkOrgIdAvailability, 300),
[checkOrgIdAvailability],
[checkOrgIdAvailability]
);
useEffect(() => {
@@ -76,7 +77,9 @@ export default function StepperForm() {
})
.catch((e) => {
toast({
title: "Error creating org...",
variant: "destructive",
title: "Error creating org",
description: formatAxiosError(e),
});
});
@@ -106,36 +109,60 @@ export default function StepperForm() {
<div className="flex justify-between mb-2">
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${currentStep === "org" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
currentStep === "org"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
}`}
>
1
</div>
<span
className={`text-sm font-medium ${currentStep === "org" ? "text-primary" : "text-muted-foreground"}`}
className={`text-sm font-medium ${
currentStep === "org"
? "text-primary"
: "text-muted-foreground"
}`}
>
Create Org
</span>
</div>
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${currentStep === "site" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
currentStep === "site"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
}`}
>
2
</div>
<span
className={`text-sm font-medium ${currentStep === "site" ? "text-primary" : "text-muted-foreground"}`}
className={`text-sm font-medium ${
currentStep === "site"
? "text-primary"
: "text-muted-foreground"
}`}
>
Create Site
</span>
</div>
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${currentStep === "resources" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`}
className={`w-8 h-8 rounded-full flex items-center justify-center mb-2 ${
currentStep === "resources"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
}`}
>
3
</div>
<span
className={`text-sm font-medium ${currentStep === "resources" ? "text-primary" : "text-muted-foreground"}`}
className={`text-sm font-medium ${
currentStep === "resources"
? "text-primary"
: "text-muted-foreground"
}`}
>
Create Resources
</span>
@@ -251,7 +278,7 @@ export default function StepperForm() {
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;