Add router provisioning workflow and filtering UI
This commit is contained in:
+57
-22
@@ -242,8 +242,11 @@ nav button:hover {
|
|||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel table {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
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 {
|
.custom-select {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -462,7 +451,7 @@ td {
|
|||||||
|
|
||||||
.custom-select-menu {
|
.custom-select-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 9999;
|
||||||
top: calc(100% + 6px);
|
top: calc(100% + 6px);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -593,12 +582,6 @@ td {
|
|||||||
background: #fef9c3;
|
background: #fef9c3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-action:disabled,
|
|
||||||
.small-action:disabled {
|
|
||||||
opacity: 0.55;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deployment-modal {
|
.deployment-modal {
|
||||||
width: 720px;
|
width: 720px;
|
||||||
max-width: calc(100vw - 48px);
|
max-width: calc(100vw - 48px);
|
||||||
@@ -644,3 +627,55 @@ td {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
font-family: "JetBrains Mono", Consolas, monospace;
|
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 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 = {
|
type Props = {
|
||||||
routers: RouterItem[];
|
routers: RouterItem[];
|
||||||
@@ -22,6 +24,32 @@ export function RoutersPage({
|
|||||||
onRemove,
|
onRemove,
|
||||||
onDownloadBundle,
|
onDownloadBundle,
|
||||||
}: Props) {
|
}: 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -37,9 +65,64 @@ export function RoutersPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel">
|
<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 ? (
|
{loading ? (
|
||||||
<p className="panel-empty">Loading routers...</p>
|
<p className="panel-empty">Loading routers...</p>
|
||||||
) : routers.length === 0 ? (
|
) : filteredRouters.length === 0 ? (
|
||||||
<p className="panel-empty">No routers found</p>
|
<p className="panel-empty">No routers found</p>
|
||||||
) : (
|
) : (
|
||||||
<table>
|
<table>
|
||||||
@@ -56,16 +139,18 @@ export function RoutersPage({
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{routers.map((router) => {
|
{filteredRouters.map((router) => {
|
||||||
const canProvision =
|
const canProvision =
|
||||||
router.status === "PENDING" || router.status === "FAILED";
|
router.status === "PENDING" || router.status === "FAILED";
|
||||||
|
|
||||||
const isProvisioned = router.status === "PROVISIONED";
|
const isProvisioned = router.status === "PROVISIONED";
|
||||||
const isBusy = actionLoading === router.id;
|
const isBusy = actionLoading === router.id;
|
||||||
|
|
||||||
const canDelete =
|
const canDelete =
|
||||||
router.status === "PENDING" ||
|
router.status === "PENDING" ||
|
||||||
router.status === "FAILED" ||
|
router.status === "FAILED" ||
|
||||||
router.status === "REMOVED";
|
router.status === "REMOVED";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={router.id}>
|
<tr key={router.id}>
|
||||||
<td>{router.name}</td>
|
<td>{router.name}</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user