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

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>

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} />

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">