Extract shared ClientsTable component and useClientRulesMap hook

This commit is contained in:
2026-09-22 13:35:02 +01:00
parent c4b057ba7a
commit 4adec1700e
5 changed files with 126 additions and 204 deletions
+57
View File
@@ -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<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>;
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 (
<div className="overflow-x-auto w-full">
<table className={`w-full ${minWidthClass}`}>
<thead className="w-full">
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03] w-full">
<th className={thClass}>Client</th>
<th className={thClass}>Host</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>{accessLabel}</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
</tr>
</thead>
<tbody>
{clients.map((client) => (
<ClientRow
key={client.id}
client={client}
rules={
client.id
? rules_by_client_id.get(Number(client.id))
: undefined
}
/>
))}
</tbody>
</table>
</div>
);
}
+38
View File
@@ -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<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>;
export default function useClientRulesMap(
initial_res?: BUN_SQLITE_WGUI_CLIENT_RULES[] | null,
): ClientRulesMap {
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>(
{
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]);
}
@@ -5,56 +5,20 @@ import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack"; import Stack from "@/src/components/twui/layout/Stack";
import Span from "@/src/components/twui/layout/Span"; import Span from "@/src/components/twui/layout/Span";
import AdminCard from "@/src/components/general/admin-card"; 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 type { import useClientRulesMap from "@/src/hooks/use-client-rules-map";
BUN_SQLITE_WGUI_CLIENT_RULES, import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
BUN_SQLITE_WGUI_CLIENTS,
} from "@/db/types/db";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import { useMemo } from "react";
type Props = { type Props = {
clients?: BUN_SQLITE_WGUI_CLIENTS[]; 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; const RECENT_LIMIT = 8;
export default function PeersTableSection({ clients }: Props) { export default function PeersTableSection({ clients }: Props) {
const recent = (clients || []).slice(0, RECENT_LIMIT); const recent = (clients || []).slice(0, RECENT_LIMIT);
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>( const rules_by_client_id = useClientRulesMap();
{
table: "client_rules",
sql_query: {
limit: 500,
},
},
);
const rules_by_client_id = useMemo(() => {
const map = new Map<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>();
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]);
return ( return (
<AdminCard className="w-full overflow-hidden"> <AdminCard className="w-full overflow-hidden">
@@ -71,37 +35,11 @@ export default function PeersTableSection({ clients }: Props) {
</Link> </Link>
</Row> </Row>
{recent.length ? ( {recent.length ? (
<div className="overflow-x-auto"> <ClientsTable
<table className="w-full min-w-[860px]"> clients={recent}
<thead className="w-full"> rules_by_client_id={rules_by_client_id}
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03] w-full"> accessLabel="Access List"
<th className={thClass}>Client</th>
<th className={thClass}>Host</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>Access List</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
</tr>
</thead>
<tbody>
{recent.map((client) => (
<ClientRow
key={client.id}
client={client}
rules={
client.id
? rules_by_client_id.get(
Number(client.id),
)
: undefined
}
/> />
))}
</tbody>
</table>
</div>
) : ( ) : (
<Stack <Stack
center center
@@ -2,29 +2,21 @@ import { useMemo, useState, type Dispatch, type SetStateAction } from "react";
import Search from "@/src/components/twui/elements/Search"; import Search from "@/src/components/twui/elements/Search";
import Loading from "@/src/components/twui/elements/Loading"; import Loading from "@/src/components/twui/elements/Loading";
import AdminCard from "@/src/components/general/admin-card"; 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 H2 from "@/src/components/twui/layout/H2";
import Span from "@/src/components/twui/layout/Span"; import Span from "@/src/components/twui/layout/Span";
import Row from "@/src/components/twui/layout/Row"; import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack"; import Stack from "@/src/components/twui/layout/Stack";
import ClientFormModal from "../(partials)/client-form-modal"; 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 { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import type { import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_CLIENT_RULES,
} from "@/db/types/db";
type Props = { type Props = {
addOpen: boolean; addOpen: boolean;
setAddOpen: Dispatch<SetStateAction<boolean>>; setAddOpen: Dispatch<SetStateAction<boolean>>;
}; };
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) { export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@@ -36,31 +28,7 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
}, },
}); });
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>({ const rules_by_client_id = useClientRulesMap();
table: "client_rules",
sql_query: {
limit: 500,
},
});
const rules_by_client_id = useMemo(() => {
const map = new Map<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>();
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 filtered = useMemo(() => { const filtered = useMemo(() => {
const clients = res || []; const clients = res || [];
@@ -102,37 +70,11 @@ export default function ClientsTableSection({ addOpen, setAddOpen }: Props) {
<Loading /> <Loading />
</Stack> </Stack>
) : filtered.length ? ( ) : filtered.length ? (
<div className="overflow-x-auto"> <ClientsTable
<table className="w-full min-w-[940px]"> clients={filtered}
<thead> rules_by_client_id={rules_by_client_id}
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]"> minWidthClass="min-w-[940px]"
<th className={thClass}>Client</th>
<th className={thClass}>Host</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>Access</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
</tr>
</thead>
<tbody>
{filtered.map((client) => (
<ClientRow
key={client.id}
client={client}
rules={
client.id
? rules_by_client_id.get(
Number(client.id),
)
: undefined
}
/> />
))}
</tbody>
</table>
</div>
) : ( ) : (
<Stack center className="w-full justify-center py-14 px-6 text-center gap-1"> <Stack center className="w-full justify-center py-14 px-6 text-center gap-1">
<Span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50"> <Span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50">
@@ -2,27 +2,21 @@ import Loading from "@/src/components/twui/elements/Loading";
import Paper from "@/src/components/twui/elements/Paper"; import Paper from "@/src/components/twui/elements/Paper";
import Center from "@/src/components/twui/layout/Center"; import Center from "@/src/components/twui/layout/Center";
import Span from "@/src/components/twui/layout/Span"; import Span from "@/src/components/twui/layout/Span";
import Stack from "@/src/components/twui/layout/Stack"; import ClientsTable from "@/src/components/general/clients-table";
import ClientRow from "@/src/components/general/client-row"; import useClientRulesMap from "@/src/hooks/use-client-rules-map";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get"; import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import { AppContext } from "@/src/pages/__root"; import { AppContext } from "@/src/pages/__root";
import { useContext, useMemo } from "react"; import { useContext } from "react";
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db"; import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
type Props = { type Props = {
host_id: string | number; 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) { export default function ClientsList({ host_id }: Props) {
const { pageProps } = useContext(AppContext); const { pageProps } = useContext(AppContext);
const { res: clients } = useAdminCrudGet({ const { res: clients } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENTS>({
table: "clients", table: "clients",
sql_query: { sql_query: {
query: { query: {
@@ -33,32 +27,7 @@ export default function ClientsList({ host_id }: Props) {
initial_res: pageProps?.clients || undefined, initial_res: pageProps?.clients || undefined,
}); });
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>({ const rules_by_client_id = useClientRulesMap(pageProps?.client_rules);
table: "client_rules",
sql_query: {
limit: 500,
},
initial_res: pageProps?.client_rules || undefined,
});
const rules_by_client_id = useMemo(() => {
const map = new Map<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>();
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]);
if (!clients) { if (!clients) {
return ( return (
@@ -81,33 +50,11 @@ export default function ClientsList({ host_id }: Props) {
} }
return ( return (
<Paper className="overflow-x-auto"> <Paper>
<table className="w-full min-w-[860px]"> <ClientsTable
<thead> clients={clients}
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]"> rules_by_client_id={rules_by_client_id}
<th className={thClass}>Client</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>Access</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
</tr>
</thead>
<tbody>
{clients.map((client) => (
<ClientRow
key={client.id}
client={client}
rules={
client.id
? rules_by_client_id.get(Number(client.id))
: undefined
}
/> />
))}
</tbody>
</table>
</Paper> </Paper>
); );
} }