mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
add block client
This commit is contained in:
@@ -1120,6 +1120,8 @@
|
|||||||
"actionDeleteClient": "Delete Client",
|
"actionDeleteClient": "Delete Client",
|
||||||
"actionArchiveClient": "Archive Client",
|
"actionArchiveClient": "Archive Client",
|
||||||
"actionUnarchiveClient": "Unarchive Client",
|
"actionUnarchiveClient": "Unarchive Client",
|
||||||
|
"actionBlockClient": "Block Client",
|
||||||
|
"actionUnblockClient": "Unblock Client",
|
||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "Update Client",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "List Clients",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "Get Client",
|
||||||
@@ -2418,5 +2420,9 @@
|
|||||||
"archiveClientQuestion": "Are you sure you want to archive this client?",
|
"archiveClientQuestion": "Are you sure you want to archive this client?",
|
||||||
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
|
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
|
||||||
"archiveClientConfirm": "Archive Client",
|
"archiveClientConfirm": "Archive Client",
|
||||||
|
"blockClient": "Block Client",
|
||||||
|
"blockClientQuestion": "Are you sure you want to block this client?",
|
||||||
|
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
|
||||||
|
"blockClientConfirm": "Block Client",
|
||||||
"active": "Active"
|
"active": "Active"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export enum ActionsEnum {
|
|||||||
deleteClient = "deleteClient",
|
deleteClient = "deleteClient",
|
||||||
archiveClient = "archiveClient",
|
archiveClient = "archiveClient",
|
||||||
unarchiveClient = "unarchiveClient",
|
unarchiveClient = "unarchiveClient",
|
||||||
|
blockClient = "blockClient",
|
||||||
|
unblockClient = "unblockClient",
|
||||||
updateClient = "updateClient",
|
updateClient = "updateClient",
|
||||||
listClients = "listClients",
|
listClients = "listClients",
|
||||||
getClient = "getClient",
|
getClient = "getClient",
|
||||||
|
|||||||
@@ -689,7 +689,8 @@ export const clients = pgTable("clients", {
|
|||||||
// endpoint: varchar("endpoint"),
|
// endpoint: varchar("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
maxConnections: integer("maxConnections"),
|
maxConnections: integer("maxConnections"),
|
||||||
archived: boolean("archived").notNull().default(false)
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
blocked: boolean("blocked").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = pgTable(
|
export const clientSitesAssociationsCache = pgTable(
|
||||||
|
|||||||
@@ -384,7 +384,8 @@ export const clients = sqliteTable("clients", {
|
|||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
// endpoint: text("endpoint"),
|
// endpoint: text("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
|
||||||
|
blocked: integer("blocked", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = sqliteTable(
|
export const clientSitesAssociationsCache = sqliteTable(
|
||||||
|
|||||||
101
server/routers/client/blockClient.ts
Normal file
101
server/routers/client/blockClient.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { clients } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { sendTerminateClient } from "./terminate";
|
||||||
|
|
||||||
|
const blockClientSchema = z.strictObject({
|
||||||
|
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/client/{clientId}/block",
|
||||||
|
description: "Block a client by its client ID.",
|
||||||
|
tags: [OpenAPITags.Client],
|
||||||
|
request: {
|
||||||
|
params: blockClientSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function blockClient(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = blockClientSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clientId } = parsedParams.data;
|
||||||
|
|
||||||
|
// Check if client exists
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Client with ID ${clientId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.blocked) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Client with ID ${clientId} is already blocked`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// Block the client
|
||||||
|
await trx
|
||||||
|
.update(clients)
|
||||||
|
.set({ blocked: true })
|
||||||
|
.where(eq(clients.clientId, clientId));
|
||||||
|
|
||||||
|
// Send terminate signal if there's an associated OLM and it's connected
|
||||||
|
if (client.olmId && client.online) {
|
||||||
|
await sendTerminateClient(client.clientId, client.olmId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Client blocked successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to block client"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ export * from "./createClient";
|
|||||||
export * from "./deleteClient";
|
export * from "./deleteClient";
|
||||||
export * from "./archiveClient";
|
export * from "./archiveClient";
|
||||||
export * from "./unarchiveClient";
|
export * from "./unarchiveClient";
|
||||||
|
export * from "./blockClient";
|
||||||
|
export * from "./unblockClient";
|
||||||
export * from "./listClients";
|
export * from "./listClients";
|
||||||
export * from "./updateClient";
|
export * from "./updateClient";
|
||||||
export * from "./getClient";
|
export * from "./getClient";
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ function queryClients(
|
|||||||
niceId: clients.niceId,
|
niceId: clients.niceId,
|
||||||
agent: olms.agent,
|
agent: olms.agent,
|
||||||
olmArchived: olms.archived,
|
olmArchived: olms.archived,
|
||||||
archived: clients.archived
|
archived: clients.archived,
|
||||||
|
blocked: clients.blocked
|
||||||
})
|
})
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||||
|
|||||||
93
server/routers/client/unblockClient.ts
Normal file
93
server/routers/client/unblockClient.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { clients } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const unblockClientSchema = z.strictObject({
|
||||||
|
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/client/{clientId}/unblock",
|
||||||
|
description: "Unblock a client by its client ID.",
|
||||||
|
tags: [OpenAPITags.Client],
|
||||||
|
request: {
|
||||||
|
params: unblockClientSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function unblockClient(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = unblockClientSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clientId } = parsedParams.data;
|
||||||
|
|
||||||
|
// Check if client exists
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Client with ID ${clientId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.blocked) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Client with ID ${clientId} is not blocked`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unblock the client
|
||||||
|
await db
|
||||||
|
.update(clients)
|
||||||
|
.set({ blocked: false })
|
||||||
|
.where(eq(clients.clientId, clientId));
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Client unblocked successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to unblock client"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -190,6 +190,22 @@ authenticated.post(
|
|||||||
client.unarchiveClient
|
client.unarchiveClient
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/client/:clientId/block",
|
||||||
|
verifyClientAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.blockClient),
|
||||||
|
logActionAudit(ActionsEnum.blockClient),
|
||||||
|
client.blockClient
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/client/:clientId/unblock",
|
||||||
|
verifyClientAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.unblockClient),
|
||||||
|
logActionAudit(ActionsEnum.unblockClient),
|
||||||
|
client.unblockClient
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/client/:clientId",
|
"/client/:clientId",
|
||||||
verifyClientAccess, // this will check if the user has access to the client
|
verifyClientAccess, // this will check if the user has access to the client
|
||||||
|
|||||||
@@ -859,6 +859,22 @@ authenticated.post(
|
|||||||
client.unarchiveClient
|
client.unarchiveClient
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/client/:clientId/block",
|
||||||
|
verifyApiKeyClientAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.blockClient),
|
||||||
|
logActionAudit(ActionsEnum.blockClient),
|
||||||
|
client.blockClient
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/client/:clientId/unblock",
|
||||||
|
verifyApiKeyClientAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.unblockClient),
|
||||||
|
logActionAudit(ActionsEnum.unblockClient),
|
||||||
|
client.unblockClient
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/client/:clientId",
|
"/client/:clientId",
|
||||||
verifyApiKeyClientAccess,
|
verifyApiKeyClientAccess,
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
|||||||
userEmail: client.userEmail,
|
userEmail: client.userEmail,
|
||||||
niceId: client.niceId,
|
niceId: client.niceId,
|
||||||
agent: client.agent,
|
agent: client.agent,
|
||||||
archived: client.archived || false
|
archived: client.archived || false,
|
||||||
|
blocked: client.blocked || false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
|||||||
userEmail: client.userEmail,
|
userEmail: client.userEmail,
|
||||||
niceId: client.niceId,
|
niceId: client.niceId,
|
||||||
agent: client.agent,
|
agent: client.agent,
|
||||||
archived: client.archived || false
|
archived: client.archived || false,
|
||||||
|
blocked: client.blocked || false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
MoreHorizontal
|
MoreHorizontal,
|
||||||
|
CircleSlash
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -43,6 +44,7 @@ export type ClientRow = {
|
|||||||
niceId: string;
|
niceId: string;
|
||||||
agent: string | null;
|
agent: string | null;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
blocked?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClientTableProps = {
|
type ClientTableProps = {
|
||||||
@@ -59,6 +61,7 @@ export default function MachineClientsTable({
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
|
||||||
const [selectedClient, setSelectedClient] = useState<ClientRow | null>(
|
const [selectedClient, setSelectedClient] = useState<ClientRow | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -138,6 +141,42 @@ export default function MachineClientsTable({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const blockClient = (clientId: number) => {
|
||||||
|
api.post(`/client/${clientId}/block`)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Error blocking client", e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error blocking client",
|
||||||
|
description: formatAxiosError(e, "Error blocking client")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
router.refresh();
|
||||||
|
setIsBlockModalOpen(false);
|
||||||
|
setSelectedClient(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const unblockClient = (clientId: number) => {
|
||||||
|
api.post(`/client/${clientId}/unblock`)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Error unblocking client", e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error unblocking client",
|
||||||
|
description: formatAxiosError(e, "Error unblocking client")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Check if there are any rows without userIds in the current view's data
|
// Check if there are any rows without userIds in the current view's data
|
||||||
const hasRowsWithoutUserId = useMemo(() => {
|
const hasRowsWithoutUserId = useMemo(() => {
|
||||||
return machineClients.some((client) => !client.userId) ?? false;
|
return machineClients.some((client) => !client.userId) ?? false;
|
||||||
@@ -174,6 +213,12 @@ export default function MachineClientsTable({
|
|||||||
{t("archived")}
|
{t("archived")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{r.blocked && (
|
||||||
|
<Badge variant="destructive" className="flex items-center gap-1">
|
||||||
|
<CircleSlash className="h-3 w-3" />
|
||||||
|
{t("blocked")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -368,6 +413,20 @@ export default function MachineClientsTable({
|
|||||||
{clientRow.archived ? "Unarchive" : "Archive"}
|
{clientRow.archived ? "Unarchive" : "Archive"}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (clientRow.blocked) {
|
||||||
|
unblockClient(clientRow.id);
|
||||||
|
} else {
|
||||||
|
setSelectedClient(clientRow);
|
||||||
|
setIsBlockModalOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{clientRow.blocked ? "Unblock" : "Block"}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedClient(clientRow);
|
setSelectedClient(clientRow);
|
||||||
@@ -418,6 +477,27 @@ export default function MachineClientsTable({
|
|||||||
title="Delete Client"
|
title="Delete Client"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{selectedClient && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={isBlockModalOpen}
|
||||||
|
setOpen={(val) => {
|
||||||
|
setIsBlockModalOpen(val);
|
||||||
|
if (!val) {
|
||||||
|
setSelectedClient(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("blockClientQuestion")}</p>
|
||||||
|
<p>{t("blockClientMessage")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("blockClientConfirm")}
|
||||||
|
onConfirm={async () => blockClient(selectedClient!.id)}
|
||||||
|
string={selectedClient.name}
|
||||||
|
title={t("blockClient")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -446,20 +526,31 @@ export default function MachineClientsTable({
|
|||||||
{
|
{
|
||||||
id: "active",
|
id: "active",
|
||||||
label: t("active") || "Active",
|
label: t("active") || "Active",
|
||||||
value: false
|
value: "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "archived",
|
id: "archived",
|
||||||
label: t("archived") || "Archived",
|
label: t("archived") || "Archived",
|
||||||
value: true
|
value: "archived"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "blocked",
|
||||||
|
label: t("blocked") || "Blocked",
|
||||||
|
value: "blocked"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
|
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
|
||||||
if (selectedValues.length === 0) return true;
|
if (selectedValues.length === 0) return true;
|
||||||
const rowArchived = row.archived || false;
|
const rowArchived = row.archived || false;
|
||||||
return selectedValues.includes(rowArchived);
|
const rowBlocked = row.blocked || false;
|
||||||
|
const isActive = !rowArchived && !rowBlocked;
|
||||||
|
|
||||||
|
if (selectedValues.includes("active") && isActive) return true;
|
||||||
|
if (selectedValues.includes("archived") && rowArchived) return true;
|
||||||
|
if (selectedValues.includes("blocked") && rowBlocked) return true;
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
defaultValues: [false] // Default to showing active clients
|
defaultValues: ["active"] // Default to showing active clients
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionDeleteClient")]: "deleteClient",
|
[t("actionDeleteClient")]: "deleteClient",
|
||||||
[t("actionArchiveClient")]: "archiveClient",
|
[t("actionArchiveClient")]: "archiveClient",
|
||||||
[t("actionUnarchiveClient")]: "unarchiveClient",
|
[t("actionUnarchiveClient")]: "unarchiveClient",
|
||||||
|
[t("actionBlockClient")]: "blockClient",
|
||||||
|
[t("actionUnblockClient")]: "unblockClient",
|
||||||
[t("actionUpdateClient")]: "updateClient",
|
[t("actionUpdateClient")]: "updateClient",
|
||||||
[t("actionListClients")]: "listClients",
|
[t("actionListClients")]: "listClients",
|
||||||
[t("actionGetClient")]: "getClient"
|
[t("actionGetClient")]: "getClient"
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
MoreHorizontal
|
MoreHorizontal,
|
||||||
|
CircleSlash
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -44,6 +45,7 @@ export type ClientRow = {
|
|||||||
niceId: string;
|
niceId: string;
|
||||||
agent: string | null;
|
agent: string | null;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
blocked?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClientTableProps = {
|
type ClientTableProps = {
|
||||||
@@ -56,6 +58,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
|
||||||
const [selectedClient, setSelectedClient] = useState<ClientRow | null>(
|
const [selectedClient, setSelectedClient] = useState<ClientRow | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -134,6 +137,42 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const blockClient = (clientId: number) => {
|
||||||
|
api.post(`/client/${clientId}/block`)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Error blocking client", e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error blocking client",
|
||||||
|
description: formatAxiosError(e, "Error blocking client")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
router.refresh();
|
||||||
|
setIsBlockModalOpen(false);
|
||||||
|
setSelectedClient(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const unblockClient = (clientId: number) => {
|
||||||
|
api.post(`/client/${clientId}/unblock`)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Error unblocking client", e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error unblocking client",
|
||||||
|
description: formatAxiosError(e, "Error unblocking client")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
startTransition(() => {
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Check if there are any rows without userIds in the current view's data
|
// Check if there are any rows without userIds in the current view's data
|
||||||
const hasRowsWithoutUserId = useMemo(() => {
|
const hasRowsWithoutUserId = useMemo(() => {
|
||||||
return userClients.some((client) => !client.userId);
|
return userClients.some((client) => !client.userId);
|
||||||
@@ -170,6 +209,12 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
{t("archived")}
|
{t("archived")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{r.blocked && (
|
||||||
|
<Badge variant="destructive" className="flex items-center gap-1">
|
||||||
|
<CircleSlash className="h-3 w-3" />
|
||||||
|
{t("blocked")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -417,6 +462,18 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
>
|
>
|
||||||
<span>{clientRow.archived ? "Unarchive" : "Archive"}</span>
|
<span>{clientRow.archived ? "Unarchive" : "Archive"}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (clientRow.blocked) {
|
||||||
|
unblockClient(clientRow.id);
|
||||||
|
} else {
|
||||||
|
setSelectedClient(clientRow);
|
||||||
|
setIsBlockModalOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{clientRow.blocked ? "Unblock" : "Block"}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
{!clientRow.userId && (
|
{!clientRow.userId && (
|
||||||
// Machine client - also show delete option
|
// Machine client - also show delete option
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -467,6 +524,27 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
title="Delete Client"
|
title="Delete Client"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{selectedClient && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={isBlockModalOpen}
|
||||||
|
setOpen={(val) => {
|
||||||
|
setIsBlockModalOpen(val);
|
||||||
|
if (!val) {
|
||||||
|
setSelectedClient(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("blockClientQuestion")}</p>
|
||||||
|
<p>{t("blockClientMessage")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("blockClientConfirm")}
|
||||||
|
onConfirm={async () => blockClient(selectedClient!.id)}
|
||||||
|
string={selectedClient.name}
|
||||||
|
title={t("blockClient")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ClientDownloadBanner />
|
<ClientDownloadBanner />
|
||||||
|
|
||||||
@@ -493,20 +571,31 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
{
|
{
|
||||||
id: "active",
|
id: "active",
|
||||||
label: t("active") || "Active",
|
label: t("active") || "Active",
|
||||||
value: false
|
value: "active"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "archived",
|
id: "archived",
|
||||||
label: t("archived") || "Archived",
|
label: t("archived") || "Archived",
|
||||||
value: true
|
value: "archived"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "blocked",
|
||||||
|
label: t("blocked") || "Blocked",
|
||||||
|
value: "blocked"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
|
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
|
||||||
if (selectedValues.length === 0) return true;
|
if (selectedValues.length === 0) return true;
|
||||||
const rowArchived = row.archived || false;
|
const rowArchived = row.archived || false;
|
||||||
return selectedValues.includes(rowArchived);
|
const rowBlocked = row.blocked || false;
|
||||||
|
const isActive = !rowArchived && !rowBlocked;
|
||||||
|
|
||||||
|
if (selectedValues.includes("active") && isActive) return true;
|
||||||
|
if (selectedValues.includes("archived") && rowArchived) return true;
|
||||||
|
if (selectedValues.includes("blocked") && rowBlocked) return true;
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
defaultValues: [false] // Default to showing active clients
|
defaultValues: ["active"] // Default to showing active clients
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user