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:
Jordan Ramos
2026-06-26 09:20:53 -06:00
parent dab3784742
commit 58996cf4cf
2 changed files with 211 additions and 1 deletions

View File

@@ -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>
);
}