Add router provisioning workflow and filtering UI
This commit is contained in:
+57
-22
@@ -242,8 +242,11 @@ nav button:hover {
|
||||
|
||||
.panel {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.panel table {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -428,20 +431,6 @@ td {
|
||||
}
|
||||
}
|
||||
|
||||
.router-form select {
|
||||
height: 40px;
|
||||
border: 1px solid #dfe3e8;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
background: #fbfbfc;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.router-form select:focus {
|
||||
outline: 2px solid rgba(93, 168, 255, 0.25);
|
||||
border-color: #5da8ff;
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
position: relative;
|
||||
}
|
||||
@@ -462,7 +451,7 @@ td {
|
||||
|
||||
.custom-select-menu {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
z-index: 9999;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -593,12 +582,6 @@ td {
|
||||
background: #fef9c3;
|
||||
}
|
||||
|
||||
.table-action:disabled,
|
||||
.small-action:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.deployment-modal {
|
||||
width: 720px;
|
||||
max-width: calc(100vw - 48px);
|
||||
@@ -644,3 +627,55 @@ td {
|
||||
white-space: pre-wrap;
|
||||
font-family: "JetBrains Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #edf0f3;
|
||||
background: white;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
height: 40px;
|
||||
min-width: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
background: #fbfbfc;
|
||||
border: 1px solid #dfe3e8;
|
||||
border-radius: 10px;
|
||||
padding: 0 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
.toolbar-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.toolbar-select .custom-select-button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: #64748b;
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import type { RouterItem } from "../types/router";
|
||||
import { Download, Play, RotateCcw, Trash2 } from "lucide-react";
|
||||
import { Download, Play, RotateCcw, Search, Trash2 } from "lucide-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
routers: RouterItem[];
|
||||
@@ -22,6 +24,32 @@ export function RoutersPage({
|
||||
onRemove,
|
||||
onDownloadBundle,
|
||||
}: Props) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("ALL");
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
|
||||
const filteredRouters = useMemo(() => {
|
||||
return routers.filter((router) => {
|
||||
const query = search.toLowerCase().trim();
|
||||
|
||||
const matchesSearch =
|
||||
router.name.toLowerCase().includes(query) ||
|
||||
router.serialNumber?.toLowerCase().includes(query) ||
|
||||
router.lanIp.toLowerCase().includes(query) ||
|
||||
router.lanSubnet.toLowerCase().includes(query) ||
|
||||
router.vpnIp?.toLowerCase().includes(query);
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "ALL" || router.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [routers, search, statusFilter]);
|
||||
|
||||
const selectedStatusLabel =
|
||||
statusFilter === "ALL"
|
||||
? "All Statuses"
|
||||
: statusFilter.charAt(0) + statusFilter.slice(1).toLowerCase();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -37,9 +65,64 @@ export function RoutersPage({
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="table-toolbar">
|
||||
<div className="search-box">
|
||||
<Search size={16} />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search routers..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="custom-select toolbar-select">
|
||||
<button
|
||||
type="button"
|
||||
className="custom-select-button"
|
||||
onClick={() => setStatusOpen(!statusOpen)}
|
||||
>
|
||||
<span>{selectedStatusLabel}</span>
|
||||
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={statusOpen ? "chevron open" : "chevron"}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{statusOpen && (
|
||||
<div className="custom-select-menu">
|
||||
{[
|
||||
"ALL",
|
||||
"PENDING",
|
||||
"PROVISIONING",
|
||||
"PROVISIONED",
|
||||
"FAILED",
|
||||
"REMOVING",
|
||||
"REMOVED",
|
||||
].map((status) => (
|
||||
<button
|
||||
key={status}
|
||||
type="button"
|
||||
className="custom-select-option"
|
||||
onClick={() => {
|
||||
setStatusFilter(status);
|
||||
setStatusOpen(false);
|
||||
}}
|
||||
>
|
||||
{status === "ALL"
|
||||
? "All Statuses"
|
||||
: status.charAt(0) +
|
||||
status.slice(1).toLowerCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="panel-empty">Loading routers...</p>
|
||||
) : routers.length === 0 ? (
|
||||
) : filteredRouters.length === 0 ? (
|
||||
<p className="panel-empty">No routers found</p>
|
||||
) : (
|
||||
<table>
|
||||
@@ -56,16 +139,18 @@ export function RoutersPage({
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{routers.map((router) => {
|
||||
{filteredRouters.map((router) => {
|
||||
const canProvision =
|
||||
router.status === "PENDING" || router.status === "FAILED";
|
||||
|
||||
const isProvisioned = router.status === "PROVISIONED";
|
||||
const isBusy = actionLoading === router.id;
|
||||
|
||||
const canDelete =
|
||||
router.status === "PENDING" ||
|
||||
router.status === "FAILED" ||
|
||||
router.status === "REMOVED";
|
||||
|
||||
return (
|
||||
<tr key={router.id}>
|
||||
<td>{router.name}</td>
|
||||
|
||||
Reference in New Issue
Block a user