From 69540cb4c4ea009a268dfa245851addfd4103682 Mon Sep 17 00:00:00 2001 From: litoral05 Date: Wed, 6 May 2026 10:43:24 +0100 Subject: [PATCH] Add router provisioning workflow and filtering UI --- src/App.css | 79 +++++++++++++++++++++++---------- src/pages/RoutersPage.tsx | 91 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 145 insertions(+), 25 deletions(-) diff --git a/src/App.css b/src/App.css index c669a30..e54b82e 100644 --- a/src/App.css +++ b/src/App.css @@ -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); @@ -643,4 +626,56 @@ td { line-height: 1.5; 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); } \ No newline at end of file diff --git a/src/pages/RoutersPage.tsx b/src/pages/RoutersPage.tsx index f8f351f..afe1611 100644 --- a/src/pages/RoutersPage.tsx +++ b/src/pages/RoutersPage.tsx @@ -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,7 +24,33 @@ 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({
+
+
+ + setSearch(e.target.value)} + placeholder="Search routers..." + /> +
+ +
+ + + {statusOpen && ( +
+ {[ + "ALL", + "PENDING", + "PROVISIONING", + "PROVISIONED", + "FAILED", + "REMOVING", + "REMOVED", + ].map((status) => ( + + ))} +
+ )} +
+
+ {loading ? (

Loading routers...

- ) : routers.length === 0 ? ( + ) : filteredRouters.length === 0 ? (

No routers found

) : ( @@ -56,16 +139,18 @@ export function RoutersPage({ - {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 (
{router.name}