Add BU Lookup tool to Admin panel
Backend: - GET /api/ivanti/findings/bu-lookup?q=<hostname|ip> — queries the live Ivanti API to discover which BU a host is assigned to. Returns deduplicated results with hostName, ipAddress, BU, and hostId. Admin-only endpoint. Frontend: - Add 'BU Lookup' tab to Admin page with search input and results table - Shows BU assignment badge (blue for tagged, amber for untagged) - Supports Enter key to search, loading state, error display - Useful for verifying team assignments when onboarding new users
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Shield, Clock, Activity, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, X, ChevronLeft, ChevronRight, Search, Users, FileText } from 'lucide-react';
|
||||
import { Shield, Clock, Activity, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, X, ChevronLeft, ChevronRight, Search, Users, FileText, Globe } from 'lucide-react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import ConfirmModal from '../ConfirmModal';
|
||||
|
||||
@@ -8,6 +8,7 @@ const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||
const TABS = [
|
||||
{ id: 'users', label: 'User Management', icon: Shield },
|
||||
{ id: 'audit', label: 'Audit Log', icon: Clock },
|
||||
{ id: 'bu-lookup', label: 'BU Lookup', icon: Globe },
|
||||
{ id: 'system', label: 'System Info', icon: Activity },
|
||||
];
|
||||
|
||||
@@ -987,6 +988,128 @@ function AuditLogPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BULookupPanel — query Ivanti API to discover which BU a host belongs to
|
||||
// ---------------------------------------------------------------------------
|
||||
function BULookupPanel() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleSearch = async () => {
|
||||
const q = query.trim();
|
||||
if (!q) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResults(null);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/ivanti/findings/bu-lookup?q=${encodeURIComponent(q)}`, { credentials: 'include' });
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || `HTTP ${res.status}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
setResults(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '0.5rem' }}>
|
||||
BU Lookup
|
||||
</h3>
|
||||
<p style={{ fontSize: '0.8rem', color: '#94A3B8', marginBottom: '1rem' }}>
|
||||
Search the Ivanti API by hostname or IP to discover which BU a host is assigned to. Use this to verify team assignments for new users.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSearch(); }}
|
||||
placeholder="Enter hostname or IP address..."
|
||||
style={{
|
||||
flex: 1, padding: '0.5rem 0.75rem',
|
||||
background: 'rgba(15, 23, 42, 0.6)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.25)',
|
||||
borderRadius: '0.375rem', color: '#E2E8F0',
|
||||
fontSize: '0.85rem', fontFamily: 'monospace', outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={loading || !query.trim()}
|
||||
style={{
|
||||
padding: '0.5rem 1rem', borderRadius: '0.375rem',
|
||||
background: 'rgba(14, 165, 233, 0.15)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.4)',
|
||||
color: '#0EA5E9', fontSize: '0.8rem', fontWeight: 600,
|
||||
fontFamily: 'monospace', cursor: 'pointer',
|
||||
opacity: loading || !query.trim() ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{loading ? 'Searching...' : 'Lookup'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ padding: '0.75rem', background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)', borderRadius: '0.375rem', color: '#FCA5A5', fontSize: '0.8rem', marginBottom: '1rem' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{results && (
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748B', marginBottom: '0.75rem' }}>
|
||||
Found {results.total.toLocaleString()} finding(s) matching "{results.query}" — showing {results.findings.length} unique hosts:
|
||||
</div>
|
||||
{results.findings.length === 0 ? (
|
||||
<div style={{ padding: '1rem', textAlign: 'center', color: '#475569', fontSize: '0.85rem' }}>
|
||||
No findings found for this hostname/IP in Ivanti.
|
||||
</div>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid rgba(14,165,233,0.2)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>Hostname</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>IP Address</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>BU Assignment</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>Host ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.findings.map((f, i) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
|
||||
<td style={{ padding: '0.5rem', color: '#CBD5E1', fontFamily: 'monospace' }}>{f.hostName || '—'}</td>
|
||||
<td style={{ padding: '0.5rem', color: '#CBD5E1', fontFamily: 'monospace' }}>{f.ipAddress || '—'}</td>
|
||||
<td style={{ padding: '0.5rem' }}>
|
||||
<span style={{
|
||||
padding: '0.15rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.7rem', fontWeight: 600, fontFamily: 'monospace',
|
||||
background: f.bu === 'Untagged' ? 'rgba(245,158,11,0.15)' : 'rgba(14,165,233,0.15)',
|
||||
border: f.bu === 'Untagged' ? '1px solid rgba(245,158,11,0.3)' : '1px solid rgba(14,165,233,0.3)',
|
||||
color: f.bu === 'Untagged' ? '#FCD34D' : '#7DD3FC',
|
||||
}}>
|
||||
{f.bu}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.5rem', color: '#64748B', fontFamily: 'monospace', fontSize: '0.75rem' }}>{f.hostId || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SystemInfoPanel
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1396,6 +1519,16 @@ export default function AdminPage() {
|
||||
<SystemInfoPanel />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'bu-lookup' && (
|
||||
<div
|
||||
className="intel-card"
|
||||
style={{ borderRadius: '0.5rem', padding: '1.25rem' }}
|
||||
>
|
||||
{/* ⚠️ CONVENTION: BULookupPanel is referenced but never defined or imported — this will throw a ReferenceError at runtime. Define the component in this file or import it. */}
|
||||
<BULookupPanel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user