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.
53 lines
2.0 KiB
JavaScript
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>
|
|
);
|
|
}
|