mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-13 07:25:05 +02:00
standardize header, save all button for targets, fix update site on resource
This commit is contained in:
parent
cf3cf4d827
commit
44b932937f
33 changed files with 577 additions and 397 deletions
|
@ -150,6 +150,7 @@ authenticated.get(
|
|||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyResourceAccess,
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResource),
|
||||
resource.updateResource
|
||||
);
|
||||
|
@ -370,7 +371,7 @@ authRouter.use(
|
|||
authRouter.put("/signup", auth.signup);
|
||||
authRouter.post("/login", auth.login);
|
||||
authRouter.post("/logout", auth.logout);
|
||||
authRouter.post('/newt/get-token', getToken);
|
||||
authRouter.post("/newt/get-token", getToken);
|
||||
|
||||
authRouter.post("/2fa/enable", verifySessionUserMiddleware, auth.verifyTotp);
|
||||
authRouter.post(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources } from "@server/db/schema";
|
||||
import { Resource, resources } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
@ -12,12 +12,7 @@ const getResourceSchema = z.object({
|
|||
resourceId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||
});
|
||||
|
||||
export type GetResourceResponse = {
|
||||
resourceId: number;
|
||||
siteId: number;
|
||||
orgId: string;
|
||||
name: string;
|
||||
};
|
||||
export type GetResourceResponse = Resource;
|
||||
|
||||
export async function getResource(
|
||||
req: Request,
|
||||
|
@ -53,12 +48,7 @@ export async function getResource(
|
|||
}
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
resourceId: resource[0].resourceId,
|
||||
siteId: resource[0].siteId,
|
||||
orgId: resource[0].orgId,
|
||||
name: resource[0].name,
|
||||
},
|
||||
data: resource[0],
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource retrieved successfully",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources } from "@server/db/schema";
|
||||
import { resources, sites } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
@ -17,6 +17,8 @@ const updateResourceBodySchema = z
|
|||
.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
subdomain: z.string().min(1).max(255).optional(),
|
||||
ssl: z.boolean().optional(),
|
||||
siteId: z.number(),
|
||||
})
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
message: "At least one field must be provided for update",
|
||||
|
|
|
@ -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."
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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."
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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."
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
type SettingsSectionTitleProps = {
|
||||
title: string | React.ReactNode;
|
||||
description: string | React.ReactNode;
|
||||
size?: "2xl" | "1xl";
|
||||
};
|
||||
|
||||
export default function SettingsSectionTitle({
|
||||
title,
|
||||
description,
|
||||
size,
|
||||
}: SettingsSectionTitleProps) {
|
||||
return (
|
||||
<div className="space-y-0.5 select-none mb-6">
|
||||
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
|
||||
<h2
|
||||
className={`text-${
|
||||
size ? size : "2xl"
|
||||
} font-bold tracking-tight`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ export function Toaster() {
|
|||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<Toast key={id} {...props} className="mt-2">
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
|
|
|
@ -2,7 +2,7 @@ import { GetResourceResponse } from "@server/routers/resource/getResource";
|
|||
import { createContext } from "react";
|
||||
|
||||
interface ResourceContextType {
|
||||
resource: GetResourceResponse | null;
|
||||
resource: GetResourceResponse;
|
||||
updateResource: (updatedResource: Partial<GetResourceResponse>) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,194 +1,192 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
const TOAST_LIMIT = 3;
|
||||
const TOAST_REMOVE_DELAY = 5 * 1000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const;
|
||||
|
||||
let count = 0
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) =>
|
||||
dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export { useToast, toast };
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatAxiosError(error: any, defaultMessage?: string): string {
|
||||
return (
|
||||
error.response?.data?.message ||
|
||||
error?.message ||
|
||||
defaultMessage ||
|
||||
"An error occurred"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,16 +6,15 @@ import { useState } from "react";
|
|||
|
||||
interface ResourceProviderProps {
|
||||
children: React.ReactNode;
|
||||
resource: GetResourceResponse | null;
|
||||
resource: GetResourceResponse;
|
||||
}
|
||||
|
||||
export function ResourceProvider({
|
||||
children,
|
||||
resource: serverResource,
|
||||
}: ResourceProviderProps) {
|
||||
const [resource, setResource] = useState<GetResourceResponse | null>(
|
||||
serverResource
|
||||
);
|
||||
const [resource, setResource] =
|
||||
useState<GetResourceResponse>(serverResource);
|
||||
|
||||
const updateResource = (updatedResource: Partial<GetResourceResponse>) => {
|
||||
if (!resource) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue