mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-28 14:44:55 +02:00
small clients ui tweaks
This commit is contained in:
parent
f960fb7d67
commit
fa6fc9e80d
25 changed files with 215 additions and 377 deletions
|
@ -2,7 +2,7 @@ import {
|
|||
encodeHexLowerCase,
|
||||
} from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Olm, olms, olmSessions, OlmSession } from "@server/db/schema";
|
||||
import { Olm, olms, olmSessions, OlmSession } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import db from "@server/db";
|
||||
import { clients, orgs, sites } from "@server/db/schema";
|
||||
import { clients, orgs, sites } from "@server/db/schemas";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { db } from "@server/db";
|
||||
import { userOrgs, clients, roleClients, userClients } from "@server/db/schema";
|
||||
import { userOrgs, clients, roleClients, userClients } from "@server/db/schemas";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
exitNodes,
|
||||
orgs,
|
||||
sites
|
||||
} from "@server/db/schema";
|
||||
} from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { clients, clientSites } from "@server/db/schema";
|
||||
import { clients, clientSites } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { clients } from "@server/db/schema";
|
||||
import { clients } from "@server/db/schemas";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
sites,
|
||||
userClients,
|
||||
clientSites
|
||||
} from "@server/db/schema";
|
||||
} from "@server/db/schemas";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
|
@ -53,6 +53,7 @@ function queryClients(orgId: string, accessibleClientIds: number[]) {
|
|||
})
|
||||
.from(clients)
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.where(
|
||||
and(
|
||||
inArray(clients.clientId, accessibleClientIds),
|
||||
|
|
|
@ -4,7 +4,7 @@ import { db } from "@server/db";
|
|||
import {
|
||||
clients,
|
||||
clientSites
|
||||
} from "@server/db/schema";
|
||||
} from "@server/db/schemas";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { clients, exitNodes, newts, olms, Site, sites, clientSites } from "@server/db/schema";
|
||||
import { clients, exitNodes, newts, olms, Site, sites, clientSites } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { clients, newts, olms, Site, sites, clientSites } from "@server/db/schema";
|
||||
import { clients, newts, olms, Site, sites, clientSites } from "@server/db/schemas";
|
||||
import { db } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
|
|
@ -3,7 +3,7 @@ import { MessageHandler } from "../ws";
|
|||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import db from "@server/db";
|
||||
import { clients, clientSites, Newt, sites } from "@server/db/schema";
|
||||
import { clients, clientSites, Newt, sites } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { updatePeer } from "../olm/peers";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import db from "@server/db";
|
||||
import { MessageHandler } from "../ws";
|
||||
import { clients, Newt } from "@server/db/schema";
|
||||
import { clients, Newt } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
import db from '@server/db';
|
||||
import { newts, sites } from '@server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { sendToClient } from '../ws';
|
||||
import logger from '@server/logger';
|
||||
import db from "@server/db";
|
||||
import { newts, sites } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sendToClient } from "../ws";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function addPeer(siteId: number, peer: {
|
||||
publicKey: string;
|
||||
allowedIps: string[];
|
||||
endpoint: string;
|
||||
}) {
|
||||
|
||||
const [site] = await db.select().from(sites).where(eq(sites.siteId, siteId)).limit(1);
|
||||
export async function addPeer(
|
||||
siteId: number,
|
||||
peer: {
|
||||
publicKey: string;
|
||||
allowedIps: string[];
|
||||
endpoint: string;
|
||||
}
|
||||
) {
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
if (!site) {
|
||||
throw new Error(`Exit node with ID ${siteId} not found`);
|
||||
}
|
||||
|
||||
// get the newt on the site
|
||||
const [newt] = await db.select().from(newts).where(eq(newts.siteId, siteId)).limit(1);
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
if (!newt) {
|
||||
throw new Error(`Site found for site ${siteId}`);
|
||||
}
|
||||
|
||||
sendToClient(newt.newtId, {
|
||||
type: 'newt/wg/peer/add',
|
||||
type: "newt/wg/peer/add",
|
||||
data: peer
|
||||
});
|
||||
|
||||
|
@ -30,19 +40,27 @@ export async function addPeer(siteId: number, peer: {
|
|||
}
|
||||
|
||||
export async function deletePeer(siteId: number, publicKey: string) {
|
||||
const [site] = await db.select().from(sites).where(eq(sites.siteId, siteId)).limit(1);
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
if (!site) {
|
||||
throw new Error(`Site with ID ${siteId} not found`);
|
||||
}
|
||||
|
||||
// get the newt on the site
|
||||
const [newt] = await db.select().from(newts).where(eq(newts.siteId, siteId)).limit(1);
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
if (!newt) {
|
||||
throw new Error(`Newt not found for site ${siteId}`);
|
||||
}
|
||||
|
||||
sendToClient(newt.newtId, {
|
||||
type: 'newt/wg/peer/remove',
|
||||
type: "newt/wg/peer/remove",
|
||||
data: {
|
||||
publicKey
|
||||
}
|
||||
|
@ -51,23 +69,35 @@ export async function deletePeer(siteId: number, publicKey: string) {
|
|||
logger.info(`Deleted peer ${publicKey} from newt ${newt.newtId}`);
|
||||
}
|
||||
|
||||
export async function updatePeer(siteId: number, publicKey: string, peer: {
|
||||
allowedIps?: string[];
|
||||
endpoint?: string;
|
||||
}) {
|
||||
const [site] = await db.select().from(sites).where(eq(sites.siteId, siteId)).limit(1);
|
||||
export async function updatePeer(
|
||||
siteId: number,
|
||||
publicKey: string,
|
||||
peer: {
|
||||
allowedIps?: string[];
|
||||
endpoint?: string;
|
||||
}
|
||||
) {
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
if (!site) {
|
||||
throw new Error(`Site with ID ${siteId} not found`);
|
||||
}
|
||||
|
||||
// get the newt on the site
|
||||
const [newt] = await db.select().from(newts).where(eq(newts.siteId, siteId)).limit(1);
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
if (!newt) {
|
||||
throw new Error(`Newt not found for site ${siteId}`);
|
||||
}
|
||||
|
||||
sendToClient(newt.newtId, {
|
||||
type: 'newt/wg/peer/update',
|
||||
type: "newt/wg/peer/update",
|
||||
data: {
|
||||
publicKey,
|
||||
...peer
|
||||
|
|
|
@ -3,7 +3,7 @@ import db from "@server/db";
|
|||
import { hash } from "@node-rs/argon2";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { newts } from "@server/db/schema";
|
||||
import { newts } from "@server/db/schemas";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/lib/response";
|
||||
import { SqliteError } from "better-sqlite3";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import db from "@server/db";
|
||||
import { olms } from "@server/db/schema";
|
||||
import { olms } from "@server/db/schemas";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import db from "@server/db";
|
||||
import { MessageHandler } from "../ws";
|
||||
import { clients, Olm } from "@server/db/schema";
|
||||
import { clients, Olm } from "@server/db/schemas";
|
||||
import { eq, lt, isNull } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
Olm,
|
||||
olms,
|
||||
sites
|
||||
} from "@server/db/schema";
|
||||
} from "@server/db/schemas";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { addPeer, deletePeer } from "../newt/peers";
|
||||
import logger from "@server/logger";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import db from "@server/db";
|
||||
import { MessageHandler } from "../ws";
|
||||
import { clients, clientSites, Olm } from "@server/db/schema";
|
||||
import { clients, clientSites, Olm } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { updatePeer } from "../newt/peers";
|
||||
import logger from "@server/logger";
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
import db from '@server/db';
|
||||
import { clients, olms, newts } from '@server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { sendToClient } from '../ws';
|
||||
import logger from '@server/logger';
|
||||
import db from "@server/db";
|
||||
import { clients, olms, newts } from "@server/db/schemas";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sendToClient } from "../ws";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function addPeer(clientId: number, peer: {
|
||||
siteId: number,
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
serverIP: string | null;
|
||||
serverPort: number | null;
|
||||
}) {
|
||||
const [olm] = await db.select().from(olms).where(eq(olms.clientId, clientId)).limit(1);
|
||||
export async function addPeer(
|
||||
clientId: number,
|
||||
peer: {
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
serverIP: string | null;
|
||||
serverPort: number | null;
|
||||
}
|
||||
) {
|
||||
const [olm] = await db
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, clientId))
|
||||
.limit(1);
|
||||
if (!olm) {
|
||||
throw new Error(`Olm with ID ${clientId} not found`);
|
||||
}
|
||||
|
||||
sendToClient(olm.olmId, {
|
||||
type: 'olm/wg/peer/add',
|
||||
type: "olm/wg/peer/add",
|
||||
data: {
|
||||
siteId: peer.siteId,
|
||||
publicKey: peer.publicKey,
|
||||
|
@ -31,13 +38,17 @@ export async function addPeer(clientId: number, peer: {
|
|||
}
|
||||
|
||||
export async function deletePeer(clientId: number, publicKey: string) {
|
||||
const [olm] = await db.select().from(olms).where(eq(olms.clientId, clientId)).limit(1);
|
||||
const [olm] = await db
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, clientId))
|
||||
.limit(1);
|
||||
if (!olm) {
|
||||
throw new Error(`Olm with ID ${clientId} not found`);
|
||||
}
|
||||
|
||||
sendToClient(olm.olmId, {
|
||||
type: 'olm/wg/peer/remove',
|
||||
type: "olm/wg/peer/remove",
|
||||
data: {
|
||||
publicKey
|
||||
}
|
||||
|
@ -46,20 +57,27 @@ export async function deletePeer(clientId: number, publicKey: string) {
|
|||
logger.info(`Deleted peer ${publicKey} from olm ${olm.olmId}`);
|
||||
}
|
||||
|
||||
export async function updatePeer(clientId: number, peer: {
|
||||
siteId: number,
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
serverIP: string | null;
|
||||
serverPort: number | null;
|
||||
}) {
|
||||
const [olm] = await db.select().from(olms).where(eq(olms.clientId, clientId)).limit(1);
|
||||
export async function updatePeer(
|
||||
clientId: number,
|
||||
peer: {
|
||||
siteId: number;
|
||||
publicKey: string;
|
||||
endpoint: string;
|
||||
serverIP: string | null;
|
||||
serverPort: number | null;
|
||||
}
|
||||
) {
|
||||
const [olm] = await db
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, clientId))
|
||||
.limit(1);
|
||||
if (!olm) {
|
||||
throw new Error(`Olm with ID ${clientId} not found`);
|
||||
}
|
||||
|
||||
sendToClient(olm.olmId, {
|
||||
type: 'olm/wg/peer/update',
|
||||
type: "olm/wg/peer/update",
|
||||
data: {
|
||||
siteId: peer.siteId,
|
||||
publicKey: peer.publicKey,
|
||||
|
|
|
@ -2,30 +2,8 @@
|
|||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
getPaginationRowModel,
|
||||
SortingState,
|
||||
getSortedRowModel,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
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, Search } from "lucide-react";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
|
@ -34,120 +12,19 @@ interface DataTableProps<TData, TValue> {
|
|||
}
|
||||
|
||||
export function ClientsDataTable<TData, TValue>({
|
||||
addClient,
|
||||
columns,
|
||||
data
|
||||
data,
|
||||
addClient
|
||||
}: DataTableProps<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(),
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 20,
|
||||
pageIndex: 0
|
||||
}
|
||||
},
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between pb-4">
|
||||
<div className="flex items-center max-w-sm mr-2 w-full relative">
|
||||
<Input
|
||||
placeholder="Search clients"
|
||||
value={
|
||||
(table
|
||||
.getColumn("name")
|
||||
?.getFilterValue() as string) ?? ""
|
||||
}
|
||||
onChange={(event) =>
|
||||
table
|
||||
.getColumn("name")
|
||||
?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="w-full pl-8"
|
||||
/>
|
||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2" />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (addClient) {
|
||||
addClient();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Client
|
||||
</Button>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<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 clients. Create one to get started.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<div className="mt-4">
|
||||
<DataTablePagination table={table} />
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="Clients"
|
||||
searchPlaceholder="Search clients..."
|
||||
searchColumn="name"
|
||||
onAdd={addClient}
|
||||
addButtonText="Add Client"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
import { ScrollArea } from "@app/components/ui/scroll-area";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { X } from "lucide-react";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
|
||||
const createClientFormSchema = z.object({
|
||||
name: z
|
||||
|
@ -57,9 +58,16 @@ const createClientFormSchema = z.object({
|
|||
.max(30, {
|
||||
message: "Name must not be longer than 30 characters."
|
||||
}),
|
||||
siteIds: z.array(z.number()).min(1, {
|
||||
message: "Select at least one site."
|
||||
}),
|
||||
siteIds: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string()
|
||||
})
|
||||
)
|
||||
.refine((val) => val.length > 0, {
|
||||
message: "At least one site is required."
|
||||
}),
|
||||
subnet: z.string().min(1, {
|
||||
message: "Subnet is required."
|
||||
})
|
||||
|
@ -89,7 +97,7 @@ export default function CreateClientForm({
|
|||
const api = createApiClient(useEnvContext());
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||
const [sites, setSites] = useState<Tag[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [clientDefaults, setClientDefaults] =
|
||||
|
@ -98,6 +106,9 @@ export default function CreateClientForm({
|
|||
const [selectedSites, setSelectedSites] = useState<
|
||||
Array<{ id: number; name: string }>
|
||||
>([]);
|
||||
const [activeSitesTagIndex, setActiveSitesTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
setIsChecked(checked);
|
||||
|
@ -111,14 +122,6 @@ export default function CreateClientForm({
|
|||
defaultValues
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Update form value when selectedSites changes
|
||||
form.setValue(
|
||||
"siteIds",
|
||||
selectedSites.map((site) => site.id)
|
||||
);
|
||||
}, [selectedSites, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
|
@ -137,7 +140,12 @@ export default function CreateClientForm({
|
|||
const sites = res.data.data.sites.filter(
|
||||
(s) => s.type === "newt" && s.subnet
|
||||
);
|
||||
setSites(sites);
|
||||
setSites(
|
||||
sites.map((site) => ({
|
||||
id: site.siteId.toString(),
|
||||
text: site.name
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const fetchDefaults = async () => {
|
||||
|
@ -167,19 +175,6 @@ export default function CreateClientForm({
|
|||
fetchDefaults();
|
||||
}, [open]);
|
||||
|
||||
const addSite = (siteId: number, siteName: string) => {
|
||||
if (!selectedSites.some((site) => site.id === siteId)) {
|
||||
setSelectedSites([
|
||||
...selectedSites,
|
||||
{ id: siteId, name: siteName }
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSite = (siteId: number) => {
|
||||
setSelectedSites(selectedSites.filter((site) => site.id !== siteId));
|
||||
};
|
||||
|
||||
async function onSubmit(data: CreateClientFormValues) {
|
||||
setLoading?.(true);
|
||||
setIsLoading(true);
|
||||
|
@ -197,7 +192,7 @@ export default function CreateClientForm({
|
|||
|
||||
const payload = {
|
||||
name: data.name,
|
||||
siteIds: data.siteIds,
|
||||
siteIds: data.siteIds.map((site) => parseInt(site.id)),
|
||||
olmId: clientDefaults.olmId,
|
||||
secret: clientDefaults.olmSecret,
|
||||
subnet: data.subnet,
|
||||
|
@ -274,7 +269,8 @@ export default function CreateClientForm({
|
|||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The address that this client will use for connectivity.
|
||||
The address that this client will use for
|
||||
connectivity.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
@ -284,97 +280,28 @@ export default function CreateClientForm({
|
|||
<FormField
|
||||
control={form.control}
|
||||
name="siteIds"
|
||||
render={() => (
|
||||
render={(field) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Sites</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"justify-between",
|
||||
selectedSites.length ===
|
||||
0 &&
|
||||
"text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{selectedSites.length > 0
|
||||
? `${selectedSites.length} site${selectedSites.length !== 1 ? "s" : ""} selected`
|
||||
: "Select sites"}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[300px]">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search sites..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
No sites found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<ScrollArea className="h-[200px]">
|
||||
{sites.map((site) => (
|
||||
<CommandItem
|
||||
value={`${site.siteId}:${site.name}:${site.niceId}`}
|
||||
key={
|
||||
site.siteId
|
||||
}
|
||||
onSelect={() => {
|
||||
addSite(
|
||||
site.siteId,
|
||||
site.name
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedSites.some(
|
||||
(
|
||||
s
|
||||
) =>
|
||||
s.id ===
|
||||
site.siteId
|
||||
)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{site.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{selectedSites.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{selectedSites.map((site) => (
|
||||
<Badge
|
||||
key={site.id}
|
||||
variant="secondary"
|
||||
>
|
||||
{site.name}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
removeSite(site.id)
|
||||
}
|
||||
className="ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={activeSitesTagIndex}
|
||||
setActiveTagIndex={setActiveSitesTagIndex}
|
||||
placeholder="Select sites"
|
||||
size="sm"
|
||||
tags={form.getValues().siteIds}
|
||||
setTags={(newTags) => {
|
||||
form.setValue(
|
||||
"siteIds",
|
||||
newTags as [Tag, ...Tag[]]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={true}
|
||||
autocompleteOptions={sites}
|
||||
allowDuplicates={false}
|
||||
restrictTagsToAutocompleteOptions={true}
|
||||
sortTags={true}
|
||||
/>
|
||||
<FormDescription>
|
||||
The client will have connectivity to the
|
||||
selected sites. The sites must be configured
|
||||
|
|
|
@ -40,11 +40,8 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
|||
|
||||
export default function GeneralPage() {
|
||||
const { client, updateClient } = useClientContext();
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<GeneralFormValues>({
|
||||
|
@ -58,31 +55,31 @@ export default function GeneralPage() {
|
|||
async function onSubmit(data: GeneralFormValues) {
|
||||
setLoading(true);
|
||||
|
||||
await api
|
||||
.post(`/client/${client?.clientId}`, {
|
||||
try {
|
||||
await api.post(`/client/${client?.clientId}`, {
|
||||
name: data.name
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update client",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while updating the client."
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
updateClient({ name: data.name });
|
||||
updateClient({ name: data.name });
|
||||
|
||||
toast({
|
||||
title: "Client updated",
|
||||
description: "The client has been updated."
|
||||
});
|
||||
toast({
|
||||
title: "Client updated",
|
||||
description: "The client has been updated."
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
router.refresh();
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update client",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while updating the client."
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,21 +2,14 @@ import { internal } from "@app/lib/api";
|
|||
import { AxiosResponse } from "axios";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
||||
import Link from "next/link";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from "@app/components/ui/breadcrumb";
|
||||
import { GetClientResponse } from "@server/routers/client";
|
||||
import ClientInfoCard from "./ClientInfoCard";
|
||||
import ClientProvider from "@app/providers/ClientProvider";
|
||||
import { redirect } from "next/navigation";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ clientId: number; orgId: string }>;
|
||||
}
|
||||
|
@ -38,39 +31,27 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||
redirect(`/${params.orgId}/settings/clients`);
|
||||
}
|
||||
|
||||
const sidebarNavItems = [
|
||||
const navItems = [
|
||||
{
|
||||
title: "General",
|
||||
href: "/{orgId}/settings/clients/{clientId}/general"
|
||||
href: `/{orgId}/settings/clients/{clientId}/general`
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex-row">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<Link href="../">Clients</Link>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{client.name}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<SettingsSectionTitle
|
||||
title={`${client?.name} Settings`}
|
||||
description="Configure the settings on your site"
|
||||
/>
|
||||
|
||||
<ClientProvider client={client}>
|
||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||
<div className="space-y-6">
|
||||
<ClientInfoCard />
|
||||
{children}
|
||||
</SidebarSettings>
|
||||
<HorizontalTabs items={navItems}>
|
||||
{children}
|
||||
</HorizontalTabs>
|
||||
</div>
|
||||
</ClientProvider>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,8 @@ import {
|
|||
Link as LinkIcon,
|
||||
Waypoints,
|
||||
Combine,
|
||||
Fingerprint
|
||||
Fingerprint,
|
||||
Workflow
|
||||
} from "lucide-react";
|
||||
|
||||
export const rootNavItems: SidebarNavItem[] = [
|
||||
|
@ -28,6 +29,11 @@ export const orgNavItems: SidebarNavItem[] = [
|
|||
href: "/{orgId}/settings/resources",
|
||||
icon: <Waypoints className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Clients",
|
||||
href: "/{orgId}/settings/clients",
|
||||
icon: <Workflow className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Access Control",
|
||||
href: "/{orgId}/settings/access",
|
||||
|
|
|
@ -29,7 +29,8 @@ export function HorizontalTabs({
|
|||
.replace("{orgId}", params.orgId as string)
|
||||
.replace("{resourceId}", params.resourceId as string)
|
||||
.replace("{niceId}", params.niceId as string)
|
||||
.replace("{userId}", params.userId as string);
|
||||
.replace("{userId}", params.userId as string)
|
||||
.replace("{clientId}", params.clientId as string);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue