Files
cve-dashboard/frontend/src/components/SidebarTabs.js
Jordan Ramos f119cca1d7 Add recent activity feed and tabbed sidebar layout
New features:
- Recent Activity feed widget shows last 8 actions from audit log
  with relative timestamps, auto-refreshes every 60s
- Right sidebar reorganized: Calendar + Activity always visible,
  Tickets/Archer/Ivanti behind tab switcher to eliminate dead space

Backend:
- New GET /api/recent-activity endpoint (any authenticated user)
  Returns last N audit entries excluding login/logout noise
  Lighter than the full admin audit-logs endpoint

Frontend:
- RecentActivityFeed component with action labels, colored dots,
  timeAgo formatting, and manual refresh button
- SidebarTabs component with Tickets/Archer/Ivanti tabs
- OpenTicketsPanel and IvantiWorkflowPanel support embedded prop
  to render without their own panel wrapper when inside tabs

Layout change:
Before: Calendar | Tickets | Archer | Ivanti (4 stacked panels)
After:  Calendar | Activity | [Tickets | Archer | Ivanti] (tabs)

This keeps the sidebar height proportional to the CVE list area
instead of extending far below the main content.
2026-06-23 12:16:40 -06:00

53 lines
2.0 KiB
JavaScript

import React, { useState } from 'react';
import { AlertCircle, Shield, Activity } from 'lucide-react';
const TAB_CONFIG = [
{ id: 'tickets', label: 'Tickets', icon: AlertCircle, color: '#F59E0B' },
{ id: 'archer', label: 'Archer', icon: Shield, color: '#8B5CF6' },
{ id: 'ivanti', label: 'Ivanti', icon: Activity, color: '#0D9488' },
];
export default function SidebarTabs({ children }) {
const [activeTab, setActiveTab] = useState('tickets');
// children should be an object: { tickets: <Node>, archer: <Node>, ivanti: <Node> }
// Or we accept children as array and map by index
const panels = children;
return (
<div className="panel-card" style={{ padding: 0, overflow: 'hidden' }}>
{/* Tab bar */}
<div className="flex" style={{ borderBottom: '1px solid rgba(100, 116, 139, 0.25)' }}>
{TAB_CONFIG.map(tab => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className="flex-1 flex items-center justify-center gap-1.5 py-2.5 px-2 transition-all font-mono text-xs uppercase tracking-wider"
style={{
color: isActive ? tab.color : '#64748B',
background: isActive ? `rgba(${tab.color === '#F59E0B' ? '245,158,11' : tab.color === '#8B5CF6' ? '139,92,246' : '13,148,136'}, 0.08)` : 'transparent',
borderBottom: isActive ? `2px solid ${tab.color}` : '2px solid transparent',
}}
aria-selected={isActive}
role="tab"
>
<Icon className="w-3.5 h-3.5" />
<span>{tab.label}</span>
</button>
);
})}
</div>
{/* Tab content */}
<div style={{ padding: '1rem' }}>
{activeTab === 'tickets' && panels.tickets}
{activeTab === 'archer' && panels.archer}
{activeTab === 'ivanti' && panels.ivanti}
</div>
</div>
);
}