Add admin page overhaul and compliance schema drift check specs, compliance upload improvements, drift checker helper
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader } from 'lucide-react';
|
||||
import { Upload, MessageSquare, RefreshCw, AlertCircle, Loader, RotateCcw } from 'lucide-react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import ComplianceUploadModal from './ComplianceUploadModal';
|
||||
import ComplianceDetailPanel from './ComplianceDetailPanel';
|
||||
@@ -143,7 +143,7 @@ function SeenBadge({ count }) {
|
||||
// Main Page
|
||||
// ---------------------------------------------------------------------------
|
||||
export default function CompliancePage({ onNavigate }) {
|
||||
const { canWrite } = useAuth();
|
||||
const { canWrite, isAdmin } = useAuth();
|
||||
|
||||
const [activeTeam, setActiveTeam] = useState('STEAM');
|
||||
const [activeTab, setActiveTab] = useState('active');
|
||||
@@ -155,6 +155,9 @@ export default function CompliancePage({ onNavigate }) {
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedHost, setSelectedHost] = useState(null);
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [rollbackConfirm, setRollbackConfirm] = useState(false);
|
||||
const [rollbackLoading, setRollbackLoading] = useState(false);
|
||||
const [rollbackResult, setRollbackResult] = useState(null);
|
||||
|
||||
const fetchSummary = useCallback(async (team) => {
|
||||
try {
|
||||
@@ -198,6 +201,28 @@ export default function CompliancePage({ onNavigate }) {
|
||||
fetchDevices(activeTeam, activeTab);
|
||||
};
|
||||
|
||||
const handleRollback = async () => {
|
||||
if (!lastUpload) return;
|
||||
setRollbackLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/compliance/rollback/${lastUpload.id}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Rollback failed');
|
||||
setRollbackResult(data);
|
||||
setRollbackConfirm(false);
|
||||
refresh();
|
||||
// Auto-dismiss result after 4 seconds
|
||||
setTimeout(() => setRollbackResult(null), 4000);
|
||||
} catch (err) {
|
||||
setRollbackResult({ error: err.message });
|
||||
} finally {
|
||||
setRollbackLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// In-memory filters
|
||||
const filteredDevices = devices
|
||||
.filter(d => !metricFilter || d.failing_metrics.some(m => m.metric_id === metricFilter))
|
||||
@@ -221,9 +246,30 @@ export default function CompliancePage({ onNavigate }) {
|
||||
</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
{lastUpload ? (
|
||||
<span style={{ fontSize: '0.72rem', color: '#475569', fontFamily: 'monospace' }}>
|
||||
Last report: <span style={{ color: '#64748B' }}>{lastUpload.report_date || lastUpload.uploaded_at?.slice(0, 10)}</span>
|
||||
</span>
|
||||
<>
|
||||
<span style={{ fontSize: '0.72rem', color: '#475569', fontFamily: 'monospace' }}>
|
||||
Last report: <span style={{ color: '#64748B' }}>{lastUpload.report_date || lastUpload.uploaded_at?.slice(0, 10)}</span>
|
||||
</span>
|
||||
{isAdmin() && (
|
||||
<button
|
||||
onClick={() => setRollbackConfirm(true)}
|
||||
title="Rollback last upload"
|
||||
style={{
|
||||
background: 'none', border: '1px solid rgba(239,68,68,0.25)',
|
||||
borderRadius: '0.25rem', padding: '0.15rem 0.4rem',
|
||||
cursor: 'pointer', color: '#64748B',
|
||||
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
|
||||
fontSize: '0.62rem', fontFamily: 'monospace',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.color = '#EF4444'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.6)'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.color = '#64748B'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.25)'; }}
|
||||
>
|
||||
<RotateCcw style={{ width: '10px', height: '10px' }} />
|
||||
Rollback
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '0.72rem', color: '#334155', fontFamily: 'monospace' }}>No reports uploaded</span>
|
||||
)}
|
||||
@@ -439,6 +485,118 @@ export default function CompliancePage({ onNavigate }) {
|
||||
onUploadComplete={() => { setShowUpload(false); refresh(); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Rollback confirmation modal ──────────────────────────── */}
|
||||
{rollbackConfirm && lastUpload && (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, zIndex: 60,
|
||||
background: 'rgba(10, 14, 39, 0.95)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '1rem',
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: '1px solid rgba(239,68,68,0.3)',
|
||||
borderRadius: '0.75rem',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.7)',
|
||||
width: '100%', maxWidth: '420px',
|
||||
padding: '2rem',
|
||||
}}>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '700', color: '#EF4444', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '1rem' }}>
|
||||
Rollback Upload
|
||||
</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#CBD5E1', lineHeight: '1.5', marginBottom: '0.5rem' }}>
|
||||
This will reverse the most recent upload:
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'monospace', fontSize: '0.75rem', color: '#94A3B8',
|
||||
background: 'rgba(15,23,42,0.6)', borderRadius: '0.375rem',
|
||||
padding: '0.625rem 0.75rem', marginBottom: '1.25rem',
|
||||
border: '1px solid rgba(239,68,68,0.15)',
|
||||
}}>
|
||||
<div><span style={{ color: '#64748B' }}>File:</span> {lastUpload.report_date || 'unknown date'}</div>
|
||||
<div style={{ marginTop: '0.25rem', fontSize: '0.68rem', color: '#475569' }}>
|
||||
New items will be deleted, resolved items will be reactivated, and the upload record will be removed.
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
||||
<button
|
||||
onClick={() => setRollbackConfirm(false)}
|
||||
style={{
|
||||
flex: 1, padding: '0.625rem', background: 'transparent',
|
||||
border: '1px solid rgba(100,116,139,0.4)', borderRadius: '0.375rem',
|
||||
color: '#64748B', cursor: 'pointer',
|
||||
fontFamily: 'monospace', fontSize: '0.8rem',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.8)'}
|
||||
onMouseLeave={e => e.currentTarget.style.borderColor = 'rgba(100,116,139,0.4)'}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRollback}
|
||||
disabled={rollbackLoading}
|
||||
style={{
|
||||
flex: 2, padding: '0.625rem',
|
||||
background: rollbackLoading ? 'rgba(239,68,68,0.05)' : 'rgba(239,68,68,0.1)',
|
||||
border: '1px solid #EF4444',
|
||||
borderRadius: '0.375rem',
|
||||
color: '#EF4444', cursor: rollbackLoading ? 'wait' : 'pointer',
|
||||
fontFamily: 'monospace', fontSize: '0.8rem',
|
||||
fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
|
||||
opacity: rollbackLoading ? 0.6 : 1,
|
||||
}}
|
||||
onMouseEnter={e => { if (!rollbackLoading) e.currentTarget.style.background = 'rgba(239,68,68,0.18)'; }}
|
||||
onMouseLeave={e => { if (!rollbackLoading) e.currentTarget.style.background = 'rgba(239,68,68,0.1)'; }}>
|
||||
{rollbackLoading
|
||||
? <><Loader style={{ width: '14px', height: '14px', animation: 'spin 1s linear infinite' }} /> Rolling back…</>
|
||||
: <><RotateCcw style={{ width: '14px', height: '14px' }} /> Confirm Rollback</>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Rollback result toast ────────────────────────────────── */}
|
||||
{rollbackResult && (
|
||||
<div style={{
|
||||
position: 'fixed', bottom: '1.5rem', right: '1.5rem', zIndex: 70,
|
||||
background: rollbackResult.error
|
||||
? 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)'
|
||||
: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: `1px solid ${rollbackResult.error ? 'rgba(239,68,68,0.4)' : 'rgba(16,185,129,0.4)'}`,
|
||||
borderRadius: '0.5rem',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
|
||||
padding: '0.875rem 1.25rem',
|
||||
maxWidth: '360px',
|
||||
fontFamily: 'monospace', fontSize: '0.75rem',
|
||||
color: rollbackResult.error ? '#F87171' : '#10B981',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => setRollbackResult(null)}
|
||||
>
|
||||
{rollbackResult.error ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<AlertCircle style={{ width: '16px', height: '16px', flexShrink: 0 }} />
|
||||
{rollbackResult.error}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
||||
<RotateCcw style={{ width: '14px', height: '14px' }} />
|
||||
{rollbackResult.message}
|
||||
</div>
|
||||
{rollbackResult.rolled_back && (
|
||||
<div style={{ fontSize: '0.68rem', color: '#64748B' }}>
|
||||
{rollbackResult.rolled_back.items_deleted} items deleted, {rollbackResult.rolled_back.items_reactivated} reactivated
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user