added last fixes for dev branch prepartion
This commit is contained in:
@@ -1,11 +1,78 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
// Custom Dropdown Component
|
||||
const CustomDropdown = ({ label, value, onChange, options, placeholder = "Auswählen" }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const selectedOption = options.find(opt => opt.value === value) || { label: placeholder }
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
{label}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full px-4 py-3 bg-slate-700/50 hover:bg-slate-700/70 active:bg-slate-700 text-white rounded-lg border border-slate-600/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all duration-200 text-sm flex items-center justify-between"
|
||||
>
|
||||
<span className={!value ? 'text-slate-400' : 'text-white'}>{selectedOption.label}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 w-full mt-2 bg-slate-800/95 backdrop-blur-sm border border-slate-600/50 rounded-lg shadow-2xl overflow-hidden">
|
||||
<div className="max-h-60 overflow-y-auto custom-scrollbar">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(option.value)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={`w-full px-4 py-2.5 text-left text-sm transition-all duration-150 ${
|
||||
value === option.value
|
||||
? 'bg-blue-600/20 text-blue-300 border-l-2 border-blue-500 font-medium'
|
||||
: 'text-slate-300 hover:bg-slate-700/50 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AuditLogs = () => {
|
||||
const { authFetch } = useAuth()
|
||||
const [logs, setLogs] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(null)
|
||||
const [filters, setFilters] = useState({
|
||||
action: '',
|
||||
resourceType: '',
|
||||
@@ -23,6 +90,7 @@ const AuditLogs = () => {
|
||||
try {
|
||||
if (!silent) {
|
||||
setLoading(true)
|
||||
setIsRefreshing(true)
|
||||
}
|
||||
setError('')
|
||||
|
||||
@@ -41,9 +109,8 @@ const AuditLogs = () => {
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Audit-Logs Response:', data)
|
||||
console.log('Anzahl Logs:', data.logs?.length || 0)
|
||||
setLogs(data.logs || [])
|
||||
setLastUpdate(new Date())
|
||||
setPagination({
|
||||
...pagination,
|
||||
total: data.total || 0,
|
||||
@@ -57,6 +124,7 @@ const AuditLogs = () => {
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setLoading(false)
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +140,7 @@ const AuditLogs = () => {
|
||||
}, 5000) // Aktualisiere alle 5 Sekunden
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [filters.action, filters.resourceType, filters.userId, pagination.offset])
|
||||
}, [filters.action, filters.resourceType, filters.userId, pagination.offset, authFetch])
|
||||
|
||||
const handleFilterChange = (key, value) => {
|
||||
setFilters({ ...filters, [key]: value })
|
||||
@@ -188,6 +256,18 @@ const AuditLogs = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const formatLastUpdate = () => {
|
||||
if (!lastUpdate) return ''
|
||||
const now = new Date()
|
||||
const diff = Math.floor((now - lastUpdate) / 1000)
|
||||
if (diff < 5) return 'Gerade eben'
|
||||
if (diff < 60) return `Vor ${diff} Sekunden`
|
||||
const minutes = Math.floor(diff / 60)
|
||||
return `Vor ${minutes} Minute${minutes > 1 ? 'n' : ''}`
|
||||
}
|
||||
|
||||
const hasActiveFilters = filters.action || filters.resourceType || filters.userId
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-r from-slate-700 to-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
@@ -196,55 +276,80 @@ const AuditLogs = () => {
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Audit Log</h1>
|
||||
<p className="text-slate-300">Übersicht aller Systemaktivitäten und Änderungen</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-400">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>Live-Aktualisierung aktiv</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{isRefreshing && (
|
||||
<div className="flex items-center gap-2 text-blue-400">
|
||||
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span className="text-sm">Aktualisiere...</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-500/20 border border-green-500/50 rounded-full">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-sm text-green-300 font-medium">Live</span>
|
||||
</div>
|
||||
{lastUpdate && (
|
||||
<div className="text-sm text-slate-400">
|
||||
{formatLastUpdate()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="bg-slate-800/50 border border-slate-600/50 rounded-lg p-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-200 mb-2">
|
||||
Aktion
|
||||
</label>
|
||||
<select
|
||||
value={filters.action}
|
||||
onChange={(e) => handleFilterChange('action', e.target.value)}
|
||||
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-white">Filter</h2>
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setFilters({ action: '', resourceType: '', userId: '' })
|
||||
setPagination({ ...pagination, offset: 0 })
|
||||
}}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-slate-700/50 hover:bg-slate-700 text-slate-300 rounded-lg transition-colors duration-200 text-sm"
|
||||
>
|
||||
<option value="">Alle Aktionen</option>
|
||||
<option value="CREATE">Erstellt</option>
|
||||
<option value="UPDATE">Aktualisiert</option>
|
||||
<option value="DELETE">Gelöscht</option>
|
||||
<option value="UPLOAD">Hochgeladen</option>
|
||||
<option value="SIGN">Signiert</option>
|
||||
<option value="ENABLE">Aktiviert</option>
|
||||
<option value="DISABLE">Deaktiviert</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-200 mb-2">
|
||||
Ressourcentyp
|
||||
</label>
|
||||
<select
|
||||
value={filters.resourceType}
|
||||
onChange={(e) => handleFilterChange('resourceType', e.target.value)}
|
||||
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="user">Benutzer</option>
|
||||
<option value="space">Space</option>
|
||||
<option value="fqdn">FQDN</option>
|
||||
<option value="csr">CSR</option>
|
||||
<option value="provider">Provider</option>
|
||||
<option value="certificate">Zertifikat</option>
|
||||
<option value="permission_group">Berechtigungsgruppen</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-200 mb-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Zurücksetzen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<CustomDropdown
|
||||
label="Aktion"
|
||||
value={filters.action}
|
||||
onChange={(value) => handleFilterChange('action', value)}
|
||||
options={[
|
||||
{ value: '', label: 'Alle Aktionen' },
|
||||
{ value: 'CREATE', label: 'Erstellt' },
|
||||
{ value: 'UPDATE', label: 'Aktualisiert' },
|
||||
{ value: 'DELETE', label: 'Gelöscht' },
|
||||
{ value: 'UPLOAD', label: 'Hochgeladen' },
|
||||
{ value: 'SIGN', label: 'Signiert' },
|
||||
{ value: 'ENABLE', label: 'Aktiviert' },
|
||||
{ value: 'DISABLE', label: 'Deaktiviert' }
|
||||
]}
|
||||
/>
|
||||
<CustomDropdown
|
||||
label="Ressourcentyp"
|
||||
value={filters.resourceType}
|
||||
onChange={(value) => handleFilterChange('resourceType', value)}
|
||||
options={[
|
||||
{ value: '', label: 'Alle Typen' },
|
||||
{ value: 'user', label: 'Benutzer' },
|
||||
{ value: 'space', label: 'Space' },
|
||||
{ value: 'fqdn', label: 'FQDN' },
|
||||
{ value: 'csr', label: 'CSR' },
|
||||
{ value: 'provider', label: 'Provider' },
|
||||
{ value: 'certificate', label: 'Zertifikat' },
|
||||
{ value: 'permission_group', label: 'Berechtigungsgruppen' }
|
||||
]}
|
||||
/>
|
||||
<div className="relative">
|
||||
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
Benutzer-ID
|
||||
</label>
|
||||
<input
|
||||
@@ -252,7 +357,7 @@ const AuditLogs = () => {
|
||||
value={filters.userId}
|
||||
onChange={(e) => handleFilterChange('userId', e.target.value)}
|
||||
placeholder="Benutzer-ID filtern"
|
||||
className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-4 py-3 bg-slate-700/50 hover:bg-slate-700/70 text-white rounded-lg border border-slate-600/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all duration-200 text-sm placeholder-slate-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,166 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
// Custom Dropdown Component
|
||||
const CustomDropdown = ({ label, value, onChange, options, placeholder = "Auswählen" }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const selectedOption = options.find(opt => opt.value === value) || { label: placeholder }
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">
|
||||
{label}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full px-4 py-3 bg-slate-700/50 hover:bg-slate-700/70 active:bg-slate-700 text-white rounded-lg border border-slate-600/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all duration-200 text-sm flex items-center justify-between"
|
||||
>
|
||||
<span className={value === 'all' ? 'text-slate-400' : 'text-white'}>{selectedOption.label}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 w-full mt-2 bg-slate-800/95 backdrop-blur-sm border border-slate-600/50 rounded-lg shadow-2xl overflow-hidden">
|
||||
<div className="max-h-60 overflow-y-auto custom-scrollbar">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(option.value)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={`w-full px-4 py-2.5 text-left text-sm transition-all duration-150 ${
|
||||
value === option.value
|
||||
? 'bg-blue-600/20 text-blue-300 border-l-2 border-blue-500 font-medium'
|
||||
: 'text-slate-300 hover:bg-slate-700/50 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RenewalQueue = () => {
|
||||
const { authFetch } = useAuth()
|
||||
const [queue, setQueue] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [statusFilter, setStatusFilter] = useState('all')
|
||||
const [spaceFilter, setSpaceFilter] = useState('all')
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(null)
|
||||
|
||||
const fetchQueue = async () => {
|
||||
const fetchQueue = async (silent = false) => {
|
||||
try {
|
||||
if (!silent) {
|
||||
setIsRefreshing(true)
|
||||
}
|
||||
const response = await authFetch('/api/renewal-queue')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
setQueue(data.queue || [])
|
||||
setLastUpdate(new Date())
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
console.error('Error fetching renewal queue:', err)
|
||||
setLoading(false)
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchQueue()
|
||||
const interval = setInterval(fetchQueue, 30000) // Refresh every 30 seconds
|
||||
// Live-Refresh alle 5 Sekunden für Echtzeit-Ansicht
|
||||
const interval = setInterval(() => fetchQueue(true), 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [authFetch])
|
||||
|
||||
// Extrahiere eindeutige Spaces für Filter
|
||||
const uniqueSpaces = useMemo(() => {
|
||||
const spaces = new Set()
|
||||
queue.forEach(item => {
|
||||
if (item.spaceName) {
|
||||
spaces.add(item.spaceName)
|
||||
}
|
||||
})
|
||||
return Array.from(spaces).sort()
|
||||
}, [queue])
|
||||
|
||||
// Filtere und sortiere Queue-Einträge
|
||||
const filteredAndSortedQueue = useMemo(() => {
|
||||
let filtered = queue
|
||||
|
||||
// Filter nach Status
|
||||
if (statusFilter !== 'all') {
|
||||
filtered = filtered.filter(item => item.status === statusFilter)
|
||||
}
|
||||
|
||||
// Filter nach Space
|
||||
if (spaceFilter !== 'all') {
|
||||
filtered = filtered.filter(item => item.spaceName === spaceFilter)
|
||||
}
|
||||
|
||||
// Sortiere nach scheduled_at (nächste oben)
|
||||
filtered.sort((a, b) => {
|
||||
const dateA = new Date(a.scheduledAt)
|
||||
const dateB = new Date(b.scheduledAt)
|
||||
return dateA - dateB
|
||||
})
|
||||
|
||||
return filtered
|
||||
}, [queue, statusFilter, spaceFilter])
|
||||
|
||||
// Teile in "Ausstehend" und "Erledigt"
|
||||
const pendingQueue = useMemo(() => {
|
||||
return filteredAndSortedQueue.filter(item =>
|
||||
item.status === 'pending' || item.status === 'processing'
|
||||
)
|
||||
}, [filteredAndSortedQueue])
|
||||
|
||||
const completedQueue = useMemo(() => {
|
||||
return filteredAndSortedQueue.filter(item =>
|
||||
item.status === 'completed' || item.status === 'failed'
|
||||
)
|
||||
}, [filteredAndSortedQueue])
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50'
|
||||
case 'processing':
|
||||
return 'bg-blue-500/20 text-blue-400 border-blue-500/50'
|
||||
case 'success':
|
||||
return 'bg-green-500/20 text-green-400 border-green-500/50'
|
||||
case 'completed':
|
||||
return 'bg-green-500/20 text-green-400 border-green-500/50'
|
||||
case 'failed':
|
||||
@@ -86,13 +214,92 @@ const RenewalQueue = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderQueueTable = (items, title) => {
|
||||
if (items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-white mb-4">{title}</h2>
|
||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-700/50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">FQDN</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Space</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Geplant für</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Verarbeitet</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Fehler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-700/50">
|
||||
{items.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-slate-700/30 transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-white font-medium">{item.fqdn || '-'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{item.spaceName || '-'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.scheduledAt)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-semibold border ${getStatusColor(item.status)}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.processedAt)}</td>
|
||||
<td className="px-6 py-4 text-sm text-red-400">{item.errorMessage || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const formatLastUpdate = () => {
|
||||
if (!lastUpdate) return ''
|
||||
const now = new Date()
|
||||
const diff = Math.floor((now - lastUpdate) / 1000)
|
||||
if (diff < 5) return 'Gerade eben'
|
||||
if (diff < 60) return `Vor ${diff} Sekunden`
|
||||
const minutes = Math.floor(diff / 60)
|
||||
return `Vor ${minutes} Minute${minutes > 1 ? 'n' : ''}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 min-h-full bg-gradient-to-r from-slate-700 to-slate-900">
|
||||
<div className="max-w-10xl mx-auto">
|
||||
<h1 className="text-4xl font-bold text-white mb-4">Renewal Queue</h1>
|
||||
<p className="text-lg text-slate-200 mb-8">
|
||||
Übersicht über geplante und laufende Zertifikatserneuerungen
|
||||
</p>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold text-white mb-2">Renewal Queue</h1>
|
||||
<p className="text-lg text-slate-200">
|
||||
Übersicht über geplante und laufende Zertifikatserneuerungen
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isRefreshing && (
|
||||
<div className="flex items-center gap-2 text-blue-400">
|
||||
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span className="text-sm">Aktualisiere...</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-500/20 border border-green-500/50 rounded-full">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-sm text-green-300 font-medium">Live</span>
|
||||
</div>
|
||||
{lastUpdate && (
|
||||
<div className="text-sm text-slate-400">
|
||||
{formatLastUpdate()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
@@ -100,44 +307,68 @@ const RenewalQueue = () => {
|
||||
<p className="text-slate-300 mt-4">Lade Queue...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 overflow-hidden">
|
||||
{queue.length === 0 ? (
|
||||
<div className="p-12 text-center">
|
||||
<p className="text-slate-400 text-lg">Keine Einträge in der Renewal Queue</p>
|
||||
<>
|
||||
{/* Filter */}
|
||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-white">Filter</h2>
|
||||
{(statusFilter !== 'all' || spaceFilter !== 'all') && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setStatusFilter('all')
|
||||
setSpaceFilter('all')
|
||||
}}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-slate-700/50 hover:bg-slate-700 text-slate-300 rounded-lg transition-colors duration-200 text-sm"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Zurücksetzen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-700/50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">FQDN</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Space</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Geplant für</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Verarbeitet</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-slate-300 uppercase tracking-wider">Fehler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-700/50">
|
||||
{queue.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-slate-700/30 transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-white font-medium">{item.fqdn || '-'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{item.spaceName || '-'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.scheduledAt)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-semibold border ${getStatusColor(item.status)}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-300">{formatDate(item.processedAt)}</td>
|
||||
<td className="px-6 py-4 text-sm text-red-400">{item.errorMessage || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<CustomDropdown
|
||||
label="Status"
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
options={[
|
||||
{ value: 'all', label: 'Alle Status' },
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'processing', label: 'Processing' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
{ value: 'failed', label: 'Failed' }
|
||||
]}
|
||||
/>
|
||||
<CustomDropdown
|
||||
label="Space"
|
||||
value={spaceFilter}
|
||||
onChange={setSpaceFilter}
|
||||
options={[
|
||||
{ value: 'all', label: 'Alle Spaces' },
|
||||
...uniqueSpaces.map(space => ({ value: space, label: space }))
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ausstehende Tasks */}
|
||||
{renderQueueTable(pendingQueue, `Ausstehend (${pendingQueue.length})`)}
|
||||
|
||||
{/* Erledigte Tasks */}
|
||||
{renderQueueTable(completedQueue, `Erledigt (${completedQueue.length})`)}
|
||||
|
||||
{/* Keine Einträge */}
|
||||
{filteredAndSortedQueue.length === 0 && (
|
||||
<div className="bg-slate-800/80 backdrop-blur-sm rounded-lg shadow-xl border border-slate-600/50 p-12 text-center">
|
||||
<p className="text-slate-400 text-lg">
|
||||
{queue.length === 0
|
||||
? 'Keine Einträge in der Renewal Queue'
|
||||
: 'Keine Einträge entsprechen den gewählten Filtern'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user