diff --git a/frontend/src/api/proxyHosts.ts b/frontend/src/api/proxyHosts.ts index a70a41dd..4ab9ebea 100644 --- a/frontend/src/api/proxyHosts.ts +++ b/frontend/src/api/proxyHosts.ts @@ -19,6 +19,7 @@ export interface Certificate { export interface ProxyHost { uuid: string; + name: string; domain_names: string; forward_scheme: string; forward_host: string; diff --git a/frontend/src/components/CertificateList.tsx b/frontend/src/components/CertificateList.tsx index 8fe20c0e..a3cc2eba 100644 --- a/frontend/src/components/CertificateList.tsx +++ b/frontend/src/components/CertificateList.tsx @@ -1,13 +1,19 @@ +import { useState, useMemo } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Trash2 } from 'lucide-react' +import { Trash2, ChevronUp, ChevronDown } from 'lucide-react' import { useCertificates } from '../hooks/useCertificates' import { deleteCertificate } from '../api/certificates' import { LoadingSpinner } from './LoadingStates' import { toast } from '../utils/toast' +type SortColumn = 'name' | 'expires' +type SortDirection = 'asc' | 'desc' + export default function CertificateList() { const { certificates, isLoading, error } = useCertificates() const queryClient = useQueryClient() + const [sortColumn, setSortColumn] = useState('name') + const [sortDirection, setSortDirection] = useState('asc') const deleteMutation = useMutation({ mutationFn: deleteCertificate, @@ -20,6 +26,41 @@ export default function CertificateList() { }, }) + const sortedCertificates = useMemo(() => { + return [...certificates].sort((a, b) => { + let comparison = 0 + + switch (sortColumn) { + case 'name': + const aName = (a.name || a.domain || '').toLowerCase() + const bName = (b.name || b.domain || '').toLowerCase() + comparison = aName.localeCompare(bName) + break + case 'expires': + const aDate = new Date(a.expires_at).getTime() + const bDate = new Date(b.expires_at).getTime() + comparison = aDate - bDate + break + } + + return sortDirection === 'asc' ? comparison : -comparison + }) + }, [certificates, sortColumn, sortDirection]) + + const handleSort = (column: SortColumn) => { + if (sortColumn === column) { + setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc') + } else { + setSortColumn(column) + setSortDirection('asc') + } + } + + const SortIcon = ({ column }: { column: SortColumn }) => { + if (sortColumn !== column) return null + return sortDirection === 'asc' ? : + } + if (isLoading) return if (error) return
Failed to load certificates
@@ -29,10 +70,26 @@ export default function CertificateList() { - + - + @@ -45,7 +102,7 @@ export default function CertificateList() { ) : ( - certificates.map((cert) => ( + sortedCertificates.map((cert) => ( diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 03a1da53..4770b0d1 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -16,6 +16,7 @@ interface ProxyHostFormProps { export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFormProps) { const [formData, setFormData] = useState({ + name: host?.name || '', domain_names: host?.domain_names || '', forward_scheme: host?.forward_scheme || 'http', forward_host: host?.forward_host || '', @@ -211,7 +212,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor } return ( -
+

@@ -226,6 +227,24 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor

)} + {/* Name Field */} +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="My Service" + className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +

+ A friendly name to identify this proxy host (optional) +

+
+
{/* Docker Container Quick Select */}
@@ -528,7 +547,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor {/* New Domain Prompt Modal */} {showDomainPrompt && ( -
+
diff --git a/frontend/src/components/RemoteServerForm.tsx b/frontend/src/components/RemoteServerForm.tsx index 18969fae..d23f9229 100644 --- a/frontend/src/components/RemoteServerForm.tsx +++ b/frontend/src/components/RemoteServerForm.tsx @@ -66,7 +66,7 @@ export default function RemoteServerForm({ server, onSubmit, onCancel }: Props) } return ( -
+

diff --git a/frontend/src/pages/ProxyHosts.tsx b/frontend/src/pages/ProxyHosts.tsx index 4cd93c06..6770a5aa 100644 --- a/frontend/src/pages/ProxyHosts.tsx +++ b/frontend/src/pages/ProxyHosts.tsx @@ -1,5 +1,5 @@ import { useState, useMemo } from 'react' -import { Loader2, ExternalLink, AlertTriangle } from 'lucide-react' +import { Loader2, ExternalLink, AlertTriangle, ChevronUp, ChevronDown } from 'lucide-react' import { useQuery } from '@tanstack/react-query' import { useProxyHosts } from '../hooks/useProxyHosts' import { useCertificates } from '../hooks/useCertificates' @@ -8,11 +8,16 @@ import type { ProxyHost } from '../api/proxyHosts' import ProxyHostForm from '../components/ProxyHostForm' import { Switch } from '../components/ui/Switch' +type SortColumn = 'name' | 'domain' | 'forward' +type SortDirection = 'asc' | 'desc' + export default function ProxyHosts() { const { hosts, loading, isFetching, error, createHost, updateHost, deleteHost } = useProxyHosts() const { certificates } = useCertificates() const [showForm, setShowForm] = useState(false) const [editingHost, setEditingHost] = useState() + const [sortColumn, setSortColumn] = useState('name') + const [sortDirection, setSortDirection] = useState('asc') const { data: settings } = useQuery({ queryKey: ['settings'], @@ -38,6 +43,49 @@ export default function ProxyHosts() { return map }, [certificates]) + // Sort hosts based on current sort column and direction + const sortedHosts = useMemo(() => { + return [...hosts].sort((a, b) => { + let aVal: string + let bVal: string + + switch (sortColumn) { + case 'name': + aVal = (a.name || a.domain_names.split(',')[0] || '').toLowerCase() + bVal = (b.name || b.domain_names.split(',')[0] || '').toLowerCase() + break + case 'domain': + aVal = (a.domain_names.split(',')[0] || '').toLowerCase() + bVal = (b.domain_names.split(',')[0] || '').toLowerCase() + break + case 'forward': + aVal = `${a.forward_host}:${a.forward_port}`.toLowerCase() + bVal = `${b.forward_host}:${b.forward_port}`.toLowerCase() + break + default: + return 0 + } + + if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1 + if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1 + return 0 + }) + }, [hosts, sortColumn, sortDirection]) + + const handleSort = (column: SortColumn) => { + if (sortColumn === column) { + setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc') + } else { + setSortColumn(column) + setSortDirection('asc') + } + } + + const SortIcon = ({ column }: { column: SortColumn }) => { + if (sortColumn !== column) return null + return sortDirection === 'asc' ? : + } + const handleDomainClick = (e: React.MouseEvent, url: string) => { if (linkBehavior === 'new_window') { e.preventDefault() @@ -108,11 +156,32 @@ export default function ProxyHosts() {

Name handleSort('name')} + className="px-6 py-3 cursor-pointer hover:text-white transition-colors" + > +
+ Name + +
+
Domain IssuerExpires handleSort('expires')} + className="px-6 py-3 cursor-pointer hover:text-white transition-colors" + > +
+ Expires + +
+
Status Actions
{cert.name || '-'} {cert.domain}
- - + - {hosts.map((host) => ( + {sortedHosts.map((host) => ( +
- Domain + handleSort('name')} + className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-200 transition-colors" + > +
+ Name + +
- Forward To + handleSort('domain')} + className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-200 transition-colors" + > +
+ Domain + +
+
handleSort('forward')} + className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-200 transition-colors" + > +
+ Forward To + +
SSL @@ -126,8 +195,13 @@ export default function ProxyHosts() {
+
+ {host.name || Unnamed} +
+
{host.domain_names.split(',').map((domain, i) => {