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.
This commit is contained in:
52
frontend/src/components/SidebarTabs.js
Normal file
52
frontend/src/components/SidebarTabs.js
Normal file
@@ -0,0 +1,52 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user