show resources table, check org access, and handle redirects on root
This commit is contained in:
parent
edde7a247a
commit
f6c7c017cb
14 changed files with 416 additions and 95 deletions
|
@ -91,7 +91,6 @@ declare global {
|
||||||
interface Request {
|
interface Request {
|
||||||
user?: User;
|
user?: User;
|
||||||
userOrgRoleId?: number;
|
userOrgRoleId?: number;
|
||||||
orgId?: string;
|
|
||||||
userOrgId?: string;
|
userOrgId?: string;
|
||||||
userOrgIds?: string[];
|
userOrgIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,56 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
import { db } from '@server/db';
|
import { db } from "@server/db";
|
||||||
import { orgs } from '@server/db/schema';
|
import { Org, orgs } from "@server/db/schema";
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import HttpCode from '@server/types/HttpCode';
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from 'http-errors';
|
import createHttpError from "http-errors";
|
||||||
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
|
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||||
import logger from '@server/logger';
|
import logger from "@server/logger";
|
||||||
|
|
||||||
const getOrgSchema = z.object({
|
const getOrgSchema = z.object({
|
||||||
orgId: z.string()
|
orgId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function getOrg(req: Request, res: Response, next: NextFunction): Promise<any> {
|
export type GetOrgResponse = {
|
||||||
|
org: Org;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrg(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedParams = getOrgSchema.safeParse(req.params);
|
const parsedParams = getOrgSchema.safeParse(req.params);
|
||||||
if (!parsedParams.success) {
|
if (!parsedParams.success) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
parsedParams.error.errors.map(e => e.message).join(', ')
|
parsedParams.error.errors.map((e) => e.message).join(", "),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
// Check if the user has permission to list sites
|
// Check if the user has permission to list sites
|
||||||
const hasPermission = await checkUserActionPermission(ActionsEnum.getOrg, req);
|
const hasPermission = await checkUserActionPermission(
|
||||||
|
ActionsEnum.getOrg,
|
||||||
|
req,
|
||||||
|
);
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have permission to perform this action",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const org = await db.select()
|
const org = await db
|
||||||
|
.select()
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
.where(eq(orgs.orgId, orgId))
|
.where(eq(orgs.orgId, orgId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
@ -42,13 +59,15 @@ export async function getOrg(req: Request, res: Response, next: NextFunction): P
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
`Organization with ID ${orgId} not found`
|
`Organization with ID ${orgId} not found`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response<GetOrgResponse>(res, {
|
||||||
data: org[0],
|
data: {
|
||||||
|
org: org[0],
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Organization retrieved successfully",
|
message: "Organization retrieved successfully",
|
||||||
|
@ -56,6 +75,11 @@ export async function getOrg(req: Request, res: Response, next: NextFunction): P
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred...",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,69 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
import { db } from '@server/db';
|
import { db } from "@server/db";
|
||||||
import { orgs } from '@server/db/schema';
|
import { Org, orgs } from "@server/db/schema";
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import HttpCode from '@server/types/HttpCode';
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from 'http-errors';
|
import createHttpError from "http-errors";
|
||||||
import { sql, inArray } from 'drizzle-orm';
|
import { sql, inArray } from "drizzle-orm";
|
||||||
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
|
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||||
import logger from '@server/logger';
|
import logger from "@server/logger";
|
||||||
|
|
||||||
const listOrgsSchema = z.object({
|
const listOrgsSchema = z.object({
|
||||||
limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)),
|
limit: z
|
||||||
offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)),
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().positive().default(10)),
|
||||||
|
offset: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.number().int().nonnegative().default(0)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function listOrgs(req: Request, res: Response, next: NextFunction): Promise<any> {
|
export type ListOrgsResponse = {
|
||||||
|
organizations: Org[];
|
||||||
|
pagination: { total: number; limit: number; offset: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function listOrgs(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const parsedQuery = listOrgsSchema.safeParse(req.query);
|
const parsedQuery = listOrgsSchema.safeParse(req.query);
|
||||||
if (!parsedQuery.success) {
|
if (!parsedQuery.success) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
parsedQuery.error.errors.map(e => e.message).join(', ')
|
parsedQuery.error.errors.map((e) => e.message).join(", "),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { limit, offset } = parsedQuery.data;
|
||||||
|
|
||||||
// Check if the user has permission to list sites
|
// Check if the user has permission to list sites
|
||||||
const hasPermission = await checkUserActionPermission(ActionsEnum.listOrgs, req);
|
const hasPermission = await checkUserActionPermission(
|
||||||
|
ActionsEnum.listOrgs,
|
||||||
|
req,
|
||||||
|
);
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have permission to perform this action'));
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have permission to perform this action",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the userOrgs passed from the middleware
|
// Use the userOrgs passed from the middleware
|
||||||
const userOrgIds = req.userOrgIds;
|
const userOrgIds = req.userOrgIds;
|
||||||
|
|
||||||
if (!userOrgIds || userOrgIds.length === 0) {
|
if (!userOrgIds || userOrgIds.length === 0) {
|
||||||
return res.status(HttpCode.OK).send(
|
return response<ListOrgsResponse>(res, {
|
||||||
response(res, {
|
|
||||||
data: {
|
data: {
|
||||||
organizations: [],
|
organizations: [],
|
||||||
pagination: {
|
pagination: {
|
||||||
|
@ -52,28 +76,23 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction):
|
||||||
error: false,
|
error: false,
|
||||||
message: "No organizations found for the user",
|
message: "No organizations found for the user",
|
||||||
status: HttpCode.OK,
|
status: HttpCode.OK,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizations = await db.select()
|
const organizations = await db
|
||||||
|
.select()
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
.where(inArray(orgs.orgId, userOrgIds))
|
.where(inArray(orgs.orgId, userOrgIds))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|
||||||
const totalCountResult = await db.select({ count: sql<number>`cast(count(*) as integer)` })
|
const totalCountResult = await db
|
||||||
|
.select({ count: sql<number>`cast(count(*) as integer)` })
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
.where(inArray(orgs.orgId, userOrgIds));
|
.where(inArray(orgs.orgId, userOrgIds));
|
||||||
const totalCount = totalCountResult[0].count;
|
const totalCount = totalCountResult[0].count;
|
||||||
|
|
||||||
// // Add the user's role for each organization
|
return response<ListOrgsResponse>(res, {
|
||||||
// const organizationsWithRoles = organizations.map(org => ({
|
|
||||||
// ...org,
|
|
||||||
// userRole: req.userOrgRoleIds[org.orgId],
|
|
||||||
// }));
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: {
|
data: {
|
||||||
organizations,
|
organizations,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
@ -89,6 +108,11 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction):
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"An error occurred...",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ function queryResources(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListSitesResponse = {
|
export type ListResourcesResponse = {
|
||||||
resources: NonNullable<Awaited<ReturnType<typeof queryResources>>>;
|
resources: NonNullable<Awaited<ReturnType<typeof queryResources>>>;
|
||||||
pagination: { total: number; limit: number; offset: number };
|
pagination: { total: number; limit: number; offset: number };
|
||||||
};
|
};
|
||||||
|
@ -126,7 +126,7 @@ export async function listResources(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orgId && orgId !== req.orgId) {
|
if (orgId && orgId !== req.userOrgId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
@ -167,7 +167,7 @@ export async function listResources(
|
||||||
const totalCountResult = await countQuery;
|
const totalCountResult = await countQuery;
|
||||||
const totalCount = totalCountResult[0].count;
|
const totalCount = totalCountResult[0].count;
|
||||||
|
|
||||||
return response<ListSitesResponse>(res, {
|
return response<ListResourcesResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
resources: resourcesList,
|
resources: resourcesList,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|
|
@ -77,7 +77,7 @@ export default function Header({ email, orgName, name }: HeaderProps) {
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<span className="truncate max-w-[150px] md:max-w-none">
|
<span className="truncate max-w-[150px] md:max-w-none font-medium">
|
||||||
{name || email}
|
{name || email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,10 @@ import Header from "./components/Header";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
|
import { internal } from "@app/api";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { GetOrgResponse } from "@server/routers/org";
|
||||||
|
import { authCookieHeader } from "@app/api/cookies";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Configuration",
|
title: "Configuration",
|
||||||
|
@ -43,14 +47,21 @@ export default async function ConfigurationLaytout({
|
||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
}: ConfigurationLaytoutProps) {
|
}: ConfigurationLaytoutProps) {
|
||||||
const loadUser = cache(async () => await verifySession());
|
const user = await verifySession();
|
||||||
|
|
||||||
const user = await loadUser();
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/auth/login");
|
redirect("/auth/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await internal.get<AxiosResponse<GetOrgResponse>>(
|
||||||
|
`/org/${params.orgId}`,
|
||||||
|
authCookieHeader(),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
redirect(`/`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full bg-muted mb-6 select-none sm:px-0 px-3 pt-3">
|
<div className="w-full bg-muted mb-6 select-none sm:px-0 px-3 pt-3">
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
export default async function Page() {
|
import { redirect } from "next/navigation";
|
||||||
return (
|
|
||||||
<>
|
type OrgPageProps = {
|
||||||
<p>IDK what this will show...</p>
|
params: { orgId: string };
|
||||||
</>
|
};
|
||||||
);
|
|
||||||
|
export default async function Page({ params }: OrgPageProps) {
|
||||||
|
redirect(`/${params.orgId}/sites`);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
142
src/app/[orgId]/resources/components/ResourcesDataTable.tsx
Normal file
142
src/app/[orgId]/resources/components/ResourcesDataTable.tsx
Normal file
|
@ -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 { DataTablePagination } from "@app/components/DataTablePagination";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
|
interface ResourcesDataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
data: TData[];
|
||||||
|
addResource?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourcesDataTable<TData, TValue>({
|
||||||
|
addResource,
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: ResourcesDataTableProps<TData, TValue>) {
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Search your resources"
|
||||||
|
value={
|
||||||
|
(table.getColumn("name")?.getFilterValue() as string) ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(event) =>
|
||||||
|
table
|
||||||
|
.getColumn("name")
|
||||||
|
?.setFilterValue(event.target.value)
|
||||||
|
}
|
||||||
|
className="max-w-sm mr-2"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (addResource) {
|
||||||
|
addResource();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" /> Add Resource
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef
|
||||||
|
.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={
|
||||||
|
row.getIsSelected() && "selected"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
No resources. Create one to get started.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<DataTablePagination table={table} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
84
src/app/[orgId]/resources/components/ResourcesTable.tsx
Normal file
84
src/app/[orgId]/resources/components/ResourcesTable.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { ResourcesDataTable } from "./ResourcesDataTable";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@app/components/ui/dropdown-menu";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export type ResourceRow = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const columns: ColumnDef<ResourceRow>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link
|
||||||
|
href={`/${resourceRow.orgId}/resources/${resourceRow.id}`}
|
||||||
|
>
|
||||||
|
View settings
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type ResourcesTableProps = {
|
||||||
|
resources: ResourceRow[];
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResourcesDataTable
|
||||||
|
columns={columns}
|
||||||
|
data={resources}
|
||||||
|
addResource={() => {
|
||||||
|
router.push(`/${orgId}/resources/create`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,14 +1,45 @@
|
||||||
export default async function Page() {
|
import { internal } from "@app/api";
|
||||||
|
import { authCookieHeader } from "@app/api/cookies";
|
||||||
|
import ResourcesTable, { ResourceRow } from "./components/ResourcesTable";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { ListResourcesResponse } from "@server/routers/resource";
|
||||||
|
|
||||||
|
type ResourcesPageProps = {
|
||||||
|
params: { orgId: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }: ResourcesPageProps) {
|
||||||
|
let resources: ListResourcesResponse["resources"] = [];
|
||||||
|
try {
|
||||||
|
const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
|
||||||
|
`/org/${params.orgId}/resources`,
|
||||||
|
authCookieHeader(),
|
||||||
|
);
|
||||||
|
resources = res.data.data.resources;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error fetching resources", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceRows: ResourceRow[] = resources.map((resource) => {
|
||||||
|
return {
|
||||||
|
id: resource.resourceId.toString(),
|
||||||
|
name: resource.name,
|
||||||
|
orgId: params.orgId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-0.5 select-none">
|
<div className="space-y-0.5 select-none mb-6">
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
<h2 className="text-2xl font-bold tracking-tight">
|
||||||
Manage Resources
|
Manage Resources
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Create secure proxies to your private resources.
|
Create secure proxies to your private applications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ResourcesTable resources={resourceRows} orgId={params.orgId} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,13 +177,13 @@ sh get-docker.sh`;
|
||||||
/>
|
/>
|
||||||
{form.watch("method") === "wg" && !isLoading ? (
|
{form.watch("method") === "wg" && !isLoading ? (
|
||||||
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
||||||
<code className="text-white whitespace-pre-wrap font-mono">{wgConfig}</code>
|
<code className="whitespace-pre-wrap font-mono">{wgConfig}</code>
|
||||||
</pre>
|
</pre>
|
||||||
) : form.watch("method") === "wg" && isLoading ? (
|
) : form.watch("method") === "wg" && isLoading ? (
|
||||||
<p>Loading WireGuard configuration...</p>
|
<p>Loading WireGuard configuration...</p>
|
||||||
) : (
|
) : (
|
||||||
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
||||||
<code className="text-white whitespace-pre-wrap">{newtConfig}</code>
|
<code className="whitespace-pre-wrap">{newtConfig}</code>
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { DataTablePagination } from "./DataTablePagination";
|
import { DataTablePagination } from "../../../../components/DataTablePagination";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
|
@ -32,7 +32,7 @@ interface DataTableProps<TData, TValue> {
|
||||||
addSite?: () => void;
|
addSite?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function SitesDataTable<TData, TValue>({
|
||||||
addSite,
|
addSite,
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
|
@ -59,7 +59,7 @@ export function DataTable<TData, TValue>({
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between pb-4">
|
<div className="flex items-center justify-between pb-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search sites"
|
placeholder="Search your sites"
|
||||||
value={
|
value={
|
||||||
(table.getColumn("name")?.getFilterValue() as string) ??
|
(table.getColumn("name")?.getFilterValue() as string) ??
|
||||||
""
|
""
|
||||||
|
@ -71,11 +71,13 @@ export function DataTable<TData, TValue>({
|
||||||
}
|
}
|
||||||
className="max-w-sm mr-2"
|
className="max-w-sm mr-2"
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => {
|
<Button
|
||||||
|
onClick={() => {
|
||||||
if (addSite) {
|
if (addSite) {
|
||||||
addSite();
|
addSite();
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" /> Add Site
|
<Plus className="mr-2 h-4 w-4" /> Add Site
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { DataTable } from "./DataTable";
|
import { SitesDataTable } from "./SitesDataTable";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
@ -99,7 +99,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<SitesDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={sites}
|
data={sites}
|
||||||
addSite={() => {
|
addSite={() => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue