feat: add name field to ProxyHost and implement sorting functionality in ProxyHosts and CertificateList components

This commit is contained in:
Wikid82
2025-11-25 02:50:32 +00:00
parent ea034ba102
commit cc6bc7d6d6
5 changed files with 164 additions and 13 deletions
+1
View File
@@ -19,6 +19,7 @@ export interface Certificate {
export interface ProxyHost {
uuid: string;
name: string;
domain_names: string;
forward_scheme: string;
forward_host: string;
+61 -4
View File
@@ -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<SortColumn>('name')
const [sortDirection, setSortDirection] = useState<SortDirection>('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' ? <ChevronUp size={14} /> : <ChevronDown size={14} />
}
if (isLoading) return <LoadingSpinner />
if (error) return <div className="text-red-500">Failed to load certificates</div>
@@ -29,10 +70,26 @@ export default function CertificateList() {
<table className="w-full text-left text-sm text-gray-400">
<thead className="bg-gray-900 text-gray-200 uppercase font-medium">
<tr>
<th className="px-6 py-3">Name</th>
<th
onClick={() => handleSort('name')}
className="px-6 py-3 cursor-pointer hover:text-white transition-colors"
>
<div className="flex items-center gap-1">
Name
<SortIcon column="name" />
</div>
</th>
<th className="px-6 py-3">Domain</th>
<th className="px-6 py-3">Issuer</th>
<th className="px-6 py-3">Expires</th>
<th
onClick={() => handleSort('expires')}
className="px-6 py-3 cursor-pointer hover:text-white transition-colors"
>
<div className="flex items-center gap-1">
Expires
<SortIcon column="expires" />
</div>
</th>
<th className="px-6 py-3">Status</th>
<th className="px-6 py-3">Actions</th>
</tr>
@@ -45,7 +102,7 @@ export default function CertificateList() {
</td>
</tr>
) : (
certificates.map((cert) => (
sortedCertificates.map((cert) => (
<tr key={cert.id || cert.domain} className="hover:bg-gray-800/50 transition-colors">
<td className="px-6 py-4 font-medium text-white">{cert.name || '-'}</td>
<td className="px-6 py-4 font-medium text-white">{cert.domain}</td>
+21 -2
View File
@@ -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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-800">
<h2 className="text-2xl font-bold text-white">
@@ -226,6 +227,24 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
</div>
)}
{/* Name Field */}
<div>
<label htmlFor="proxy-name" className="block text-sm font-medium text-gray-300 mb-2">
Name
</label>
<input
id="proxy-name"
type="text"
value={formData.name}
onChange={e => 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"
/>
<p className="text-xs text-gray-500 mt-1">
A friendly name to identify this proxy host (optional)
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Docker Container Quick Select */}
<div>
@@ -528,7 +547,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
{/* New Domain Prompt Modal */}
{showDomainPrompt && (
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-[60]">
<div className="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-[60]">
<div className="bg-gray-800 rounded-lg border border-gray-700 max-w-md w-full p-6 shadow-xl">
<div className="flex items-center gap-3 mb-4 text-blue-400">
<AlertCircle size={24} />
+1 -1
View File
@@ -66,7 +66,7 @@ export default function RemoteServerForm({ server, onSubmit, onCancel }: Props)
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-lg w-full">
<div className="p-6 border-b border-gray-800">
<h2 className="text-2xl font-bold text-white">
+80 -6
View File
@@ -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<ProxyHost | undefined>()
const [sortColumn, setSortColumn] = useState<SortColumn>('name')
const [sortDirection, setSortDirection] = useState<SortDirection>('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' ? <ChevronUp size={14} /> : <ChevronDown size={14} />
}
const handleDomainClick = (e: React.MouseEvent, url: string) => {
if (linkBehavior === 'new_window') {
e.preventDefault()
@@ -108,11 +156,32 @@ export default function ProxyHosts() {
<table className="w-full">
<thead className="bg-gray-900 border-b border-gray-800">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Domain
<th
onClick={() => 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"
>
<div className="flex items-center gap-1">
Name
<SortIcon column="name" />
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Forward To
<th
onClick={() => 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"
>
<div className="flex items-center gap-1">
Domain
<SortIcon column="domain" />
</div>
</th>
<th
onClick={() => 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"
>
<div className="flex items-center gap-1">
Forward To
<SortIcon column="forward" />
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
SSL
@@ -126,8 +195,13 @@ export default function ProxyHosts() {
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{hosts.map((host) => (
{sortedHosts.map((host) => (
<tr key={host.uuid} className="hover:bg-gray-900/50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-white">
{host.name || <span className="text-gray-500 italic">Unnamed</span>}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-white">
{host.domain_names.split(',').map((domain, i) => {