Add host setup functions and client components

This commit is contained in:
2026-09-13 10:52:30 +01:00
parent 76679aabe7
commit 86eb3a0b05
53 changed files with 1284 additions and 1325 deletions
@@ -1,20 +0,0 @@
import AdminCard from "@/src/components/general/admin-card";
import ActivityRow from "../(partials)/activity-row";
import { ACTIVITY } from "../(data)/dashboard-mock-data";
export default function ActivitySection() {
return (
<AdminCard tier="secondary" className="w-full">
<div className="px-5 pt-4 pb-1">
<h2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60">
Recent activity
</h2>
</div>
<ul className="divide-y divide-slate-200/60 dark:divide-white/5">
{ACTIVITY.map((event) => (
<ActivityRow key={event.id} event={event} />
))}
</ul>
</AdminCard>
);
}
+10 -15
View File
@@ -1,23 +1,18 @@
import StatCard from "../(partials)/stat-card";
import Sparkline from "../(partials)/sparkline";
import { HERO_KPI } from "../(data)/dashboard-mock-data";
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
export default function KpiHeroSection() {
type Props = {
clients?: BUN_SQLITE_WGUI_CLIENTS[];
};
export default function KpiHeroSection({ clients }: Props) {
return (
<StatCard
tier="default"
label={HERO_KPI.label}
value={HERO_KPI.value}
delta={HERO_KPI.delta}
deltaTone={HERO_KPI.deltaTone}
subtext={HERO_KPI.subtext}
label="Total clients"
value={String(clients?.length || 0)}
subtext="WireGuard peers on this network"
valueClassName="text-4xl"
trailing={
<Sparkline
data={HERO_KPI.sparkline}
className="text-secondary dark:text-secondary"
/>
}
/>
);
}
}
@@ -1,18 +1,51 @@
import StatCard from "../(partials)/stat-card";
import { SECONDARY_KPIS } from "../(data)/dashboard-mock-data";
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
BUN_SQLITE_WGUI_VARIABLES,
} from "@/db/types/db";
type Props = {
clients?: BUN_SQLITE_WGUI_CLIENTS[];
hosts?: BUN_SQLITE_WGUI_HOSTS[];
variables?: BUN_SQLITE_WGUI_VARIABLES[];
};
export default function KpiSecondarySection({
clients,
hosts,
variables,
}: Props) {
const kpis = [
{
key: "k-clients",
label: "Total clients",
value: String(clients?.length || 0),
},
{
key: "k-hosts",
label: "Total hosts",
value: String(hosts?.length || 0),
},
{
key: "k-host-address",
label: "Main host address",
value:
variables?.find(
(v) => v.key == "main_host_wg_ip_address",
)?.value || "—",
},
];
export default function KpiSecondarySection() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 w-full">
{SECONDARY_KPIS.map((kpi) => (
{kpis.map((kpi) => (
<StatCard
key={kpi.id}
key={kpi.key}
label={kpi.label}
value={kpi.value}
delta={kpi.delta}
deltaTone={kpi.deltaTone}
/>
))}
</div>
);
}
}
@@ -1,9 +1,16 @@
import { ArrowUpRight } from "lucide-react";
import Link from "@/src/components/twui/layout/Link";
import H2 from "@/src/components/twui/layout/H2";
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 PeerRow from "../(partials)/peer-row";
import AsidePanel from "../(partials)/aside-panel";
import { PEERS } from "../(data)/dashboard-mock-data";
import ClientRow from "@/src/components/general/client-row";
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] " +
@@ -11,42 +18,54 @@ const thClass =
const thRightClass = `${thClass} text-right`;
export default function PeersTableSection() {
const RECENT_LIMIT = 8;
export default function PeersTableSection({ clients }: Props) {
const recent = (clients || []).slice(0, RECENT_LIMIT);
return (
<div className="grid grid-cols-1 lg:grid-cols-[1fr_260px] gap-4 w-full items-start">
<AdminCard className="overflow-hidden">
<div className="flex items-center justify-between px-5 h-11">
<h2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60">
Recent peers
</h2>
<Link
href="/admin/clients"
className="inline-flex items-center gap-0.5 no-underline! border-b-0! text-[12.5px] text-foreground-light/50 dark:text-foreground-dark/50 hover:text-foreground-light dark:hover:text-foreground-dark transition-colors"
>
View all
<ArrowUpRight size={12} className="-mt-0.5" />
</Link>
</div>
<AdminCard className="w-full overflow-hidden">
<Row className="justify-between gap-3 px-5 h-12 flex-wrap">
<H2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60 mb-0!">
Recent peers
</H2>
<Link
href="/admin/clients"
className="inline-flex items-center gap-0.5 no-underline! border-b-0! text-[12.5px] text-foreground-light/50 dark:text-foreground-dark/50 hover:text-foreground-light dark:hover:text-foreground-dark transition-colors"
>
View all
<ArrowUpRight size={12} className="-mt-0.5" />
</Link>
</Row>
{recent.length ? (
<div className="overflow-x-auto">
<table className="w-full min-w-[680px]">
<table className="w-full min-w-[860px]">
<thead>
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]">
<th className={thClass}>Peer</th>
<th className={thClass}>Client</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Endpoint</th>
<th className={thRightClass}>Traffic</th>
<th className={thRightClass}>Last handshake</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
</tr>
</thead>
<tbody>
{PEERS.map((peer) => (
<PeerRow key={peer.id} peer={peer} />
{recent.map((client) => (
<ClientRow key={client.id} client={client} />
))}
</tbody>
</table>
</div>
</AdminCard>
<AsidePanel />
</div>
) : (
<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">
No clients yet
</Span>
<Span className="text-[12.5px] text-foreground-light/35 dark:text-foreground-dark/35">
Manage peers from the Clients page
</Span>
</Stack>
)}
</AdminCard>
);
}
}
@@ -1,78 +0,0 @@
import { useMemo } from "react";
import AdminCard from "@/src/components/general/admin-card";
import TrafficChart from "../(partials)/traffic-chart";
import { TRAFFIC_SERIES } from "../(data)/dashboard-mock-data";
function formatGb(value: number) {
return `${value.toFixed(1)} GB`;
}
export default function TrafficChartSection() {
const footer = useMemo(() => {
const maxDown = Math.max(...TRAFFIC_SERIES.map((p) => p.down));
const maxUp = Math.max(...TRAFFIC_SERIES.map((p) => p.up));
const avgDown =
TRAFFIC_SERIES.reduce((sum, p) => sum + p.down, 0) /
TRAFFIC_SERIES.length;
const total =
TRAFFIC_SERIES.reduce((sum, p) => sum + p.down + p.up, 0);
return {
maxDown: formatGb(maxDown),
maxUp: formatGb(maxUp),
avgDown: formatGb(avgDown),
total: formatGb(total),
};
}, []);
const stats = [
{ label: "Peak download", value: footer.maxDown },
{ label: "Peak upload", value: footer.maxUp },
{ label: "Avg download", value: footer.avgDown },
{ label: "Total · 24h", value: footer.total },
];
return (
<AdminCard className="w-full">
<div className="flex items-center justify-between px-5 pt-4 flex-wrap gap-2">
<div>
<h2 className="text-[13.5px] font-medium text-foreground-light/60 dark:text-foreground-dark/60">
Network traffic
</h2>
<p className="text-[12px] text-foreground-light/40 dark:text-foreground-dark/40">
Last 24 hours
</p>
</div>
<div className="flex items-center gap-4">
<span className="inline-flex items-center gap-1.5 text-[12px] text-foreground-light/60 dark:text-foreground-dark/60">
<span className="w-[7px] h-[7px] rounded-full bg-secondary" />
Download
</span>
<span className="inline-flex items-center gap-1.5 text-[12px] text-foreground-light/60 dark:text-foreground-dark/60">
<span className="w-[7px] h-[7px] rounded-full bg-slate-400" />
Upload
</span>
</div>
</div>
<div className="px-5 pt-3 pb-1">
<TrafficChart data={TRAFFIC_SERIES} />
</div>
<div className="mx-5 mt-3 mb-4 pt-3 border-t border-slate-200 dark:border-white/10 flex items-center gap-5">
{stats.map((stat, i) => (
<div key={stat.label} className="flex items-center gap-5">
{i > 0 ? (
<span className="w-px h-4 bg-slate-200 dark:bg-white/10" />
) : null}
<div className="flex items-baseline gap-1.5">
<span className="text-[11.5px] text-foreground-light/45 dark:text-foreground-dark/45">
{stat.label}
</span>
<span className="tabular text-[13px] font-medium text-foreground-light/80 dark:text-foreground-dark/80">
{stat.value}
</span>
</div>
</div>
))}
</div>
</AdminCard>
);
}