UNPKG

@axlotl-lab/navigrator

Version:

A powerful local domain manager for development environments. Navigrator helps you manage local domains and SSL certificates with a simple web interface.

529 lines (528 loc) 33 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = __importStar(require("react")); require("./styles.css"); function App() { const [domains, setDomains] = (0, react_1.useState)([]); const [certificates, setCertificates] = (0, react_1.useState)([]); const [proxies, setProxies] = (0, react_1.useState)([]); const [statuses, setStatuses] = (0, react_1.useState)({}); const [newDomain, setNewDomain] = (0, react_1.useState)(''); const [loading, setLoading] = (0, react_1.useState)(true); const [error, setError] = (0, react_1.useState)(null); const [notification, setNotification] = (0, react_1.useState)(null); const [activeTab, setActiveTab] = (0, react_1.useState)('domains'); const [confirmImport, setConfirmImport] = (0, react_1.useState)(false); const [confirmDeleteCertificate, setConfirmDeleteCertificate] = (0, react_1.useState)(null); const [confirmDeleteProxy, setConfirmDeleteProxy] = (0, react_1.useState)(null); const [newProxy, setNewProxy] = (0, react_1.useState)({ domain: '', target: '', port: 443 }); const [editProxy, setEditProxy] = (0, react_1.useState)(null); (0, react_1.useEffect)(() => { fetchData(); }, []); const fetchData = () => __awaiter(this, void 0, void 0, function* () { setLoading(true); setError(null); try { yield Promise.all([ fetchHosts(), fetchCertificates(), fetchProxies() ]); setLoading(false); } catch (error) { setError('Failed to load data. Please check if the server is running with admin privileges.'); setLoading(false); } }); const fetchHosts = () => __awaiter(this, void 0, void 0, function* () { const response = yield fetch('/api/hosts'); if (!response.ok) throw new Error('Failed to fetch hosts'); const data = yield response.json(); setDomains(data.hosts); yield Promise.all(data.hosts.map((host) => fetchDomainStatus(host.domain))); }); const fetchCertificates = () => __awaiter(this, void 0, void 0, function* () { const response = yield fetch('/api/certificates'); if (!response.ok) throw new Error('Failed to fetch certificates'); const data = yield response.json(); setCertificates(data.certificates); }); const fetchProxies = () => __awaiter(this, void 0, void 0, function* () { const response = yield fetch('/api/proxies'); if (!response.ok) throw new Error('Failed to fetch proxies'); const data = yield response.json(); setProxies(data.proxies); }); const fetchDomainStatus = (domain) => __awaiter(this, void 0, void 0, function* () { const response = yield fetch(`/api/status/${domain}`); if (!response.ok) return; const data = yield response.json(); setStatuses(prev => (Object.assign(Object.assign({}, prev), { [domain]: data.status }))); }); const addDomain = (e) => __awaiter(this, void 0, void 0, function* () { e.preventDefault(); if (!newDomain) return; setLoading(true); try { const hostResponse = yield fetch('/api/hosts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain: newDomain }) }); if (!hostResponse.ok) throw new Error('Failed to add host'); const certResponse = yield fetch('/api/certificates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain: newDomain }) }); if (!certResponse.ok) throw new Error('Failed to create certificate'); yield fetchData(); showNotification(`Domain ${newDomain} added successfully`, 'success'); setNewDomain(''); } catch (error) { showNotification(`Error adding domain: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const adoptDomain = (domain, ip) => __awaiter(this, void 0, void 0, function* () { setLoading(true); try { const response = yield fetch(`/api/hosts/${domain}/adopt`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ip }) }); if (!response.ok) throw new Error('Failed to adopt host'); yield fetchData(); showNotification(`Domain ${domain} adopted successfully`, 'success'); } catch (error) { showNotification(`Error adopting domain: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const importAllDomains = () => __awaiter(this, void 0, void 0, function* () { setLoading(true); setConfirmImport(false); try { const response = yield fetch('/api/hosts/import-all', { method: 'POST' }); if (!response.ok) throw new Error('Failed to import hosts'); const data = yield response.json(); yield fetchData(); showNotification(data.message, 'success'); } catch (error) { showNotification(`Error importing domains: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const removeDomain = (domain) => __awaiter(this, void 0, void 0, function* () { if (!confirm(`Are you sure you want to remove ${domain}?`)) return; setLoading(true); try { const response = yield fetch(`/api/hosts/${domain}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to remove host'); const data = yield response.json(); yield fetchData(); showNotification(data.message, 'success'); } catch (error) { showNotification(`Error removing domain: ${error === null || error === void 0 ? void 0 : error.message} `, 'error'); } finally { setLoading(false); } }); const toggleDomainState = (domain, disable) => __awaiter(this, void 0, void 0, function* () { setLoading(true); try { const response = yield fetch(`/api/hosts/${domain}/toggle`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: disable }) }); if (!response.ok) throw new Error('Failed to toggle domain state'); yield fetchData(); const state = disable ? 'disabled' : 'enabled'; showNotification(`Domain ${domain} ${state} successfully`, 'success'); } catch (error) { showNotification(`Error updating domain state: ${error === null || error === void 0 ? void 0 : error.message} `, 'error'); } finally { setLoading(false); } }); const deleteCertificate = (domain) => __awaiter(this, void 0, void 0, function* () { setConfirmDeleteCertificate(null); setLoading(true); try { const response = yield fetch(`/api/certificates/${domain}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete certificate'); yield fetchData(); showNotification(`Certificate for ${domain} deleted successfully`, 'success'); } catch (error) { showNotification(`Error deleting certificate: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const addProxy = (e) => __awaiter(this, void 0, void 0, function* () { e.preventDefault(); if (!newProxy.domain || !newProxy.target) return; setLoading(true); try { const response = yield fetch('/api/proxies', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newProxy) }); if (!response.ok) { const errorData = yield response.json(); throw new Error(errorData.error || 'Failed to add proxy'); } yield fetchProxies(); showNotification(`Proxy for ${newProxy.domain} added successfully`, 'success'); setNewProxy({ domain: '', target: '', port: 443 }); } catch (error) { showNotification(`Error adding proxy: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const updateProxy = (e) => __awaiter(this, void 0, void 0, function* () { e.preventDefault(); if (!editProxy) return; setLoading(true); try { const response = yield fetch(`/api/proxies/${editProxy.domain}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: editProxy.target, port: editProxy.port }) }); if (!response.ok) throw new Error('Failed to update proxy'); yield fetchProxies(); showNotification(`Proxy for ${editProxy.domain} updated successfully`, 'success'); setEditProxy(null); } catch (error) { showNotification(`Error updating proxy: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const deleteProxy = (domain) => __awaiter(this, void 0, void 0, function* () { setConfirmDeleteProxy(null); setLoading(true); try { const response = yield fetch(`/api/proxies/${domain}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete proxy'); yield fetchProxies(); showNotification(`Proxy for ${domain} deleted successfully`, 'success'); } catch (error) { showNotification(`Error deleting proxy: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const toggleProxy = (domain, shouldStart) => __awaiter(this, void 0, void 0, function* () { setLoading(true); try { const endpoint = shouldStart ? 'start' : 'stop'; const response = yield fetch(`/api/proxies/${domain}/${endpoint}`, { method: 'POST' }); if (!response.ok) { const errorData = yield response.json(); throw new Error(errorData.error || `Failed to ${endpoint} proxy`); } yield fetchProxies(); const action = shouldStart ? 'started' : 'stopped'; showNotification(`Proxy for ${domain} ${action} successfully`, 'success'); } catch (error) { showNotification(`Error toggling proxy: ${error === null || error === void 0 ? void 0 : error.message}`, 'error'); } finally { setLoading(false); } }); const showNotification = (message, type) => { setNotification({ message, type }); setTimeout(() => setNotification(null), 3000); }; const refreshCertificate = (domain) => __awaiter(this, void 0, void 0, function* () { setLoading(true); try { const response = yield fetch('/api/certificates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain }) }); if (!response.ok) throw new Error('Failed to refresh certificate'); yield fetchData(); showNotification(`Certificate for ${domain} refreshed successfully`, 'success'); } catch (error) { showNotification(`Error refreshing certificate: ${error === null || error === void 0 ? void 0 : error.message} `, 'error'); } finally { setLoading(false); } }); return (react_1.default.createElement("div", { className: "app-container" }, react_1.default.createElement("header", { className: "app-header" }, react_1.default.createElement("h1", null, "@axlotl-lab/navigrator"), react_1.default.createElement("p", { className: "subtitle" }, "Local Domain Manager")), loading && react_1.default.createElement("div", { className: "loading" }, "Loading..."), error && (react_1.default.createElement("div", { className: "error-banner" }, react_1.default.createElement("p", null, error), react_1.default.createElement("button", { onClick: fetchData }, "Retry"))), notification && (react_1.default.createElement("div", { className: `notification ${notification.type} ` }, notification.message)), confirmImport && (react_1.default.createElement("div", { className: "confirmation-dialog" }, react_1.default.createElement("div", { className: "confirmation-content" }, react_1.default.createElement("h3", null, "Import All Local Domains"), react_1.default.createElement("p", null, "This will mark all local domains as managed by Navigrator. Are you sure?"), react_1.default.createElement("div", { className: "confirmation-actions" }, react_1.default.createElement("button", { className: "button danger", onClick: () => setConfirmImport(false) }, "Cancel"), react_1.default.createElement("button", { className: "button success", onClick: () => importAllDomains() }, "Confirm Import"))))), confirmDeleteCertificate && (react_1.default.createElement("div", { className: "confirmation-dialog" }, react_1.default.createElement("div", { className: "confirmation-content" }, react_1.default.createElement("h3", null, "Delete Certificate"), react_1.default.createElement("p", null, "Are you sure you want to delete the certificate for ", confirmDeleteCertificate, "?"), react_1.default.createElement("p", { className: "warning-text" }, "This action cannot be undone."), react_1.default.createElement("div", { className: "confirmation-actions" }, react_1.default.createElement("button", { className: "button secondary", onClick: () => setConfirmDeleteCertificate(null) }, "Cancel"), react_1.default.createElement("button", { className: "button danger", onClick: () => deleteCertificate(confirmDeleteCertificate) }, "Delete Certificate"))))), confirmDeleteProxy && (react_1.default.createElement("div", { className: "confirmation-dialog" }, react_1.default.createElement("div", { className: "confirmation-content" }, react_1.default.createElement("h3", null, "Delete Proxy"), react_1.default.createElement("p", null, "Are you sure you want to delete the proxy for ", confirmDeleteProxy, "?"), react_1.default.createElement("p", { className: "warning-text" }, "If the proxy is running, it will be stopped."), react_1.default.createElement("div", { className: "confirmation-actions" }, react_1.default.createElement("button", { className: "button secondary", onClick: () => setConfirmDeleteProxy(null) }, "Cancel"), react_1.default.createElement("button", { className: "button danger", onClick: () => deleteProxy(confirmDeleteProxy) }, "Delete Proxy"))))), react_1.default.createElement("div", { className: "tabs" }, react_1.default.createElement("button", { className: activeTab === 'domains' ? 'active' : '', onClick: () => setActiveTab('domains') }, "Domains"), react_1.default.createElement("button", { className: activeTab === 'certificates' ? 'active' : '', onClick: () => setActiveTab('certificates') }, "Certificates"), react_1.default.createElement("button", { className: activeTab === 'proxies' ? 'active' : '', onClick: () => setActiveTab('proxies') }, "Proxies")), react_1.default.createElement("div", { className: "tab-content" }, activeTab === 'domains' && (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement("form", { className: "add-domain-form", onSubmit: addDomain }, react_1.default.createElement("h2", null, "Add New Domain"), react_1.default.createElement("div", { className: "form-row" }, react_1.default.createElement("input", { type: "text", placeholder: "Enter domain (e.g. myapp.local)", value: newDomain, onChange: (e) => setNewDomain(e.target.value), disabled: loading }), react_1.default.createElement("button", { type: "submit", disabled: loading || !newDomain }, "Add Domain")), react_1.default.createElement("p", { className: "help-text" }, "All domains will point to 127.0.0.1 and include a local SSL certificate")), react_1.default.createElement("div", { className: "domains-list" }, react_1.default.createElement("div", { className: "domains-header" }, react_1.default.createElement("h2", null, "Your Local Domains"), react_1.default.createElement("button", { className: "button secondary import-button", onClick: () => setConfirmImport(true), disabled: loading, title: "Import all local domains to manage them with Navigrator" }, "Import All Domains")), domains.length === 0 ? (react_1.default.createElement("p", { className: "no-data" }, "No domains configured yet")) : (react_1.default.createElement("table", null, react_1.default.createElement("thead", null, react_1.default.createElement("tr", null, react_1.default.createElement("th", null, "Domain"), react_1.default.createElement("th", null, "Certificate"), react_1.default.createElement("th", null, "State"), react_1.default.createElement("th", null, "Actions"))), react_1.default.createElement("tbody", null, domains.map((domain) => { var _a, _b; return (react_1.default.createElement("tr", { key: domain.domain, className: domain.isDisabled ? 'disabled-row' : '' }, react_1.default.createElement("td", null, domain.domain), react_1.default.createElement("td", null, react_1.default.createElement("span", { className: `status ${((_a = statuses[domain.domain]) === null || _a === void 0 ? void 0 : _a.certificateValid) ? 'valid' : 'invalid'} ` }, ((_b = statuses[domain.domain]) === null || _b === void 0 ? void 0 : _b.certificateValid) ? 'Valid' : 'Invalid/Missing')), react_1.default.createElement("td", null, react_1.default.createElement("span", { className: `status ${domain.isDisabled ? 'warning' : 'valid'}` }, domain.isDisabled ? 'Disabled' : 'Enabled')), react_1.default.createElement("td", { className: "actions-cell" }, domain.isCreatedByUs ? (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement("button", { onClick: () => toggleDomainState(domain.domain, !domain.isDisabled), className: `button ${domain.isDisabled ? 'success' : 'warning'}`, disabled: loading, title: domain.isDisabled ? 'Enable domain' : 'Disable domain' }, domain.isDisabled ? 'Enable' : 'Disable'), react_1.default.createElement("button", { onClick: () => refreshCertificate(domain.domain), className: "button secondary", disabled: loading, title: "Refresh SSL certificate" }, "Refresh Cert"), react_1.default.createElement("button", { onClick: () => removeDomain(domain.domain), className: "button danger", disabled: loading, title: "Remove domain" }, "Remove"))) : (react_1.default.createElement("button", { onClick: () => adoptDomain(domain.domain, domain.ip), className: "button primary adopt-button", disabled: loading, title: "Start managing this domain with Navigrator" }, "Adopt"))))); }))))))), activeTab === 'certificates' && (react_1.default.createElement("div", { className: "certificates-list" }, react_1.default.createElement("h2", null, "SSL Certificates"), certificates.length === 0 ? (react_1.default.createElement("p", { className: "no-data" }, "No certificates generated yet")) : (react_1.default.createElement("table", null, react_1.default.createElement("thead", null, react_1.default.createElement("tr", null, react_1.default.createElement("th", null, "Domain"), react_1.default.createElement("th", null, "Status"), react_1.default.createElement("th", null, "Issuer"), react_1.default.createElement("th", null, "Valid Until"), react_1.default.createElement("th", null, "Actions"))), react_1.default.createElement("tbody", null, certificates.map((cert) => (react_1.default.createElement("tr", { key: cert.domain }, react_1.default.createElement("td", null, cert.domain), react_1.default.createElement("td", null, react_1.default.createElement("span", { className: `status ${cert.isValid ? 'valid' : 'invalid'} ` }, cert.isValid ? 'Valid' : 'Invalid')), react_1.default.createElement("td", null, cert.issuer), react_1.default.createElement("td", null, new Date(cert.validTo).toLocaleDateString()), react_1.default.createElement("td", { className: "actions-cell" }, react_1.default.createElement("button", { onClick: () => refreshCertificate(cert.domain), className: "button secondary", disabled: loading, title: "Refresh certificate" }, "Refresh"), react_1.default.createElement("button", { onClick: () => setConfirmDeleteCertificate(cert.domain), className: "button danger", disabled: loading, title: "Delete certificate" }, "Delete")))))))))), activeTab === 'proxies' && (react_1.default.createElement(react_1.default.Fragment, null, editProxy ? (react_1.default.createElement("form", { className: "add-domain-form", onSubmit: updateProxy }, react_1.default.createElement("h2", null, "Edit Proxy for ", editProxy.domain), react_1.default.createElement("div", { className: "form-row" }, react_1.default.createElement("input", { type: "text", placeholder: "Target URL (e.g. localhost:3000)", value: editProxy.target, onChange: (e) => setEditProxy(Object.assign(Object.assign({}, editProxy), { target: e.target.value })), disabled: loading }), react_1.default.createElement("input", { type: "number", placeholder: "Port (default: 443)", value: editProxy.port, onChange: (e) => setEditProxy(Object.assign(Object.assign({}, editProxy), { port: parseInt(e.target.value) || 443 })), disabled: loading, className: "port-input" }), react_1.default.createElement("button", { type: "submit", disabled: loading || !editProxy.target }, "Update Proxy"), react_1.default.createElement("button", { type: "button", className: "button secondary", onClick: () => setEditProxy(null), disabled: loading }, "Cancel")))) : (react_1.default.createElement("form", { className: "add-domain-form", onSubmit: addProxy }, react_1.default.createElement("h2", null, "Create HTTPS Proxy"), react_1.default.createElement("div", { className: "form-row form-row-multi" }, react_1.default.createElement("div", { className: "input-group" }, react_1.default.createElement("label", { htmlFor: "proxy-domain" }, "Domain"), react_1.default.createElement("select", { id: "proxy-domain", value: newProxy.domain, onChange: (e) => setNewProxy(Object.assign(Object.assign({}, newProxy), { domain: e.target.value })), disabled: loading, required: true }, react_1.default.createElement("option", { value: "" }, "Select a domain"), domains .filter(domain => { var _a; return !domain.isDisabled && ((_a = statuses[domain.domain]) === null || _a === void 0 ? void 0 : _a.certificateValid) && !proxies.find(p => p.domain === domain.domain); }) .map(domain => (react_1.default.createElement("option", { key: domain.domain, value: domain.domain }, domain.domain))))), react_1.default.createElement("div", { className: "input-group" }, react_1.default.createElement("label", { htmlFor: "proxy-target" }, "Target URL"), react_1.default.createElement("input", { id: "proxy-target", type: "text", placeholder: "localhost:3000", value: newProxy.target, onChange: (e) => setNewProxy(Object.assign(Object.assign({}, newProxy), { target: e.target.value })), disabled: loading, required: true })), react_1.default.createElement("div", { className: "input-group" }, react_1.default.createElement("label", { htmlFor: "proxy-port" }, "HTTPS Port"), react_1.default.createElement("input", { id: "proxy-port", type: "number", placeholder: "443", value: newProxy.port, onChange: (e) => setNewProxy(Object.assign(Object.assign({}, newProxy), { port: parseInt(e.target.value) || 443 })), disabled: loading, min: "1", max: "65535", className: "port-input" })), react_1.default.createElement("button", { type: "submit", disabled: loading || !newProxy.domain || !newProxy.target, className: "button primary create-proxy-button" }, "Create Proxy")), react_1.default.createElement("p", { className: "help-text" }, "Create a secure HTTPS proxy to your local development server. Traffic to your domain will be forwarded to the target URL."))), react_1.default.createElement("div", { className: "proxies-list" }, react_1.default.createElement("h2", null, "Your Proxies"), proxies.length === 0 ? (react_1.default.createElement("p", { className: "no-data" }, "No proxies configured yet")) : (react_1.default.createElement("table", null, react_1.default.createElement("thead", null, react_1.default.createElement("tr", null, react_1.default.createElement("th", null, "Domain"), react_1.default.createElement("th", null, "Target"), react_1.default.createElement("th", null, "Port"), react_1.default.createElement("th", null, "Status"), react_1.default.createElement("th", null, "Actions"))), react_1.default.createElement("tbody", null, proxies.map((proxy) => (react_1.default.createElement("tr", { key: proxy.domain }, react_1.default.createElement("td", null, proxy.domain), react_1.default.createElement("td", null, proxy.target), react_1.default.createElement("td", null, proxy.port), react_1.default.createElement("td", null, react_1.default.createElement("span", { className: `status ${proxy.isRunning ? 'valid' : 'warning'}` }, proxy.isRunning ? 'Running' : 'Stopped')), react_1.default.createElement("td", { className: "actions-cell" }, proxy.isRunning ? (react_1.default.createElement("button", { onClick: () => toggleProxy(proxy.domain, false), className: "button warning", disabled: loading, title: "Stop proxy" }, "Stop")) : (react_1.default.createElement("button", { onClick: () => toggleProxy(proxy.domain, true), className: "button success", disabled: loading, title: "Start proxy" }, "Start")), react_1.default.createElement("button", { onClick: () => setEditProxy(proxy), className: "button secondary", disabled: loading || proxy.isRunning, title: proxy.isRunning ? "Stop proxy before editing" : "Edit proxy configuration" }, "Edit"), react_1.default.createElement("button", { onClick: () => setConfirmDeleteProxy(proxy.domain), className: "button danger", disabled: loading, title: "Delete proxy" }, "Delete")))))))))))))); } exports.default = App;