diff --git a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
index 328acf5b..33b20aff 100644
--- a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
+++ b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
@@ -3,6 +3,7 @@
import { useMemo, useState, useEffect } from "react";
import {
Alert,
+ Autocomplete,
Box,
Button,
Card,
@@ -16,6 +17,7 @@ import {
DialogTitle,
FormControlLabel,
IconButton,
+ InputAdornment,
MenuItem,
Stack,
Switch,
@@ -34,6 +36,9 @@ import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/Delete";
import CloseIcon from "@mui/icons-material/Close";
import SearchIcon from "@mui/icons-material/Search";
+import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
+import PlayArrowRoundedIcon from "@mui/icons-material/PlayArrowRounded";
+import PauseRoundedIcon from "@mui/icons-material/PauseRounded";
import { useFormState } from "react-dom";
import type { AccessList } from "@/src/lib/models/access-lists";
import type { Certificate } from "@/src/lib/models/certificates";
@@ -351,6 +356,7 @@ function CreateHostDialog({
{state.message}
)}
+
-
+
{certificates.map((cert) => (
@@ -388,11 +385,6 @@ function CreateHostDialog({
))}
-
-
-
-
-
)}
+
-
+
{certificates.map((cert) => (
@@ -512,11 +501,6 @@ function EditHostDialog({
))}
-
-
-
-
-
);
}
+
+type ToggleSetting = {
+ name: string;
+ label: string;
+ description: string;
+ defaultChecked: boolean;
+ color?: "success" | "warning" | "default";
+};
+
+function SettingsToggles({
+ hstsSubdomains = false,
+ skipHttpsValidation = false,
+ enabled = true
+}: {
+ hstsSubdomains?: boolean;
+ skipHttpsValidation?: boolean;
+ enabled?: boolean;
+}) {
+ const [values, setValues] = useState({
+ hsts_subdomains: hstsSubdomains,
+ skip_https_hostname_validation: skipHttpsValidation,
+ enabled: enabled
+ });
+
+ const handleChange = (name: keyof typeof values) => (event: React.ChangeEvent) => {
+ setValues(prev => ({ ...prev, [name]: event.target.checked }));
+ };
+
+ const toggleEnabled = () => {
+ setValues(prev => ({ ...prev, enabled: !prev.enabled }));
+ };
+
+ const settings: ToggleSetting[] = [
+ {
+ name: "hsts_subdomains",
+ label: "HSTS Subdomains",
+ description: "Include subdomains in the Strict-Transport-Security header",
+ defaultChecked: values.hsts_subdomains,
+ color: "default"
+ },
+ {
+ name: "skip_https_hostname_validation",
+ label: "Skip HTTPS Validation",
+ description: "Skip SSL certificate hostname verification for backend connections",
+ defaultChecked: values.skip_https_hostname_validation,
+ color: "warning"
+ }
+ ];
+
+ return (
+
+ {/* Prominent Enabled/Paused Control */}
+
+
+
+
+
+ {values.enabled ? (
+
+ ) : (
+
+ )}
+
+
+
+ {values.enabled ? "Active" : "Paused"}
+
+
+ {values.enabled
+ ? "This proxy host is enabled and routing traffic"
+ : "This proxy host is paused and not routing traffic"}
+
+
+
+ Click to {values.enabled ? "pause" : "activate"}
+
+
+
+
+ {/* Other Options */}
+
+
+
+ Advanced Options
+
+
+ }>
+ {settings.map((setting) => (
+
+
+
+
+
+ {setting.label}
+
+
+ {setting.description}
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
+
+const PROTOCOL_OPTIONS = ["http://", "https://"];
+
+type UpstreamEntry = {
+ protocol: string;
+ address: string;
+};
+
+function parseUpstream(upstream: string): UpstreamEntry {
+ if (upstream.startsWith("https://")) {
+ return { protocol: "https://", address: upstream.slice(8) };
+ }
+ if (upstream.startsWith("http://")) {
+ return { protocol: "http://", address: upstream.slice(7) };
+ }
+ // Default to http:// if no protocol specified
+ return { protocol: "http://", address: upstream };
+}
+
+function UpstreamInput({
+ defaultUpstreams = [],
+ name = "upstreams"
+}: {
+ defaultUpstreams?: string[];
+ name?: string;
+}) {
+ const initialEntries: UpstreamEntry[] = defaultUpstreams.length > 0
+ ? defaultUpstreams.map(parseUpstream)
+ : [{ protocol: "http://", address: "" }];
+
+ const [entries, setEntries] = useState(initialEntries);
+
+ const handleProtocolChange = (index: number, newProtocol: string | null) => {
+ const updated = [...entries];
+ updated[index].protocol = newProtocol || "http://";
+ setEntries(updated);
+ };
+
+ const handleAddressChange = (index: number, newAddress: string) => {
+ const updated = [...entries];
+ updated[index].address = newAddress;
+ setEntries(updated);
+ };
+
+ const handleAdd = () => {
+ setEntries([...entries, { protocol: "http://", address: "" }]);
+ };
+
+ const handleRemove = (index: number) => {
+ if (entries.length === 1) return;
+ setEntries(entries.filter((_, i) => i !== index));
+ };
+
+ // Serialize entries to a single string for form submission
+ const serializedValue = entries
+ .filter(e => e.address.trim() !== "")
+ .map(e => `${e.protocol}${e.address}`)
+ .join("\n");
+
+ return (
+
+
+
+ Upstreams
+
+
+ {entries.map((entry, index) => (
+
+ handleProtocolChange(index, newValue)}
+ onInputChange={(_, newInputValue) => {
+ if (newInputValue) {
+ handleProtocolChange(index, newInputValue);
+ }
+ }}
+ disableClearable
+ sx={{ width: 140 }}
+ renderInput={(params) => (
+
+ )}
+ />
+ handleAddressChange(index, e.target.value)}
+ placeholder="10.0.0.5:8080"
+ size="small"
+ fullWidth
+ required={index === 0}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ bgcolor: "rgba(20, 20, 22, 0.6)",
+ }
+ }}
+ />
+
+
+ handleRemove(index)}
+ disabled={entries.length === 1}
+ sx={{
+ color: entries.length === 1 ? "rgba(255, 255, 255, 0.2)" : "rgba(239, 68, 68, 0.7)",
+ "&:hover": { bgcolor: "rgba(239, 68, 68, 0.1)" },
+ mt: 0.5
+ }}
+ >
+
+
+
+
+
+ ))}
+ }
+ onClick={handleAdd}
+ size="small"
+ sx={{
+ alignSelf: "flex-start",
+ color: "rgba(99, 102, 241, 0.9)",
+ "&:hover": { bgcolor: "rgba(99, 102, 241, 0.1)" }
+ }}
+ >
+ Add Upstream
+
+
+
+ Backend servers to proxy requests to (supports load balancing with multiple upstreams)
+
+
+ );
+}