Initial Commit. Split cloud service from the combined project
This commit is contained in:
29
sysadmin_panel/src/components/ConfirmModal.jsx
Normal file
29
sysadmin_panel/src/components/ConfirmModal.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
export default function ConfirmModal({ title, message, confirmLabel = 'Confirm', danger = false, onConfirm, onCancel, children }) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-xl p-6 w-full max-w-md shadow-2xl">
|
||||
<h2 className="text-white font-semibold text-lg mb-2">{title}</h2>
|
||||
{message && <p className="text-gray-400 text-sm mb-4">{message}</p>}
|
||||
{children}
|
||||
<div className="flex gap-3 justify-end mt-5">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm text-gray-300 hover:text-white bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className={`px-4 py-2 text-sm font-semibold rounded-lg transition-colors ${
|
||||
danger
|
||||
? 'bg-red-600 hover:bg-red-500 text-white'
|
||||
: 'bg-cyan-600 hover:bg-cyan-500 text-white'
|
||||
}`}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
sysadmin_panel/src/components/LicenseStatus.jsx
Normal file
17
sysadmin_panel/src/components/LicenseStatus.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
export default function LicenseStatus({ site }) {
|
||||
const now = new Date()
|
||||
const expires = new Date(site.license_expires_at)
|
||||
const lastSeen = site.last_seen_at ? new Date(site.last_seen_at) : null
|
||||
const hoursAgo = lastSeen ? (now - lastSeen) / 1000 / 3600 : null
|
||||
|
||||
if (site.is_locked) {
|
||||
return <span className="inline-flex items-center gap-1.5 text-xs font-medium text-red-400"><span className="w-2 h-2 rounded-full bg-red-500" />Locked</span>
|
||||
}
|
||||
if (expires < now) {
|
||||
return <span className="inline-flex items-center gap-1.5 text-xs font-medium text-red-400"><span className="w-2 h-2 rounded-full bg-red-500" />Expired</span>
|
||||
}
|
||||
if (hoursAgo === null || hoursAgo > 12) {
|
||||
return <span className="inline-flex items-center gap-1.5 text-xs font-medium text-yellow-400"><span className="w-2 h-2 rounded-full bg-yellow-400" />No Heartbeat</span>
|
||||
}
|
||||
return <span className="inline-flex items-center gap-1.5 text-xs font-medium text-emerald-400"><span className="w-2 h-2 rounded-full bg-emerald-400" />Active</span>
|
||||
}
|
||||
64
sysadmin_panel/src/components/SiteCard.jsx
Normal file
64
sysadmin_panel/src/components/SiteCard.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import LicenseStatus from './LicenseStatus'
|
||||
|
||||
function fmtDate(dt) {
|
||||
if (!dt) return '—'
|
||||
return new Date(dt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
function fmtAgo(dt) {
|
||||
if (!dt) return 'Never'
|
||||
const diff = (Date.now() - new Date(dt)) / 1000
|
||||
if (diff < 60) return 'Just now'
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
|
||||
return `${Math.floor(diff / 86400)}d ago`
|
||||
}
|
||||
|
||||
export default function SiteCard({ site }) {
|
||||
const navigate = useNavigate()
|
||||
const expires = new Date(site.license_expires_at)
|
||||
const isExpired = expires < new Date()
|
||||
const isLocked = site.is_locked
|
||||
|
||||
const borderColor = isLocked || isExpired
|
||||
? 'border-red-800/60 hover:border-red-700'
|
||||
: site.last_seen_at && (Date.now() - new Date(site.last_seen_at)) / 3600000 < 12
|
||||
? 'border-emerald-800/40 hover:border-emerald-600'
|
||||
: 'border-yellow-800/40 hover:border-yellow-600'
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => navigate(`/sites/${site.site_id}`)}
|
||||
className={`bg-gray-900 border ${borderColor} rounded-xl p-4 cursor-pointer transition-all hover:bg-gray-800`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-white font-semibold text-sm truncate">{site.name}</h3>
|
||||
<p className="text-gray-500 text-xs truncate">{site.owner_name}</p>
|
||||
</div>
|
||||
<LicenseStatus site={site} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
<div>
|
||||
<span className="text-gray-600">Expires</span>
|
||||
<p className={`font-medium ${isExpired ? 'text-red-400' : 'text-gray-300'}`}>{fmtDate(site.license_expires_at)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Last seen</span>
|
||||
<p className="text-gray-300 font-medium">{fmtAgo(site.last_seen_at)}</p>
|
||||
</div>
|
||||
<div className="col-span-2 mt-1">
|
||||
<span className="text-gray-600 font-mono text-[10px]">{site.site_id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLocked && site.lock_reason && (
|
||||
<p className="mt-2 text-xs text-red-400 bg-red-900/20 border border-red-800/30 rounded px-2 py-1 truncate">
|
||||
Locked: {site.lock_reason}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user