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