feat: add name field to ProxyHost and implement sorting functionality in ProxyHosts and CertificateList components
This commit is contained in:
@@ -19,6 +19,7 @@ export interface Certificate {
|
||||
|
||||
export interface ProxyHost {
|
||||
uuid: string;
|
||||
name: string;
|
||||
domain_names: string;
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user