add block client

This commit is contained in:
miloschwartz
2026-01-12 20:37:53 -08:00
parent b941b5571f
commit 673cd0fcd1
15 changed files with 438 additions and 15 deletions

View File

@@ -60,7 +60,8 @@ export default async function ClientsPage(props: ClientsPageProps) {
userEmail: client.userEmail,
niceId: client.niceId,
agent: client.agent,
archived: client.archived || false
archived: client.archived || false,
blocked: client.blocked || false
};
};

View File

@@ -56,7 +56,8 @@ export default async function ClientsPage(props: ClientsPageProps) {
userEmail: client.userEmail,
niceId: client.niceId,
agent: client.agent,
archived: client.archived || false
archived: client.archived || false,
blocked: client.blocked || false
};
};

View File

@@ -17,7 +17,8 @@ import {
ArrowRight,
ArrowUpDown,
ArrowUpRight,
MoreHorizontal
MoreHorizontal,
CircleSlash
} from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
@@ -43,6 +44,7 @@ export type ClientRow = {
niceId: string;
agent: string | null;
archived?: boolean;
blocked?: boolean;
};
type ClientTableProps = {
@@ -59,6 +61,7 @@ export default function MachineClientsTable({
const t = useTranslations();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
const [selectedClient, setSelectedClient] = useState<ClientRow | 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
const hasRowsWithoutUserId = useMemo(() => {
return machineClients.some((client) => !client.userId) ?? false;
@@ -174,6 +213,12 @@ export default function MachineClientsTable({
{t("archived")}
</Badge>
)}
{r.blocked && (
<Badge variant="destructive" className="flex items-center gap-1">
<CircleSlash className="h-3 w-3" />
{t("blocked")}
</Badge>
)}
</div>
);
}
@@ -368,6 +413,20 @@ export default function MachineClientsTable({
{clientRow.archived ? "Unarchive" : "Archive"}
</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (clientRow.blocked) {
unblockClient(clientRow.id);
} else {
setSelectedClient(clientRow);
setIsBlockModalOpen(true);
}
}}
>
<span>
{clientRow.blocked ? "Unblock" : "Block"}
</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelectedClient(clientRow);
@@ -418,6 +477,27 @@ export default function MachineClientsTable({
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
columns={columns}
@@ -446,20 +526,31 @@ export default function MachineClientsTable({
{
id: "active",
label: t("active") || "Active",
value: false
value: "active"
},
{
id: "archived",
label: t("archived") || "Archived",
value: true
value: "archived"
},
{
id: "blocked",
label: t("blocked") || "Blocked",
value: "blocked"
}
],
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
if (selectedValues.length === 0) return true;
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
}
]}
/>

View File

@@ -105,6 +105,8 @@ function getActionsCategories(root: boolean) {
[t("actionDeleteClient")]: "deleteClient",
[t("actionArchiveClient")]: "archiveClient",
[t("actionUnarchiveClient")]: "unarchiveClient",
[t("actionBlockClient")]: "blockClient",
[t("actionUnblockClient")]: "unblockClient",
[t("actionUpdateClient")]: "updateClient",
[t("actionListClients")]: "listClients",
[t("actionGetClient")]: "getClient"

View File

@@ -17,7 +17,8 @@ import {
ArrowRight,
ArrowUpDown,
ArrowUpRight,
MoreHorizontal
MoreHorizontal,
CircleSlash
} from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
@@ -44,6 +45,7 @@ export type ClientRow = {
niceId: string;
agent: string | null;
archived?: boolean;
blocked?: boolean;
};
type ClientTableProps = {
@@ -56,6 +58,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
const t = useTranslations();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
const [selectedClient, setSelectedClient] = useState<ClientRow | 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
const hasRowsWithoutUserId = useMemo(() => {
return userClients.some((client) => !client.userId);
@@ -170,6 +209,12 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
{t("archived")}
</Badge>
)}
{r.blocked && (
<Badge variant="destructive" className="flex items-center gap-1">
<CircleSlash className="h-3 w-3" />
{t("blocked")}
</Badge>
)}
</div>
);
}
@@ -417,6 +462,18 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
>
<span>{clientRow.archived ? "Unarchive" : "Archive"}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (clientRow.blocked) {
unblockClient(clientRow.id);
} else {
setSelectedClient(clientRow);
setIsBlockModalOpen(true);
}
}}
>
<span>{clientRow.blocked ? "Unblock" : "Block"}</span>
</DropdownMenuItem>
{!clientRow.userId && (
// Machine client - also show delete option
<DropdownMenuItem
@@ -467,6 +524,27 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
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 />
@@ -493,20 +571,31 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
{
id: "active",
label: t("active") || "Active",
value: false
value: "active"
},
{
id: "archived",
label: t("archived") || "Archived",
value: true
value: "archived"
},
{
id: "blocked",
label: t("blocked") || "Blocked",
value: "blocked"
}
],
filterFn: (row: ClientRow, selectedValues: (string | number | boolean)[]) => {
if (selectedValues.length === 0) return true;
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
}
]}
/>