diff --git a/package.json b/package.json index 07fee8aa..2fd771c2 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-separator": "1.1.0", "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-switch": "1.1.1", + "@radix-ui/react-tabs": "1.1.1", "@radix-ui/react-toast": "1.2.2", "@react-email/components": "0.0.25", "@react-email/tailwind": "0.1.0", @@ -81,11 +82,11 @@ "@types/nodemailer": "6.4.16", "@types/react": "npm:types-react@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", + "@types/ws": "8.5.13", "@types/yargs": "17.0.33", "drizzle-kit": "0.24.2", "esbuild": "0.20.1", "esbuild-node-externals": "1.13.0", - "@types/ws": "8.5.13", "eslint": "^8", "eslint-config-next": "15.0.1", "postcss": "^8", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 2c17760a..2e40f85c 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -7,7 +7,7 @@ import HttpCode from "@server/types/HttpCode"; export enum ActionsEnum { createOrg = "createOrg", - deleteOrg = "deleteOrg", + // deleteOrg = "deleteOrg", getOrg = "getOrg", updateOrg = "updateOrg", createSite = "createSite", @@ -39,16 +39,16 @@ export enum ActionsEnum { addRoleResource = "addRoleResource", removeRoleResource = "removeRoleResource", removeRoleSite = "removeRoleSite", - addRoleAction = "addRoleAction", - removeRoleAction = "removeRoleAction", + // addRoleAction = "addRoleAction", + // removeRoleAction = "removeRoleAction", listRoleSites = "listRoleSites", listRoleResources = "listRoleResources", listRoleActions = "listRoleActions", addUserRole = "addUserRole", addUserResource = "addUserResource", addUserSite = "addUserSite", - addUserAction = "addUserAction", - removeUserAction = "removeUserAction", + // addUserAction = "addUserAction", + // removeUserAction = "removeUserAction", removeUserResource = "removeUserResource", removeUserSite = "removeUserSite", } diff --git a/server/db/ensureActions.ts b/server/db/ensureActions.ts index 9d2c6011..2bd74fc5 100644 --- a/server/db/ensureActions.ts +++ b/server/db/ensureActions.ts @@ -56,14 +56,13 @@ export async function ensureActions() { } export async function createAdminRole(orgId: string) { - // Create the Default role if it doesn't exist const [insertedRole] = await db .insert(roles) .values({ orgId, isAdmin: true, name: "Admin", - description: "Admin role most permissions", + description: "Admin role with the most permissions", }) .returning({ roleId: roles.roleId }) .execute(); diff --git a/server/routers/external.ts b/server/routers/external.ts index 9cdda915..6b196486 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -53,12 +53,12 @@ authenticated.post( verifyUserHasAction(ActionsEnum.updateOrg), org.updateOrg ); -// authenticated.delete( -// "/org/:orgId", -// verifyOrgAccess, -// verifyUserIsOrgOwner, -// org.deleteOrg -// ); +authenticated.delete( + "/org/:orgId", + verifyOrgAccess, + verifyUserIsOrgOwner, + org.deleteOrg +); authenticated.put( "/org/:orgId/site", @@ -192,13 +192,13 @@ authenticated.delete( target.deleteTarget ); -// authenticated.put( -// "/org/:orgId/role", -// verifyOrgAccess, -// verifyAdmin, -// verifyUserHasAction(ActionsEnum.createRole), -// role.createRole -// ); +authenticated.put( + "/org/:orgId/role", + verifyOrgAccess, + verifyAdmin, + verifyUserHasAction(ActionsEnum.createRole), + role.createRole +); authenticated.get( "/org/:orgId/roles", verifyOrgAccess, diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index ad3a3453..ad6330ce 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -45,13 +45,6 @@ export async function createOrg( ); } - // TODO: we cant do this when they create an org because they are not in an org yet... maybe we need to make the org id optional on the userActions table - // Check if the user has permission - // const hasPermission = await checkUserActionPermission(ActionsEnum.createOrg, req); - // if (!hasPermission) { - // return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action')); - // } - const { orgId, name } = parsedBody.data; // make sure the orgId is unique @@ -113,10 +106,7 @@ export async function createOrg( } catch (error) { logger.error(error); return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred..." - ) + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 76bc9a2b..c39929c8 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { db } from "@server/db"; import { orgs, + Resource, resources, roleResources, roles, @@ -29,6 +30,8 @@ const createResourceSchema = z.object({ subdomain: z.string().min(1).max(255).optional(), }); +export type CreateResourceResponse = Resource; + export async function createResource( req: Request, res: Response, @@ -82,10 +85,8 @@ export async function createResource( ); } - // Generate a unique resourceId const fullDomain = `${subdomain}.${org[0].domain}`; - // Create new resource in the database const newResource = await db .insert(resources) .values({ @@ -122,7 +123,7 @@ export async function createResource( }); } - response(res, { + response(res, { data: newResource[0], success: true, error: false, @@ -130,12 +131,8 @@ export async function createResource( status: HttpCode.CREATED, }); } catch (error) { - throw error; return next( - createHttpError( - HttpCode.INTERNAL_SERVER_ERROR, - "An error occurred..." - ) + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } } diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index 25f5656b..4009ceaa 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -1,12 +1,14 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles } from "@server/db/schema"; +import { orgs, Role, roleActions, roles } from "@server/db/schema"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +import { ActionsEnum } from "@server/auth/actions"; +import { eq, and } from "drizzle-orm"; const createRoleParamsSchema = z.object({ orgId: z.string(), @@ -17,6 +19,8 @@ const createRoleSchema = z.object({ description: z.string().optional(), }); +export type CreateRoleResponse = Role; + export async function createRole( req: Request, res: Response, @@ -47,6 +51,25 @@ export async function createRole( const { orgId } = parsedParams.data; + const allRoles = await db + .select({ + roleId: roles.roleId, + name: roles.name, + }) + .from(roles) + .leftJoin(orgs, eq(roles.orgId, orgs.orgId)) + .where(and(eq(roles.name, roleData.name), eq(roles.orgId, orgId))); + + // make sure name is unique + if (allRoles.length > 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Role with that name already exists" + ) + ); + } + const newRole = await db .insert(roles) .values({ @@ -55,7 +78,25 @@ export async function createRole( }) .returning(); - return response(res, { + // default allowed actions for a non admin role + const allowedActions: ActionsEnum[] = [ + ActionsEnum.getOrg, + ActionsEnum.getResource, + ActionsEnum.listResources, + ]; + + await db + .insert(roleActions) + .values( + allowedActions.map((action) => ({ + roleId: newRole[0].roleId, + actionId: action, + orgId, + })) + ) + .execute(); + + return response(res, { data: newRole[0], success: true, error: false, diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 14f543f8..ce513040 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -79,6 +79,7 @@ export async function createSite( subnet, }) .returning(); + const adminRole = await db .select() .from(roles) @@ -104,7 +105,7 @@ export async function createSite( }); } - // Add the peer to the exit node + // add the peer to the exit node await addPeer(exitNodeId, { publicKey: pubKey, allowedIps: [], diff --git a/src/app/[orgId]/settings/access/layout.tsx b/src/app/[orgId]/settings/access/layout.tsx new file mode 100644 index 00000000..cb21ecd5 --- /dev/null +++ b/src/app/[orgId]/settings/access/layout.tsx @@ -0,0 +1,41 @@ +import { SidebarSettings } from "@app/components/SidebarSettings"; + +interface AccessLayoutProps { + children: React.ReactNode; + params: Promise<{ resourceId: number | string; orgId: string }>; +} + +export default async function ResourceLayout(props: AccessLayoutProps) { + const params = await props.params; + const { children } = props; + + const sidebarNavItems = [ + { + title: "Users", + href: `/{orgId}/settings/access/users`, + }, + { + title: "Roles", + href: `/{orgId}/settings/access/roles`, + }, + ]; + + return ( + <> +
+

+ Users & Roles +

+

+ Manage users and roles for your organization. +

+
+ + + {children} + + + ); +} diff --git a/src/app/[orgId]/settings/access/page.tsx b/src/app/[orgId]/settings/access/page.tsx new file mode 100644 index 00000000..229ffffb --- /dev/null +++ b/src/app/[orgId]/settings/access/page.tsx @@ -0,0 +1,12 @@ +import { redirect } from "next/navigation"; + +type AccessPageProps = { + params: Promise<{ orgId: string }>; +}; + +export default async function AccessPage(props: AccessPageProps) { + const params = await props.params; + redirect(`/${params.orgId}/settings/access/users`); + + return <>; +} diff --git a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx new file mode 100644 index 00000000..1141a5fc --- /dev/null +++ b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + SortingState, + getSortedRowModel, + ColumnFiltersState, + getFilteredRowModel, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { Input } from "@app/components/ui/input"; +import { Plus } from "lucide-react"; +import { DataTablePagination } from "@app/components/DataTablePagination"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + addRole?: () => void; +} + +export function RolesDataTable({ + addRole, + columns, + data, +}: DataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + }, + }); + + return ( +
+
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="max-w-sm mr-2" + /> + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No roles. Create a role, then add users to + the it. + + + )} + +
+
+
+ +
+
+ ); +} diff --git a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx new file mode 100644 index 00000000..3330a87c --- /dev/null +++ b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import api from "@app/api"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { useToast } from "@app/hooks/useToast"; +import { RolesDataTable } from "./RolesDataTable"; +import { Role } from "@server/db/schema"; + +export type RoleRow = Role; + +type RolesTableProps = { + roles: RoleRow[]; +}; + +export default function UsersTable({ roles }: RolesTableProps) { + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [roleToRemove, setUserToRemove] = useState(null); + + const { org } = useOrgContext(); + const { toast } = useToast(); + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "description", + header: "Description", + }, + { + id: "actions", + cell: ({ row }) => { + const roleRow = row.original; + + return ( + <> + {!roleRow.isAdmin && ( + + + + + + + + + + + )} + + ); + }, + }, + ]; + + async function removeRole() { + if (roleToRemove) { + const res = await api + .delete(`/org/${org!.org.orgId}/role/${roleToRemove.roleId}`) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to remove role", + description: + e.message ?? + "An error occurred while removing the role.", + }); + }); + + if (res && res.status === 200) { + toast({ + variant: "default", + title: "Role removed", + description: `The role ${roleToRemove.name} has been removed from the organization.`, + }); + } + } + setIsDeleteModalOpen(false); + } + + return ( + <> + { + setIsDeleteModalOpen(val); + setUserToRemove(null); + }} + dialog={ +
+

+ Are you sure you want to remove the role{" "} + {roleToRemove?.name} from the organization? +

+ +

+ You cannot undo this action. Please select a new + role to move existing users to after deletion. +

+ +

+ To confirm, please type the name of the role below. +

+
+ } + buttonText="Confirm remove role" + onConfirm={removeRole} + string={roleToRemove?.name ?? ""} + title="Remove role from organization" + /> + + + + ); +} diff --git a/src/app/[orgId]/settings/access/roles/page.tsx b/src/app/[orgId]/settings/access/roles/page.tsx new file mode 100644 index 00000000..6ec5586d --- /dev/null +++ b/src/app/[orgId]/settings/access/roles/page.tsx @@ -0,0 +1,57 @@ +import { internal } from "@app/api"; +import { authCookieHeader } from "@app/api/cookies"; +import { AxiosResponse } from "axios"; +import { GetOrgResponse } from "@server/routers/org"; +import { cache } from "react"; +import OrgProvider from "@app/providers/OrgProvider"; +import { ListRolesResponse } from "@server/routers/role"; +import RolesTable, { RoleRow } from "./components/RolesTable"; + +type RolesPageProps = { + params: Promise<{ orgId: string }>; +}; + +export default async function RolesPage(props: RolesPageProps) { + const params = await props.params; + + let roles: ListRolesResponse["roles"] = []; + const res = await internal + .get>( + `/org/${params.orgId}/roles`, + await authCookieHeader() + ) + .catch((e) => { + console.error(e); + }); + + if (res && res.status === 200) { + roles = res.data.data.roles; + } + + let org: GetOrgResponse | null = null; + const getOrg = cache(async () => + internal + .get>( + `/org/${params.orgId}`, + await authCookieHeader() + ) + .catch((e) => { + console.error(e); + }) + ); + const orgRes = await getOrg(); + + if (orgRes && orgRes.status === 200) { + org = orgRes.data.data; + } + + const roleRows: RoleRow[] = roles; + + return ( + <> + + + + + ); +} diff --git a/src/app/[orgId]/settings/users/components/InviteUserForm.tsx b/src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx similarity index 100% rename from src/app/[orgId]/settings/users/components/InviteUserForm.tsx rename to src/app/[orgId]/settings/access/users/components/InviteUserForm.tsx diff --git a/src/app/[orgId]/settings/users/components/UsersDataTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx similarity index 98% rename from src/app/[orgId]/settings/users/components/UsersDataTable.tsx rename to src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx index 9a9a69f5..c0bb1e5d 100644 --- a/src/app/[orgId]/settings/users/components/UsersDataTable.tsx +++ b/src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx @@ -22,7 +22,7 @@ import { import { Button } from "@app/components/ui/button"; import { useState } from "react"; import { Input } from "@app/components/ui/input"; -import { DataTablePagination } from "../../../../../components/DataTablePagination"; +import { DataTablePagination } from "../../../../../../components/DataTablePagination"; import { Plus } from "lucide-react"; interface DataTableProps { diff --git a/src/app/[orgId]/settings/users/components/UsersTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx similarity index 100% rename from src/app/[orgId]/settings/users/components/UsersTable.tsx rename to src/app/[orgId]/settings/access/users/components/UsersTable.tsx diff --git a/src/app/[orgId]/settings/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx similarity index 82% rename from src/app/[orgId]/settings/users/page.tsx rename to src/app/[orgId]/settings/access/users/page.tsx index 2440ff2f..68d85f5e 100644 --- a/src/app/[orgId]/settings/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -8,7 +8,6 @@ import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; -import CopyTextBox from "@app/components/CopyTextBox"; type UsersPageProps = { params: Promise<{ orgId: string }>; @@ -63,16 +62,6 @@ export default async function UsersPage(props: UsersPageProps) { return ( <> -
-

- Manage Users -

-

- Manage existing your users or invite new ones to your - organization. -

-
- diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index 1108aec0..6e161d5c 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -29,8 +29,8 @@ const topNavItems = [ icon: , }, { - title: "Users", - href: "/{orgId}/settings/users", + title: "Access", + href: "/{orgId}/settings/access", icon: , }, { diff --git a/src/app/[orgId]/settings/resources/[resourceId]/components/ClientLayout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/components/ClientLayout.tsx deleted file mode 100644 index f0c3a761..00000000 --- a/src/app/[orgId]/settings/resources/[resourceId]/components/ClientLayout.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; - -import { SidebarNav } from "@app/components/sidebar-nav"; -import { useResourceContext } from "@app/hooks/useResourceContext"; - -const sidebarNavItems = [ - { - title: "General", - href: "/{orgId}/settings/resources/{resourceId}", - }, - { - title: "Targets", - href: "/{orgId}/settings/resources/{resourceId}/targets", - }, -]; - -export function ClientLayout({ - isCreate, - children, -}: { - isCreate: boolean; - children: React.ReactNode; -}) { - const { resource } = useResourceContext(); - return ( -
-
-

- {isCreate ? "New Resource" : resource?.name + " Settings"} -

-

- {isCreate - ? "Create a new resource" - : "Configure the settings on your resource: " + - resource?.name || ""} - . -

-
-
- -
{children}
-
-
- ); -} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx index 0e6eb108..c7ae77d1 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx @@ -1,12 +1,10 @@ -import Image from "next/image"; import ResourceProvider from "@app/providers/ResourceProvider"; import { internal } from "@app/api"; import { GetResourceResponse } from "@server/routers/resource"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/api/cookies"; -import Link from "next/link"; -import { ClientLayout } from "./components/ClientLayout"; +import { SidebarSettings } from "@app/components/SidebarSettings"; interface ResourceLayoutProps { children: React.ReactNode; @@ -32,19 +30,42 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { } } + const sidebarNavItems = [ + { + title: "General", + href: `/{orgId}/settings/resources/resourceId`, + }, + { + title: "Targets", + href: `/{orgId}/settings/resources/{resourceId}/targets`, + }, + ]; + + const isCreate = params.resourceId === "create"; + return ( <> -
- +
+

+ {isCreate ? "New Resource" : resource?.name + " Settings"} +

+

+ {isCreate + ? "Create a new resource" + : "Configure the settings on your resource: " + + resource?.name || ""} + . +

- + {children} - + ); diff --git a/src/app/[orgId]/settings/sites/[niceId]/components/ClientLayout.tsx b/src/app/[orgId]/settings/sites/[niceId]/components/ClientLayout.tsx deleted file mode 100644 index d6feefdb..00000000 --- a/src/app/[orgId]/settings/sites/[niceId]/components/ClientLayout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { SidebarNav } from "@app/components/sidebar-nav"; -import { useSiteContext } from "@app/hooks/useSiteContext"; - -const sidebarNavItems = [ - { - title: "General", - href: "/{orgId}/settings/sites/{niceId}", - }, -]; - -export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) { - const { site } = useSiteContext(); - return (
-
-

- {isCreate - ? "New Site" - : site?.name + " Settings"} -

-

- {isCreate - ? "Create a new site" - : "Configure the settings on your site: " + - site?.name || ""} - . -

-
-
- -
- {children} -
-
-
); -} \ No newline at end of file diff --git a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx index 96dc25b8..972177ed 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx @@ -4,8 +4,7 @@ import { GetSiteResponse } from "@server/routers/site"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/api/cookies"; -import Link from "next/link"; -import { ClientLayout } from "./components/ClientLayout"; +import { SidebarSettings } from "@app/components/SidebarSettings"; interface SettingsLayoutProps { children: React.ReactNode; @@ -31,20 +30,37 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { } } + const sidebarNavItems = [ + { + title: "General", + href: "/{orgId}/settings/sites/{niceId}", + }, + ]; + + const isCreate = params.niceId === "create"; + return ( <> -
- +
+

+ {isCreate ? "New Site" : site?.name + " Settings"} +

+

+ {isCreate + ? "Create a new site" + : "Configure the settings on your site: " + + site?.name || ""} + . +

- - - {children} - - + + {children} + ); } diff --git a/src/components/SidebarSettings.tsx b/src/components/SidebarSettings.tsx new file mode 100644 index 00000000..55d1557d --- /dev/null +++ b/src/components/SidebarSettings.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { SidebarNav } from "@app/components/sidebar-nav"; + +interface SideBarSettingsProps { + children: React.ReactNode; + sidebarNavItems: Array<{ title: string; href: string }>; + disabled?: boolean; + limitWidth?: boolean; +} + +export function SidebarSettings({ + children, + sidebarNavItems, + disabled, + limitWidth, +}: SideBarSettingsProps) { + return ( +
+
+ +
+ {children} +
+
+
+ ); +} diff --git a/src/components/sidebar-nav.tsx b/src/components/sidebar-nav.tsx index 5ea9c8e3..6f423766 100644 --- a/src/components/sidebar-nav.tsx +++ b/src/components/sidebar-nav.tsx @@ -1,53 +1,122 @@ -"use client" -import React from 'react' -import Link from "next/link" -import { useParams, usePathname, useRouter } from "next/navigation" -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +"use client"; + +import React from "react"; +import Link from "next/link"; +import { useParams, usePathname, useRouter } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; interface SidebarNavProps extends React.HTMLAttributes { items: { - href: string - title: string - }[] - disabled?: boolean + href: string; + title: string; + }[]; + disabled?: boolean; } -export function SidebarNav({ className, items, disabled = false, ...props }: SidebarNavProps) { +export function SidebarNav({ + className, + items, + disabled = false, + ...props +}: SidebarNavProps) { const pathname = usePathname(); const params = useParams(); const orgId = params.orgId as string; const niceId = params.niceId as string; const resourceId = params.resourceId as string; + const router = useRouter(); + + const handleSelectChange = (value: string) => { + if (!disabled) { + router.push(value); + } + }; + + function getSelectedValue() { + const item = items.find((item) => hydrateHref(item.href) === pathname); + return hydrateHref(item?.href || ""); + } + + function hydrateHref(val: string): string { + return val + .replace("{orgId}", orgId) + .replace("{niceId}", niceId) + .replace("{resourceId}", resourceId); + } + return ( -