feat: fix audit log display, add dashboard counts, restructure WireGuard page, add translations
This commit is contained in:
parent
f8ad3fe807
commit
ec55362d15
5 changed files with 373 additions and 288 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import db from "../db.js";
|
||||||
import internalDeadHost from "./dead-host.js";
|
import internalDeadHost from "./dead-host.js";
|
||||||
import internalProxyHost from "./proxy-host.js";
|
import internalProxyHost from "./proxy-host.js";
|
||||||
import internalRedirectionHost from "./redirection-host.js";
|
import internalRedirectionHost from "./redirection-host.js";
|
||||||
|
|
@ -23,15 +24,29 @@ const internalReport = {
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.then((counts) => {
|
.then(async (counts) => {
|
||||||
|
const knex = db();
|
||||||
|
let wgServers = 0;
|
||||||
|
let wgClients = 0;
|
||||||
|
try {
|
||||||
|
const srvResult = await knex("wg_interface").count("id as count").first();
|
||||||
|
wgServers = srvResult?.count || 0;
|
||||||
|
const cliResult = await knex("wg_client").count("id as count").first();
|
||||||
|
wgClients = cliResult?.count || 0;
|
||||||
|
} catch (_) {
|
||||||
|
// WireGuard tables may not exist yet
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
proxy: counts.shift(),
|
proxy: counts.shift(),
|
||||||
redirection: counts.shift(),
|
redirection: counts.shift(),
|
||||||
stream: counts.shift(),
|
stream: counts.shift(),
|
||||||
dead: counts.shift(),
|
dead: counts.shift(),
|
||||||
|
wgServers: Number(wgServers),
|
||||||
|
wgClients: Number(wgClients),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalReport;
|
export default internalReport;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,41 +34,11 @@ export function SiteFooter() {
|
||||||
<div className="col-12 col-lg-auto mt-3 mt-lg-0">
|
<div className="col-12 col-lg-auto mt-3 mt-lg-0">
|
||||||
<ul className="list-inline list-inline-dots mb-0">
|
<ul className="list-inline list-inline-dots mb-0">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
© 2025{" "}
|
© D3V.AC 2026{" "}
|
||||||
<a href="https://jc21.com" rel="noreferrer" target="_blank" className="link-secondary">
|
|
||||||
jc21.com
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
|
||||||
Theme by{" "}
|
|
||||||
<a href="https://tabler.io" rel="noreferrer" target="_blank" className="link-secondary">
|
|
||||||
Tabler
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<a
|
|
||||||
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${getVersion()}`}
|
|
||||||
className="link-secondary"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
{getVersion()}{" "}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{versionData?.updateAvailable && versionData?.latest && (
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<a
|
|
||||||
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${versionData.latest}`}
|
|
||||||
className="link-warning fw-bold"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
title={`New version ${versionData.latest} is available`}
|
|
||||||
>
|
|
||||||
<T id="update-available" data={{ latestVersion: versionData.latest }} />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
|
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconNetwork, IconServer, IconShield, IconUser } from "@tabler/icons-react";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import type { AuditLog } from "src/api/backend";
|
import type { AuditLog } from "src/api/backend";
|
||||||
import { useLocaleState } from "src/context";
|
import { useLocaleState } from "src/context";
|
||||||
|
|
@ -17,6 +17,12 @@ const getEventValue = (event: AuditLog) => {
|
||||||
return event.meta?.incomingPort || "N/A";
|
return event.meta?.incomingPort || "N/A";
|
||||||
case "certificate":
|
case "certificate":
|
||||||
return event.meta?.domainNames?.join(", ") || event.meta?.niceName || "N/A";
|
return event.meta?.domainNames?.join(", ") || event.meta?.niceName || "N/A";
|
||||||
|
case "wireguard-server":
|
||||||
|
return event.meta?.name || `Server #${event.objectId}`;
|
||||||
|
case "wireguard-client":
|
||||||
|
return event.meta?.name || `Client #${event.objectId}`;
|
||||||
|
case "wireguard-server-links":
|
||||||
|
return `Server #${event.objectId}`;
|
||||||
default:
|
default:
|
||||||
return `UNKNOWN EVENT TYPE: ${event.objectType}`;
|
return `UNKNOWN EVENT TYPE: ${event.objectType}`;
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +64,13 @@ const getIcon = (row: AuditLog) => {
|
||||||
case "certificate":
|
case "certificate":
|
||||||
ico = <IconShield size={16} className={c} />;
|
ico = <IconShield size={16} className={c} />;
|
||||||
break;
|
break;
|
||||||
|
case "wireguard-server":
|
||||||
|
case "wireguard-server-links":
|
||||||
|
ico = <IconServer size={16} className={c} />;
|
||||||
|
break;
|
||||||
|
case "wireguard-client":
|
||||||
|
ico = <IconNetwork size={16} className={c} />;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ico;
|
return ico;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react";
|
import {
|
||||||
|
IconArrowsCross,
|
||||||
|
IconBolt,
|
||||||
|
IconBoltOff,
|
||||||
|
IconDisc,
|
||||||
|
IconNetwork,
|
||||||
|
IconServer,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { HasPermission } from "src/components";
|
import { HasPermission } from "src/components";
|
||||||
import { useHostReport } from "src/hooks";
|
import { useHostReport } from "src/hooks";
|
||||||
import { T } from "src/locale";
|
import { T } from "src/locale";
|
||||||
import { DEAD_HOSTS, PROXY_HOSTS, REDIRECTION_HOSTS, STREAMS, VIEW } from "src/modules/Permissions";
|
import {
|
||||||
|
DEAD_HOSTS,
|
||||||
|
PROXY_HOSTS,
|
||||||
|
REDIRECTION_HOSTS,
|
||||||
|
STREAMS,
|
||||||
|
VIEW,
|
||||||
|
} from "src/modules/Permissions";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { data: hostReport } = useHostReport();
|
const { data: hostReport } = useHostReport();
|
||||||
|
|
@ -122,6 +135,58 @@ const Dashboard = () => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</HasPermission>
|
</HasPermission>
|
||||||
|
{/* WireGuard Servers */}
|
||||||
|
<div className="col-sm-6 col-lg-3">
|
||||||
|
<a
|
||||||
|
href="/wireguard"
|
||||||
|
className="card card-sm card-link card-link-pop"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate("/wireguard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col-auto">
|
||||||
|
<span className="bg-purple text-white avatar">
|
||||||
|
<IconServer />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="col">
|
||||||
|
<div className="font-weight-medium">
|
||||||
|
<T id="wireguard-servers.count" data={{ count: hostReport?.wgServers ?? 0 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* WireGuard Clients */}
|
||||||
|
<div className="col-sm-6 col-lg-3">
|
||||||
|
<a
|
||||||
|
href="/wireguard"
|
||||||
|
className="card card-sm card-link card-link-pop"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate("/wireguard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col-auto">
|
||||||
|
<span className="bg-cyan text-white avatar">
|
||||||
|
<IconNetwork />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="col">
|
||||||
|
<div className="font-weight-medium">
|
||||||
|
<T id="wireguard-clients.count" data={{ count: hostReport?.wgClients ?? 0 }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,6 @@ function timeAgo(date: string | null): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function WireGuard() {
|
function WireGuard() {
|
||||||
const [activeTab, setActiveTab] = useState<"servers" | "clients">("servers");
|
|
||||||
|
|
||||||
const { data: clients, isLoading: clientsLoading } = useWgClients();
|
const { data: clients, isLoading: clientsLoading } = useWgClients();
|
||||||
const { data: interfaces, isLoading: ifacesLoading } = useWgInterfaces();
|
const { data: interfaces, isLoading: ifacesLoading } = useWgInterfaces();
|
||||||
|
|
||||||
|
|
@ -64,19 +62,12 @@ function WireGuard() {
|
||||||
|
|
||||||
const [clientFilter, setClientFilter] = useState("");
|
const [clientFilter, setClientFilter] = useState("");
|
||||||
const [serverFilter, setServerFilter] = useState("");
|
const [serverFilter, setServerFilter] = useState("");
|
||||||
|
const [selectedServerId, setSelectedServerId] = useState<number | "all">("all");
|
||||||
|
|
||||||
if (clientsLoading || ifacesLoading) {
|
if (clientsLoading || ifacesLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredClients = clients?.filter(
|
|
||||||
(c) =>
|
|
||||||
!clientFilter ||
|
|
||||||
c.name.toLowerCase().includes(clientFilter.toLowerCase()) ||
|
|
||||||
c.ipv4Address.includes(clientFilter) ||
|
|
||||||
c.interfaceName?.toLowerCase().includes(clientFilter.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredInterfaces = interfaces?.filter(
|
const filteredInterfaces = interfaces?.filter(
|
||||||
(i) =>
|
(i) =>
|
||||||
!serverFilter ||
|
!serverFilter ||
|
||||||
|
|
@ -85,6 +76,23 @@ function WireGuard() {
|
||||||
(i.host && i.host.toLowerCase().includes(serverFilter.toLowerCase()))
|
(i.host && i.host.toLowerCase().includes(serverFilter.toLowerCase()))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filteredClients = clients?.filter((c) => {
|
||||||
|
// Filter by selected server
|
||||||
|
if (selectedServerId !== "all" && c.interfaceId !== selectedServerId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Filter by search text
|
||||||
|
if (
|
||||||
|
clientFilter &&
|
||||||
|
!c.name.toLowerCase().includes(clientFilter.toLowerCase()) &&
|
||||||
|
!c.ipv4Address.includes(clientFilter) &&
|
||||||
|
!c.interfaceName?.toLowerCase().includes(clientFilter.toLowerCase())
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Server Handlers
|
// Server Handlers
|
||||||
const handleNewServer = async () => {
|
const handleNewServer = async () => {
|
||||||
const result = (await EasyModal.show(WireGuardServerModal)) as any;
|
const result = (await EasyModal.show(WireGuardServerModal)) as any;
|
||||||
|
|
@ -159,235 +167,225 @@ function WireGuard() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* ================== SERVERS TABLE ================== */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">
|
||||||
|
<IconServer className="me-2" size={20} />
|
||||||
|
WireGuard Servers
|
||||||
|
<span className="badge bg-blue ms-2">{interfaces?.length || 0}</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
|
||||||
|
<div className="d-flex w-100 flex-column flex-md-row justify-content-between align-items-center">
|
||||||
|
<div className="text-muted d-none d-md-block">
|
||||||
|
Listing WireGuard Servers
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-wrap gap-2 justify-content-md-end w-100 w-md-auto align-items-center">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder="Search servers..."
|
||||||
|
value={serverFilter}
|
||||||
|
onChange={(e) => setServerFilter(e.target.value)}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={handleNewServer}
|
||||||
|
id="wg-new-server-btn"
|
||||||
|
>
|
||||||
|
<IconPlus size={16} className="me-1" />
|
||||||
|
New Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table className="table table-vcenter table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Interface</th>
|
||||||
|
<th>Subnet</th>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Endpoint Host</th>
|
||||||
|
<th>Isolation</th>
|
||||||
|
<th>Links</th>
|
||||||
|
<th className="text-end">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredInterfaces?.map((iface) => (
|
||||||
|
<tr key={iface.id}>
|
||||||
|
<td className="fw-bold">{iface.name}</td>
|
||||||
|
<td>
|
||||||
|
<code>{iface.ipv4Cidr}</code>
|
||||||
|
</td>
|
||||||
|
<td>{iface.listenPort}</td>
|
||||||
|
<td className="text-muted">{iface.host || "None"}</td>
|
||||||
|
<td>
|
||||||
|
{iface.isolateClients ? (
|
||||||
|
<span className="badge bg-green text-green-fg">Enabled</span>
|
||||||
|
) : (
|
||||||
|
<span className="badge bg-secondary text-secondary-fg">Disabled</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<span className="badge bg-azure me-2">{iface.linkedServers?.length || 0}</span>
|
||||||
|
{iface.linkedServers?.length > 0 && interfaces && (
|
||||||
|
<span className="text-muted small">
|
||||||
|
({interfaces.filter(i => iface.linkedServers.includes(i.id)).map(i => i.name).join(", ")})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<div className="btn-group btn-group-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
title="Linked Servers"
|
||||||
|
onClick={() => handleManageLinks(iface)}
|
||||||
|
>
|
||||||
|
<IconLink size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
title="Edit Server"
|
||||||
|
onClick={() => handleEditServer(iface)}
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-danger"
|
||||||
|
title="Delete Server"
|
||||||
|
onClick={() => handleDeleteServer(iface.id, iface.name)}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{(!filteredInterfaces || filteredInterfaces.length === 0) && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className="text-center text-muted py-5">
|
||||||
|
{serverFilter
|
||||||
|
? "No servers match your filter"
|
||||||
|
: "No WireGuard servers configured. Click 'New Server' to create one."}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ================== CLIENTS TABLE ================== */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<ul className="nav nav-tabs card-header-tabs" data-bs-toggle="tabs">
|
<h3 className="card-title">
|
||||||
<li className="nav-item cursor-pointer">
|
<IconNetwork className="me-2" size={20} />
|
||||||
<a className={`nav-link ${activeTab === "servers" ? "active" : ""}`} onClick={() => setActiveTab("servers")}>
|
WireGuard Clients
|
||||||
<IconServer className="me-1" size={16}/> Servers
|
<span className="badge bg-green ms-2">{clients?.length || 0}</span>
|
||||||
<span className="badge bg-blue ms-2">{interfaces?.length || 0}</span>
|
</h3>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item cursor-pointer">
|
|
||||||
<a className={`nav-link ${activeTab === "clients" ? "active" : ""}`} onClick={() => setActiveTab("clients")}>
|
|
||||||
<IconNetwork className="me-1" size={16}/> Clients
|
|
||||||
<span className="badge bg-green ms-2">{clients?.length || 0}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
{/* Tab Content */}
|
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
|
||||||
{activeTab === "clients" && (
|
<div className="d-flex w-100 flex-column flex-md-row justify-content-between align-items-center">
|
||||||
<div className="table-responsive">
|
<div className="text-muted d-none d-md-block">
|
||||||
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
|
Listing WireGuard Clients
|
||||||
<div className="d-flex w-100 flex-column flex-md-row justify-content-between align-items-center">
|
</div>
|
||||||
<div className="text-muted d-none d-md-block">
|
<div className="d-flex flex-wrap gap-2 justify-content-md-end w-100 w-md-auto align-items-center">
|
||||||
Listing WireGuard Clients
|
{/* Server filter dropdown */}
|
||||||
</div>
|
<select
|
||||||
<div className="d-flex flex-wrap gap-2 justify-content-md-end w-100 w-md-auto align-items-center">
|
className="form-select form-select-sm"
|
||||||
<input
|
style={{ width: 200 }}
|
||||||
type="text"
|
value={selectedServerId}
|
||||||
className="form-control form-control-sm"
|
onChange={(e) =>
|
||||||
placeholder="Search clients..."
|
setSelectedServerId(e.target.value === "all" ? "all" : Number(e.target.value))
|
||||||
value={clientFilter}
|
}
|
||||||
onChange={(e) => setClientFilter(e.target.value)}
|
>
|
||||||
style={{ width: 250 }}
|
<option value="all">All Servers</option>
|
||||||
/>
|
{interfaces?.map((iface) => (
|
||||||
<button
|
<option key={iface.id} value={iface.id}>
|
||||||
type="button"
|
{iface.name}
|
||||||
className="btn btn-primary btn-sm"
|
</option>
|
||||||
onClick={handleNewClient}
|
))}
|
||||||
id="wg-new-client-btn"
|
</select>
|
||||||
>
|
<input
|
||||||
<IconPlus size={16} className="me-1" />
|
type="text"
|
||||||
New Client
|
className="form-control form-control-sm"
|
||||||
</button>
|
placeholder="Search clients..."
|
||||||
</div>
|
value={clientFilter}
|
||||||
|
onChange={(e) => setClientFilter(e.target.value)}
|
||||||
|
style={{ width: 250 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={handleNewClient}
|
||||||
|
id="wg-new-client-btn"
|
||||||
|
>
|
||||||
|
<IconPlus size={16} className="me-1" />
|
||||||
|
New Client
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table className="table table-vcenter table-nowrap card-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Server</th>
|
|
||||||
<th>IP Address</th>
|
|
||||||
<th>Last Handshake</th>
|
|
||||||
<th>Transfer ↓ / ↑</th>
|
|
||||||
<th className="text-end">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredClients?.map((client) => {
|
|
||||||
const isConnected =
|
|
||||||
client.latestHandshakeAt &&
|
|
||||||
Date.now() - new Date(client.latestHandshakeAt).getTime() <
|
|
||||||
3 * 60 * 1000;
|
|
||||||
return (
|
|
||||||
<tr key={client.id}>
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
className={`badge ${
|
|
||||||
!client.enabled
|
|
||||||
? "bg-secondary"
|
|
||||||
: isConnected
|
|
||||||
? "bg-success"
|
|
||||||
: "bg-warning"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{!client.enabled
|
|
||||||
? "Disabled"
|
|
||||||
: isConnected
|
|
||||||
? "Connected"
|
|
||||||
: "Idle"}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="fw-bold">{client.name}</td>
|
|
||||||
<td>
|
|
||||||
<div className="text-muted">{client.interfaceName}</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code>{client.ipv4Address}</code>
|
|
||||||
</td>
|
|
||||||
<td>{timeAgo(client.latestHandshakeAt)}</td>
|
|
||||||
<td>
|
|
||||||
<div className="d-flex flex-column text-muted small">
|
|
||||||
<span>↓ {formatBytes(client.transferRx)}</span>
|
|
||||||
<span>↑ {formatBytes(client.transferTx)}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="text-end">
|
|
||||||
<div className="btn-group btn-group-sm">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
title="QR Code"
|
|
||||||
onClick={() =>
|
|
||||||
handleQR(client.id, client.name)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconQrcode size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
title="Download Config"
|
|
||||||
onClick={() =>
|
|
||||||
handleDownload(client.id, client.name)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconDownload size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn ${client.enabled ? "btn-outline-warning" : "btn-outline-success"}`}
|
|
||||||
title={
|
|
||||||
client.enabled ? "Disable" : "Enable"
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
handleToggleClient(client.id, client.enabled)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{client.enabled ? (
|
|
||||||
<IconPlayerPause size={16} />
|
|
||||||
) : (
|
|
||||||
<IconPlayerPlay size={16} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-danger"
|
|
||||||
title="Delete"
|
|
||||||
onClick={() =>
|
|
||||||
handleDeleteClient(client.id, client.name)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{(!filteredClients || filteredClients.length === 0) && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={7} className="text-center text-muted py-5">
|
|
||||||
{clientFilter
|
|
||||||
? "No clients match your filter"
|
|
||||||
: "No WireGuard clients yet. Click 'New Client' to create one."}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<table className="table table-vcenter table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
{activeTab === "servers" && (
|
<tr>
|
||||||
<div className="table-responsive">
|
<th>Status</th>
|
||||||
<div className="p-3 border-bottom d-flex align-items-center justify-content-between">
|
<th>Name</th>
|
||||||
<div className="d-flex w-100 flex-column flex-md-row justify-content-between align-items-center">
|
<th>Server</th>
|
||||||
<div className="text-muted d-none d-md-block">
|
<th>IP Address</th>
|
||||||
Listing WireGuard Servers
|
<th>Last Handshake</th>
|
||||||
</div>
|
<th>Transfer ↓ / ↑</th>
|
||||||
<div className="d-flex flex-wrap gap-2 justify-content-md-end w-100 w-md-auto align-items-center">
|
<th className="text-end">Actions</th>
|
||||||
<input
|
</tr>
|
||||||
type="text"
|
</thead>
|
||||||
className="form-control form-control-sm"
|
<tbody>
|
||||||
placeholder="Search servers..."
|
{filteredClients?.map((client) => {
|
||||||
value={serverFilter}
|
const isConnected =
|
||||||
onChange={(e) => setServerFilter(e.target.value)}
|
client.latestHandshakeAt &&
|
||||||
style={{ width: 250 }}
|
Date.now() - new Date(client.latestHandshakeAt).getTime() <
|
||||||
/>
|
3 * 60 * 1000;
|
||||||
<button
|
return (
|
||||||
type="button"
|
<tr key={client.id}>
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
onClick={handleNewServer}
|
|
||||||
>
|
|
||||||
<IconPlus size={16} className="me-1" />
|
|
||||||
New Server
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table className="table table-vcenter table-nowrap card-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Interface</th>
|
|
||||||
<th>Subnet</th>
|
|
||||||
<th>Port</th>
|
|
||||||
<th>Endpoint Host</th>
|
|
||||||
<th>Isolation</th>
|
|
||||||
<th>Links</th>
|
|
||||||
<th className="text-end">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredInterfaces?.map((iface) => (
|
|
||||||
<tr key={iface.id}>
|
|
||||||
<td className="fw-bold">{iface.name}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<code>{iface.ipv4Cidr}</code>
|
<span
|
||||||
|
className={`badge ${
|
||||||
|
!client.enabled
|
||||||
|
? "bg-secondary"
|
||||||
|
: isConnected
|
||||||
|
? "bg-success"
|
||||||
|
: "bg-warning"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{!client.enabled
|
||||||
|
? "Disabled"
|
||||||
|
: isConnected
|
||||||
|
? "Connected"
|
||||||
|
: "Idle"}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{iface.listenPort}</td>
|
<td className="fw-bold">{client.name}</td>
|
||||||
<td className="text-muted">{iface.host || "None"}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{iface.isolateClients ? (
|
<div className="text-muted">{client.interfaceName || "—"}</div>
|
||||||
<span className="badge bg-green text-green-fg">Enabled</span>
|
|
||||||
) : (
|
|
||||||
<span className="badge bg-secondary text-secondary-fg">Disabled</span>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="d-flex align-items-center">
|
<code>{client.ipv4Address}</code>
|
||||||
<span className="badge bg-azure me-2">{iface.linkedServers?.length || 0}</span>
|
</td>
|
||||||
{iface.linkedServers?.length > 0 && interfaces && (
|
<td>{timeAgo(client.latestHandshakeAt)}</td>
|
||||||
<span className="text-muted small">
|
<td>
|
||||||
({interfaces.filter(i => iface.linkedServers.includes(i.id)).map(i => i.name).join(", ")})
|
<div className="d-flex flex-column text-muted small">
|
||||||
</span>
|
<span>↓ {formatBytes(client.transferRx)}</span>
|
||||||
)}
|
<span>↑ {formatBytes(client.transferTx)}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
|
|
@ -395,42 +393,66 @@ function WireGuard() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-primary"
|
className="btn btn-outline-primary"
|
||||||
title="Linked Servers"
|
title="QR Code"
|
||||||
onClick={() => handleManageLinks(iface)}
|
onClick={() =>
|
||||||
|
handleQR(client.id, client.name)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<IconLink size={16} />
|
<IconQrcode size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-primary"
|
className="btn btn-outline-primary"
|
||||||
title="Edit Server"
|
title="Download Config"
|
||||||
onClick={() => handleEditServer(iface)}
|
onClick={() =>
|
||||||
|
handleDownload(client.id, client.name)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<IconEdit size={16} />
|
<IconDownload size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${client.enabled ? "btn-outline-warning" : "btn-outline-success"}`}
|
||||||
|
title={
|
||||||
|
client.enabled ? "Disable" : "Enable"
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
handleToggleClient(client.id, client.enabled)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{client.enabled ? (
|
||||||
|
<IconPlayerPause size={16} />
|
||||||
|
) : (
|
||||||
|
<IconPlayerPlay size={16} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-danger"
|
className="btn btn-outline-danger"
|
||||||
title="Delete Server"
|
title="Delete"
|
||||||
onClick={() => handleDeleteServer(iface.id, iface.name)}
|
onClick={() =>
|
||||||
|
handleDeleteClient(client.id, client.name)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<IconTrash size={16} />
|
<IconTrash size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
{(!interfaces || interfaces.length === 0) && (
|
})}
|
||||||
<tr>
|
{(!filteredClients || filteredClients.length === 0) && (
|
||||||
<td colSpan={7} className="text-center text-muted py-5">
|
<tr>
|
||||||
No WireGuard servers configured.
|
<td colSpan={7} className="text-center text-muted py-5">
|
||||||
</td>
|
{clientFilter || selectedServerId !== "all"
|
||||||
</tr>
|
? "No clients match your filter"
|
||||||
)}
|
: "No WireGuard clients yet. Click 'New Client' to create one."}
|
||||||
</tbody>
|
</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
)}
|
||||||
)}
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue