From 18c890bb21a7aa3ece15d982e62665e4db762b39 Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:55:40 +0100 Subject: [PATCH] feat: redesign GeoBlockFields UI with tabs, Autocomplete tag inputs, and accordion --- src/components/proxy-hosts/GeoBlockFields.tsx | 535 +++++++++--------- 1 file changed, 263 insertions(+), 272 deletions(-) diff --git a/src/components/proxy-hosts/GeoBlockFields.tsx b/src/components/proxy-hosts/GeoBlockFields.tsx index 541e4145..47e5a196 100644 --- a/src/components/proxy-hosts/GeoBlockFields.tsx +++ b/src/components/proxy-hosts/GeoBlockFields.tsx @@ -1,20 +1,30 @@ "use client"; import { + Accordion, + AccordionDetails, + AccordionSummary, + Autocomplete, Box, Chip, Collapse, Divider, - FormControlLabel, + Grid, IconButton, Stack, Switch, + Tab, + Tabs, TextField, ToggleButton, ToggleButtonGroup, + Tooltip, Typography } from "@mui/material"; -import { useState, KeyboardEvent } from "react"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import AddIcon from "@mui/icons-material/Add"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import { useState, SyntheticEvent } from "react"; import { GeoBlockSettings } from "@/src/lib/settings"; import { GeoBlockMode } from "@/src/lib/models/proxy-hosts"; @@ -25,109 +35,73 @@ type TagInputProps = { label: string; initialValues?: string[]; placeholder?: string; + helperText?: string; validate?: (value: string) => boolean; uppercase?: boolean; }; -function TagInput({ name, label, initialValues = [], placeholder, validate, uppercase = false }: TagInputProps) { +function TagInput({ name, label, initialValues = [], placeholder, helperText, validate, uppercase = false }: TagInputProps) { const [tags, setTags] = useState(initialValues); - const [inputValue, setInputValue] = useState(""); + + function processValue(raw: string): string { + return uppercase ? raw.trim().toUpperCase() : raw.trim(); + } function addTag(raw: string) { - const value = uppercase ? raw.trim().toUpperCase() : raw.trim(); + const value = processValue(raw); if (!value) return; if (validate && !validate(value)) return; if (tags.includes(value)) return; setTags((prev) => [...prev, value]); - setInputValue(""); - } - - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); - addTag(inputValue); - } else if (e.key === "Backspace" && inputValue === "" && tags.length > 0) { - setTags((prev) => prev.slice(0, -1)); - } - } - - function handleBlur() { - if (inputValue.trim()) { - addTag(inputValue); - } - } - - function removeTag(index: number) { - setTags((prev) => prev.filter((_, i) => i !== index)); } return ( - { + const processed = newValue.map((v) => processValue(v as string)).filter((v) => { + if (!v) return false; + if (validate && !validate(v)) return false; + return true; + }); + // Deduplicate + setTags([...new Set(processed)]); }} - onClick={(e) => { - const input = (e.currentTarget as HTMLElement).querySelector("input[type='text']") as HTMLInputElement | null; - input?.focus(); + onBlur={(e) => { + const input = (e.target as HTMLInputElement).value; + if (input.trim()) addTag(input); }} - > - - {label} - - {tags.map((tag, i) => ( - + value.map((option, index) => { + const { key, ...tagProps } = getTagProps({ index }); + return ; + }) + } + renderInput={(params) => ( + removeTag(i)} + onKeyDown={(e) => { + if (e.key === "," || e.key === " ") { + e.preventDefault(); + const input = (e.target as HTMLInputElement).value; + if (input.trim()) { + addTag(input); + (e.target as HTMLInputElement).value = ""; + } + } + }} /> - ))} - setInputValue(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleBlur} - placeholder={tags.length === 0 ? placeholder : ""} - style={{ - border: "none", - outline: "none", - background: "transparent", - fontSize: "0.875rem", - flexGrow: 1, - minWidth: 80, - padding: "2px 4px" - }} - /> - + )} + /> ); } @@ -141,63 +115,51 @@ function ResponseHeadersEditor({ initialHeaders }: { initialHeaders: Record ({ key, value })) ); - function addRow() { - setRows((prev) => [...prev, { key: "", value: "" }]); - } - - function removeRow(index: number) { - setRows((prev) => prev.filter((_, i) => i !== index)); - } - - function updateRow(index: number, field: "key" | "value", val: string) { - setRows((prev) => prev.map((row, i) => (i === index ? { ...row, [field]: val } : row))); - } - return ( - + 0 ? 1 : 0}> - Response Headers + Custom Response Headers - - + - + + setRows((prev) => [...prev, { key: "", value: "" }])}> + + + - {rows.length === 0 && ( + {rows.length === 0 ? ( - No custom response headers. Click + to add one. + No custom headers — click + to add one. + ) : ( + + {rows.map((row, i) => ( + + + + setRows((prev) => prev.map((r, j) => j === i ? { ...r, key: e.target.value } : r))} + size="small" + fullWidth + /> + setRows((prev) => prev.map((r, j) => j === i ? { ...r, value: e.target.value } : r))} + size="small" + fullWidth + /> + + setRows((prev) => prev.filter((_, j) => j !== i))} sx={{ mt: 0.5 }}> + + + + + ))} + )} - - {rows.map((row, i) => ( - - - - updateRow(i, "key", e.target.value)} - size="small" - fullWidth - /> - updateRow(i, "value", e.target.value)} - size="small" - fullWidth - /> - removeRow(i)} title="Remove header" color="error"> - × - - - ))} - ); } @@ -216,6 +178,7 @@ export function GeoBlockFields({ initialValues, showModeSelector = true }: GeoBl const initial = initialValues?.geoblock ?? null; const [enabled, setEnabled] = useState(initial?.enabled ?? false); const [mode, setMode] = useState(initialValues?.geoblock_mode ?? "merge"); + const [rulesTab, setRulesTab] = useState(0); return ( theme.palette.mode === "dark" ? "rgba(237,108,2,0.06)" : "rgba(237,108,2,0.04)", + p: 2 }} > - {/* Always-present sentinel */} - - {/* Header row: title + toggle */} - - - - Geo Blocking - - - Block or allow traffic by country, continent, ASN, CIDR, or IP - - - setEnabled(checked)} - /> - + {/* Header */} + + + + Geo Blocking + + + Block or allow traffic by country, continent, ASN, CIDR, or IP + + + setEnabled(checked)} + /> + - {/* Mode selector (merge vs override) */} - - {showModeSelector && ( - - { - if (newMode) setMode(newMode); - }} - > - Merge with global - Override global - - - )} + {/* Mode selector */} + + {showModeSelector && ( + + { if (v) setMode(v); }} + > + Merge with global + Override global + + + )} - {/* Collapsible detail fields */} - - - + {/* Detail fields */} + + + - {/* Block Rules */} - - - Block Rules - - + {/* Block / Allow tabs */} + setRulesTab(v)} + variant="fullWidth" + sx={{ mb: 2, "& .MuiTab-root": { textTransform: "none", fontWeight: 500 } }} + > + + + + + {/* Block Rules */} + - + + + - - - {/* Allow Rules */} - - - Allow Rules{" "} - - (override block rules) - - - + {/* Allow Rules */} + - + + + - - - {/* Trusted Proxies */} - - - Trusted Proxies - - - - - - - {/* Block Response */} - - - Block Response - - - - - + + } sx={{ minHeight: 44, "& .MuiAccordionSummary-content": { my: 0.5 } }}> + Trusted Proxies & Block Response + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + ); }