Routers form working properly and listing
This commit is contained in:
+93
@@ -199,4 +199,97 @@ th {
|
||||
.badge.removed {
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 520px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-header p {
|
||||
margin: 4px 0 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border: 0;
|
||||
background: #f3f4f6;
|
||||
border-radius: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.router-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.router-form label {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.router-form input {
|
||||
height: 42px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 10px;
|
||||
padding: 0 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
padding: 12px 18px;
|
||||
background: #f3f4f6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
+125
-7
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiGet } from "./api";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { apiGet, apiPost } from "./api";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Router,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Activity,
|
||||
Server,
|
||||
Settings,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import "./App.css";
|
||||
|
||||
@@ -19,19 +20,66 @@ type RouterItem = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
type CreateRouterRequest = {
|
||||
name: string;
|
||||
serialNumber: string;
|
||||
lanIp: string;
|
||||
lanSubnet: string;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [page, setPage] = useState("dashboard");
|
||||
const [routers, setRouters] = useState<RouterItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [form, setForm] = useState<CreateRouterRequest>({
|
||||
name: "",
|
||||
serialNumber: "",
|
||||
lanIp: "",
|
||||
lanSubnet: "",
|
||||
});
|
||||
|
||||
async function loadRouters() {
|
||||
setLoading(true);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await apiGet<RouterItem[]>("/api/routers");
|
||||
const data = await apiGet<RouterItem[]>("/api/routers");
|
||||
setRouters(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load routers");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
setRouters(data);
|
||||
setLoading(false);
|
||||
async function createRouter(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
await apiPost<RouterItem, CreateRouterRequest>("/api/routers", form);
|
||||
|
||||
setCreateOpen(false);
|
||||
setForm({
|
||||
name: "",
|
||||
serialNumber: "",
|
||||
lanIp: "",
|
||||
lanSubnet: "",
|
||||
});
|
||||
|
||||
await loadRouters();
|
||||
setPage("routers");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to create router");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,6 +125,8 @@ function App() {
|
||||
</aside>
|
||||
|
||||
<main className="content">
|
||||
{error && <div className="error-banner">{error}</div>}
|
||||
|
||||
{page === "dashboard" && (
|
||||
<>
|
||||
<div className="page-header">
|
||||
@@ -103,7 +153,9 @@ function App() {
|
||||
<h1>Routers</h1>
|
||||
<p>Manage routers and OpenVPN provisioning</p>
|
||||
</div>
|
||||
<button className="primary">+ New Router</button>
|
||||
<button className="primary" onClick={() => setCreateOpen(true)}>
|
||||
+ New Router
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
@@ -143,6 +195,72 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{createOpen && (
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<div>
|
||||
<h2>New Router</h2>
|
||||
<p>Create a router before allocating VPN details.</p>
|
||||
</div>
|
||||
<button className="icon-button" onClick={() => setCreateOpen(false)}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={createRouter} className="router-form">
|
||||
<label>
|
||||
Router Name
|
||||
<input
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="Ex: Loja Braga"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Serial Number
|
||||
<input
|
||||
value={form.serialNumber}
|
||||
onChange={(e) => setForm({ ...form, serialNumber: e.target.value })}
|
||||
placeholder="Ex: LR-001"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
LAN IP
|
||||
<input
|
||||
required
|
||||
value={form.lanIp}
|
||||
onChange={(e) => setForm({ ...form, lanIp: e.target.value })}
|
||||
placeholder="Ex: 192.168.60.1"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
LAN Subnet
|
||||
<input
|
||||
required
|
||||
value={form.lanSubnet}
|
||||
onChange={(e) => setForm({ ...form, lanSubnet: e.target.value })}
|
||||
placeholder="Ex: 192.168.60.0/24"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="secondary" onClick={() => setCreateOpen(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="primary" disabled={saving}>
|
||||
{saving ? "Saving..." : "Save Router"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+24
@@ -2,6 +2,9 @@ const API_BASE = import.meta.env.VITE_API_BASE;
|
||||
const API_KEY = import.meta.env.VITE_API_KEY;
|
||||
|
||||
export async function apiGet<T>(path: string): Promise<T> {
|
||||
console.log("API_BASE:", API_BASE);
|
||||
console.log("Calling:", `${API_BASE}${path}`);
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
headers: {
|
||||
"X-API-Key": API_KEY,
|
||||
@@ -12,5 +15,26 @@ export async function apiGet<T>(path: string): Promise<T> {
|
||||
throw new Error(`API error ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiPost<TResponse, TBody>(
|
||||
path: string,
|
||||
body: TBody
|
||||
): Promise<TResponse> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": API_KEY,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => null);
|
||||
throw new Error(error?.error || `API error ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
Reference in New Issue
Block a user