From 4adec1700e869cdd891f7219735a4cabfd47d047 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 22 Sep 2026 13:35:02 +0100 Subject: [PATCH] Extract shared ClientsTable component and useClientRulesMap hook --- src/components/general/clients-table.tsx | 57 +++++++++++++ src/hooks/use-client-rules-map.ts | 38 +++++++++ .../admin/(sections)/peers-table-section.tsx | 82 +++---------------- .../(sections)/clients-table-section.tsx | 76 ++--------------- .../clients/(partials)/clients-list.tsx | 77 +++-------------- 5 files changed, 126 insertions(+), 204 deletions(-) create mode 100644 src/components/general/clients-table.tsx create mode 100644 src/hooks/use-client-rules-map.ts diff --git a/src/components/general/clients-table.tsx b/src/components/general/clients-table.tsx new file mode 100644 index 0000000..90c11f7 --- /dev/null +++ b/src/components/general/clients-table.tsx @@ -0,0 +1,57 @@ +import ClientRow from "@/src/components/general/client-row"; +import type { + BUN_SQLITE_WGUI_CLIENTS, + BUN_SQLITE_WGUI_CLIENT_RULES, +} from "@/db/types/db"; + +type Props = { + clients: BUN_SQLITE_WGUI_CLIENTS[]; + rules_by_client_id: Map; + accessLabel?: string; + minWidthClass?: string; +}; + +const thClass = + "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " + + "text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap"; + +const thRightClass = `${thClass} text-right`; + +export default function ClientsTable({ + clients, + rules_by_client_id, + accessLabel = "Access", + minWidthClass = "min-w-[860px]", +}: Props) { + return ( +
+ + + + + + + + + + + + + + + {clients.map((client) => ( + + ))} + +
ClientHostTunnel IPAllowed IPs{accessLabel}Public keyCreatedActions
+
+ ); +} diff --git a/src/hooks/use-client-rules-map.ts b/src/hooks/use-client-rules-map.ts new file mode 100644 index 0000000..0f8bf65 --- /dev/null +++ b/src/hooks/use-client-rules-map.ts @@ -0,0 +1,38 @@ +import { useMemo } from "react"; +import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get"; +import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db"; + +type ClientRulesMap = Map; + +export default function useClientRulesMap( + initial_res?: BUN_SQLITE_WGUI_CLIENT_RULES[] | null, +): ClientRulesMap { + const { res: client_rules } = useAdminCrudGet( + { + table: "client_rules", + sql_query: { + limit: 500, + }, + initial_res: initial_res ?? undefined, + }, + ); + + return useMemo(() => { + const map: ClientRulesMap = new Map(); + + for (let i = 0; i < (client_rules || []).length; i++) { + const rule = client_rules?.[i]; + const client_id = Number(rule?.client_id); + + if (!rule || !client_id) { + continue; + } + + const existing = map.get(client_id) || []; + existing.push(rule); + map.set(client_id, existing); + } + + return map; + }, [client_rules]); +} diff --git a/src/pages/admin/(sections)/peers-table-section.tsx b/src/pages/admin/(sections)/peers-table-section.tsx index 501b30f..59e56cb 100644 --- a/src/pages/admin/(sections)/peers-table-section.tsx +++ b/src/pages/admin/(sections)/peers-table-section.tsx @@ -5,56 +5,20 @@ import Row from "@/src/components/twui/layout/Row"; import Stack from "@/src/components/twui/layout/Stack"; import Span from "@/src/components/twui/layout/Span"; import AdminCard from "@/src/components/general/admin-card"; -import ClientRow from "@/src/components/general/client-row"; -import type { - BUN_SQLITE_WGUI_CLIENT_RULES, - BUN_SQLITE_WGUI_CLIENTS, -} from "@/db/types/db"; -import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get"; -import { useMemo } from "react"; +import ClientsTable from "@/src/components/general/clients-table"; +import useClientRulesMap from "@/src/hooks/use-client-rules-map"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; type Props = { clients?: BUN_SQLITE_WGUI_CLIENTS[]; }; -const thClass = - "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " + - "text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap"; - -const thRightClass = `${thClass} text-right`; - const RECENT_LIMIT = 8; export default function PeersTableSection({ clients }: Props) { const recent = (clients || []).slice(0, RECENT_LIMIT); - const { res: client_rules } = useAdminCrudGet( - { - table: "client_rules", - sql_query: { - limit: 500, - }, - }, - ); - - const rules_by_client_id = useMemo(() => { - const map = new Map(); - - for (let i = 0; i < (client_rules || []).length; i++) { - const rule = client_rules?.[i]; - const client_id = Number(rule?.client_id); - - if (!rule || !client_id) { - continue; - } - - const existing = map.get(client_id) || []; - existing.push(rule); - map.set(client_id, existing); - } - - return map; - }, [client_rules]); + const rules_by_client_id = useClientRulesMap(); return ( @@ -71,37 +35,11 @@ export default function PeersTableSection({ clients }: Props) { {recent.length ? ( -
- - - - - - - - - - - - - - - {recent.map((client) => ( - - ))} - -
ClientHostTunnel IPAllowed IPsAccess ListPublic keyCreatedActions
-
+ ) : ( ); -} +} \ No newline at end of file diff --git a/src/pages/admin/clients/(sections)/clients-table-section.tsx b/src/pages/admin/clients/(sections)/clients-table-section.tsx index 15d22cf..52bcf02 100644 --- a/src/pages/admin/clients/(sections)/clients-table-section.tsx +++ b/src/pages/admin/clients/(sections)/clients-table-section.tsx @@ -2,29 +2,21 @@ import { useMemo, useState, type Dispatch, type SetStateAction } from "react"; import Search from "@/src/components/twui/elements/Search"; import Loading from "@/src/components/twui/elements/Loading"; import AdminCard from "@/src/components/general/admin-card"; -import ClientRow from "@/src/components/general/client-row"; +import ClientsTable from "@/src/components/general/clients-table"; import H2 from "@/src/components/twui/layout/H2"; import Span from "@/src/components/twui/layout/Span"; import Row from "@/src/components/twui/layout/Row"; import Stack from "@/src/components/twui/layout/Stack"; import ClientFormModal from "../(partials)/client-form-modal"; +import useClientRulesMap from "@/src/hooks/use-client-rules-map"; import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get"; -import type { - BUN_SQLITE_WGUI_CLIENTS, - BUN_SQLITE_WGUI_CLIENT_RULES, -} from "@/db/types/db"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; type Props = { addOpen: boolean; setAddOpen: Dispatch>; }; -const thClass = - "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " + - "text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap"; - -const thRightClass = `${thClass} text-right`; - export default function ClientsTableSection({ addOpen, setAddOpen }: Props) { const [query, setQuery] = useState(""); @@ -36,31 +28,7 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) { }, }); - const { res: client_rules } = useAdminCrudGet({ - table: "client_rules", - sql_query: { - limit: 500, - }, - }); - - const rules_by_client_id = useMemo(() => { - const map = new Map(); - - for (let i = 0; i < (client_rules || []).length; i++) { - const rule = client_rules?.[i]; - const client_id = Number(rule?.client_id); - - if (!rule || !client_id) { - continue; - } - - const existing = map.get(client_id) || []; - existing.push(rule); - map.set(client_id, existing); - } - - return map; - }, [client_rules]); + const rules_by_client_id = useClientRulesMap(); const filtered = useMemo(() => { const clients = res || []; @@ -102,37 +70,11 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) { ) : filtered.length ? ( -
- - - - - - - - - - - - - - - {filtered.map((client) => ( - - ))} - -
ClientHostTunnel IPAllowed IPsAccessPublic keyCreatedActions
-
+ ) : ( diff --git a/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx b/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx index 75de836..07767f6 100644 --- a/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx +++ b/src/pages/admin/hosts/[host_id]/clients/(partials)/clients-list.tsx @@ -2,27 +2,21 @@ import Loading from "@/src/components/twui/elements/Loading"; import Paper from "@/src/components/twui/elements/Paper"; import Center from "@/src/components/twui/layout/Center"; import Span from "@/src/components/twui/layout/Span"; -import Stack from "@/src/components/twui/layout/Stack"; -import ClientRow from "@/src/components/general/client-row"; +import ClientsTable from "@/src/components/general/clients-table"; +import useClientRulesMap from "@/src/hooks/use-client-rules-map"; import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get"; import { AppContext } from "@/src/pages/__root"; -import { useContext, useMemo } from "react"; -import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db"; +import { useContext } from "react"; +import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db"; type Props = { host_id: string | number; }; -const thClass = - "px-4 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.08em] " + - "text-foreground-light/40 dark:text-foreground-dark/40 whitespace-nowrap"; - -const thRightClass = `${thClass} text-right`; - export default function ClientsList({ host_id }: Props) { const { pageProps } = useContext(AppContext); - const { res: clients } = useAdminCrudGet({ + const { res: clients } = useAdminCrudGet({ table: "clients", sql_query: { query: { @@ -33,32 +27,7 @@ export default function ClientsList({ host_id }: Props) { initial_res: pageProps?.clients || undefined, }); - const { res: client_rules } = useAdminCrudGet({ - table: "client_rules", - sql_query: { - limit: 500, - }, - initial_res: pageProps?.client_rules || undefined, - }); - - const rules_by_client_id = useMemo(() => { - const map = new Map(); - - for (let i = 0; i < (client_rules || []).length; i++) { - const rule = client_rules?.[i]; - const client_id = Number(rule?.client_id); - - if (!rule || !client_id) { - continue; - } - - const existing = map.get(client_id) || []; - existing.push(rule); - map.set(client_id, existing); - } - - return map; - }, [client_rules]); + const rules_by_client_id = useClientRulesMap(pageProps?.client_rules); if (!clients) { return ( @@ -81,33 +50,11 @@ export default function ClientsList({ host_id }: Props) { } return ( - - - - - - - - - - - - - - - {clients.map((client) => ( - - ))} - -
ClientTunnel IPAllowed IPsAccessPublic keyCreatedActions
+ + ); -} \ No newline at end of file +}