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 {
|
||||
user?: User;
|
||||
userOrgRoleId?: number;
|
||||
orgId?: string;
|
||||
userOrgId?: string;
|
||||
userOrgIds?: string[];
|
||||
}
|
||||
|
|
|
@ -1,39 +1,56 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@server/db';
|
||||
import { orgs } from '@server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { Org, orgs } from "@server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
import createHttpError from 'http-errors';
|
||||
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
|
||||
import logger from '@server/logger';
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import logger from "@server/logger";
|
||||
|
||||
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 {
|
||||
const parsedParams = getOrgSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
parsedParams.error.errors.map(e => e.message).join(', ')
|
||||
)
|
||||
parsedParams.error.errors.map((e) => e.message).join(", "),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
@ -42,13 +59,15 @@ export async function getOrg(req: Request, res: Response, next: NextFunction): P
|
|||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Organization with ID ${orgId} not found`
|
||||
)
|
||||
`Organization with ID ${orgId} not found`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: org[0],
|
||||
return response<GetOrgResponse>(res, {
|
||||
data: {
|
||||
org: org[0],
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Organization retrieved successfully",
|
||||
|
@ -56,6 +75,11 @@ export async function getOrg(req: Request, res: Response, next: NextFunction): P
|
|||
});
|
||||
} catch (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,79 +1,98 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { db } from '@server/db';
|
||||
import { orgs } from '@server/db/schema';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { Org, orgs } from "@server/db/schema";
|
||||
import response from "@server/utils/response";
|
||||
import HttpCode from '@server/types/HttpCode';
|
||||
import createHttpError from 'http-errors';
|
||||
import { sql, inArray } from 'drizzle-orm';
|
||||
import { ActionsEnum, checkUserActionPermission } from '@server/auth/actions';
|
||||
import logger from '@server/logger';
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { sql, inArray } from "drizzle-orm";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import logger from "@server/logger";
|
||||
|
||||
const listOrgsSchema = z.object({
|
||||
limit: z.string().optional().transform(Number).pipe(z.number().int().positive().default(10)),
|
||||
offset: z.string().optional().transform(Number).pipe(z.number().int().nonnegative().default(0)),
|
||||
limit: z
|
||||
.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 {
|
||||
const parsedQuery = listOrgsSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
parsedQuery.error.errors.map(e => e.message).join(', ')
|
||||
)
|
||||
parsedQuery.error.errors.map((e) => e.message).join(", "),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const { limit, offset } = parsedQuery.data;
|
||||
|
||||
// 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) {
|
||||
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
|
||||
const userOrgIds = req.userOrgIds;
|
||||
|
||||
if (!userOrgIds || userOrgIds.length === 0) {
|
||||
return res.status(HttpCode.OK).send(
|
||||
response(res, {
|
||||
data: {
|
||||
organizations: [],
|
||||
pagination: {
|
||||
total: 0,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
return response<ListOrgsResponse>(res, {
|
||||
data: {
|
||||
organizations: [],
|
||||
pagination: {
|
||||
total: 0,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "No organizations found for the user",
|
||||
status: HttpCode.OK,
|
||||
})
|
||||
);
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "No organizations found for the user",
|
||||
status: HttpCode.OK,
|
||||
});
|
||||
}
|
||||
|
||||
const organizations = await db.select()
|
||||
const organizations = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(inArray(orgs.orgId, userOrgIds))
|
||||
.limit(limit)
|
||||
.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)
|
||||
.where(inArray(orgs.orgId, userOrgIds));
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
// // Add the user's role for each organization
|
||||
// const organizationsWithRoles = organizations.map(org => ({
|
||||
// ...org,
|
||||
// userRole: req.userOrgRoleIds[org.orgId],
|
||||
// }));
|
||||
|
||||
return response(res, {
|
||||
return response<ListOrgsResponse>(res, {
|
||||
data: {
|
||||
organizations,
|
||||
pagination: {
|
||||
|
@ -89,6 +108,11 @@ export async function listOrgs(req: Request, res: Response, next: NextFunction):
|
|||
});
|
||||
} catch (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>>>;
|
||||
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(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
|
@ -167,7 +167,7 @@ export async function listResources(
|
|||
const totalCountResult = await countQuery;
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
return response<ListSitesResponse>(res, {
|
||||
return response<ListResourcesResponse>(res, {
|
||||
data: {
|
||||
resources: resourcesList,
|
||||
pagination: {
|
||||
|
|
|
@ -77,7 +77,7 @@ export default function Header({ email, orgName, name }: HeaderProps) {
|
|||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,10 @@ import Header from "./components/Header";
|
|||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import { redirect } from "next/navigation";
|
||||
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 = {
|
||||
title: "Configuration",
|
||||
|
@ -15,22 +19,22 @@ const topNavItems = [
|
|||
{
|
||||
title: "Sites",
|
||||
href: "/{orgId}/sites",
|
||||
icon: <Combine className="h-5 w-5"/>,
|
||||
icon: <Combine className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: "Resources",
|
||||
href: "/{orgId}/resources",
|
||||
icon: <Waypoints className="h-5 w-5"/>,
|
||||
icon: <Waypoints className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
href: "/{orgId}/users",
|
||||
icon: <Users className="h-5 w-5"/>,
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: "General",
|
||||
href: "/{orgId}/general",
|
||||
icon: <Cog className="h-5 w-5"/>,
|
||||
icon: <Cog className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -43,14 +47,21 @@ export default async function ConfigurationLaytout({
|
|||
children,
|
||||
params,
|
||||
}: ConfigurationLaytoutProps) {
|
||||
const loadUser = cache(async () => await verifySession());
|
||||
|
||||
const user = await loadUser();
|
||||
const user = await verifySession();
|
||||
|
||||
if (!user) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
try {
|
||||
await internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${params.orgId}`,
|
||||
authCookieHeader(),
|
||||
);
|
||||
} catch {
|
||||
redirect(`/`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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() {
|
||||
return (
|
||||
<>
|
||||
<p>IDK what this will show...</p>
|
||||
</>
|
||||
);
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
type OrgPageProps = {
|
||||
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 (
|
||||
<>
|
||||
<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">
|
||||
Manage Resources
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Create secure proxies to your private resources.
|
||||
Create secure proxies to your private applications.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ResourcesTable resources={resourceRows} orgId={params.orgId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -177,13 +177,13 @@ sh get-docker.sh`;
|
|||
/>
|
||||
{form.watch("method") === "wg" && !isLoading ? (
|
||||
<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>
|
||||
) : form.watch("method") === "wg" && isLoading ? (
|
||||
<p>Loading WireGuard configuration...</p>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
import { Button } from "@app/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { DataTablePagination } from "./DataTablePagination";
|
||||
import { DataTablePagination } from "../../../../components/DataTablePagination";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
|
@ -32,7 +32,7 @@ interface DataTableProps<TData, TValue> {
|
|||
addSite?: () => void;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
export function SitesDataTable<TData, TValue>({
|
||||
addSite,
|
||||
columns,
|
||||
data,
|
||||
|
@ -59,7 +59,7 @@ export function DataTable<TData, TValue>({
|
|||
<div>
|
||||
<div className="flex items-center justify-between pb-4">
|
||||
<Input
|
||||
placeholder="Search sites"
|
||||
placeholder="Search your sites"
|
||||
value={
|
||||
(table.getColumn("name")?.getFilterValue() as string) ??
|
||||
""
|
||||
|
@ -71,11 +71,13 @@ export function DataTable<TData, TValue>({
|
|||
}
|
||||
className="max-w-sm mr-2"
|
||||
/>
|
||||
<Button onClick={() => {
|
||||
if (addSite) {
|
||||
addSite();
|
||||
}
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (addSite) {
|
||||
addSite();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Site
|
||||
</Button>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "./DataTable";
|
||||
import { SitesDataTable } from "./SitesDataTable";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
@ -99,7 +99,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
<SitesDataTable
|
||||
columns={columns}
|
||||
data={sites}
|
||||
addSite={() => {
|
||||
|
|
Loading…
Add table
Reference in a new issue