Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 126 additions & 23 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

420 changes: 420 additions & 0 deletions src/components/common/EnhancedTable.tsx

Large diffs are not rendered by default.

84 changes: 66 additions & 18 deletions src/components/dashboard/Analytics.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import React from "react";
import React, { useState } from "react";
import { useAnalytics } from "../../hooks/useAnalytics";
import AnalyticsChart from "../charts/AnalyticsChart";
import CorrelationGraph from "../charts/CorrelationGraph";
import { StatCard } from "./Card";
import CustomReports from "./CustomReports";
import EnhancedTable, { type TableDensity, type ColumnPreset } from "../common/EnhancedTable";
import { useTablePresets } from "../../hooks/useTablePresets";
import type { AlertEntry } from "./types";

const RISK_SIGNAL_COLUMNS = [
{ id: 'label', label: 'Risk Signal', width: '2fr' },
{ id: 'severity', label: 'Severity', width: '1fr' },
{ id: 'status', label: 'Status', width: '1fr' },
];

function RiskItem({ signal }: { signal: AlertEntry }) {
const color =
signal.severity === "high"
Expand Down Expand Up @@ -37,6 +45,9 @@ export default function Analytics() {
const network = analytics?.network || {};
const risks: AlertEntry[] = analytics?.risks || [];

// Table presets for risk signals
const riskPresets = useTablePresets('analytics-risk-signals', ['label', 'severity', 'status'], 'comfortable');

return (
<div className="animate-in" style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<div style={{ fontFamily: "var(--font-display)", fontSize: "22px", fontWeight: 700 }}>
Expand All @@ -62,24 +73,61 @@ export default function Analytics() {

<CorrelationGraph />

<div
style={{
background: "var(--bg-card)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-lg)",
padding: "14px",
display: "flex",
flexDirection: "column",
gap: "10px",
}}
{/* Risk Signals Table */}
<EnhancedTable
columns={RISK_SIGNAL_COLUMNS}
visibleColumns={riskPresets.visibleColumns}
onVisibleColumnsChange={riskPresets.setVisibleColumns}
density={riskPresets.density}
onDensityChange={riskPresets.setDensity}
presets={riskPresets.presets}
onPresetSave={riskPresets.onPresetSave}
onPresetDelete={riskPresets.onPresetDelete}
onPresetApply={riskPresets.onPresetApply}
stickyHeader={true}
maxHeight="400px"
>
<div style={{ fontFamily: "var(--font-display)", fontSize: "13px" }}>Risk Signals</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr", gap: "8px" }}>
{risks.map((risk) => (
<RiskItem key={risk.id} signal={risk} />
))}
</div>
</div>
{risks.map((risk) => (
<div
key={risk.id}
style={{
display: 'grid',
gridTemplateColumns: riskPresets.visibleColumns.map((id) => {
const col = RISK_SIGNAL_COLUMNS.find((c) => c.id === id);
return col?.width || '1fr';
}).join(' '),
gap: '12px',
padding: riskPresets.density === 'compact' ? '8px 12px' : riskPresets.density === 'comfortable' ? '12px 18px' : '16px 24px',
borderBottom: '1px solid var(--border)',
fontSize: riskPresets.density === 'compact' ? '11px' : riskPresets.density === 'comfortable' ? '12px' : '13px',
alignItems: 'center',
}}
>
{riskPresets.visibleColumns.includes('label') && (
<span style={{ color: 'var(--text-primary)' }}>{risk.label}</span>
)}
{riskPresets.visibleColumns.includes('severity') && (
<span style={{
padding: '2px 8px',
borderRadius: 'var(--radius-sm)',
fontSize: '10px',
fontWeight: 600,
textTransform: 'uppercase',
background: risk.severity === 'high' ? 'var(--red-glow-sm)' : risk.severity === 'medium' ? 'var(--amber-glow-sm)' : 'var(--cyan-glow-sm)',
color: risk.severity === 'high' ? 'var(--red)' : risk.severity === 'medium' ? 'var(--amber)' : 'var(--cyan)',
border: `1px solid ${risk.severity === 'high' ? 'var(--red)' : risk.severity === 'medium' ? 'var(--amber)' : 'var(--cyan)'}`,
}}>
{risk.severity}
</span>
)}
{riskPresets.visibleColumns.includes('status') && (
<span style={{ color: risk.active ? 'var(--green)' : 'var(--text-muted)' }}>
{risk.active ? 'Active' : 'Inactive'}
</span>
)}
</div>
))}
</EnhancedTable>
</div>
);
}
207 changes: 168 additions & 39 deletions src/components/dashboard/AuditLog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { useState, useEffect, useCallback, useRef, type MouseEvent, type ReactNode } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useStore } from '../../lib/store';
import { formatDistanceToNow } from 'date-fns';
import { Download, Printer } from 'lucide-react';
import Card from './Card';
import EnhancedTable, { type TableDensity } from '../common/EnhancedTable';
import { useTablePresets } from '../../hooks/useTablePresets';
import type { AuditEntry } from '../../types/audit';

interface LogRowProps {
Expand Down Expand Up @@ -35,6 +38,13 @@
const CATEGORIES = ['security', 'transaction', 'auth', 'system', 'network', 'user'];
const SEVERITIES = ['critical', 'error', 'warning', 'info', 'debug'];

const AUDIT_LOG_COLUMNS = [
{ id: 'severity', label: 'Severity', width: 'auto' },
{ id: 'category', label: 'Category', width: '100px' },
{ id: 'action', label: 'Action', width: '1fr' },
{ id: 'timestamp', label: 'Time', width: '140px' },
];

const SeverityBadge = ({ severity }: SeverityBadgeProps) => {
const style = SEVERITY_COLORS[severity] || SEVERITY_COLORS.info;
return (
Expand Down Expand Up @@ -286,6 +296,9 @@
const [filters, setFilters] = useState<FilterState>({ severity: [], category: [], search: '' });
const listRef = useRef<HTMLDivElement>(null);

// Table presets for audit log
const auditPresets = useTablePresets('audit-log', ['severity', 'category', 'action', 'timestamp'], 'comfortable');

useEffect(() => {
async function fetchLogs() {
setIsLoading(true);
Expand Down Expand Up @@ -318,6 +331,33 @@

const selectedEntry = logs.find((e: AuditEntry) => e.id === selectedId) || null;

const handleExportCsv = useCallback(() => {
const headers = ['Severity', 'Category', 'Action', 'Timestamp'];
const rows = filteredLogs.map(log => [
log.severity,
log.category,
log.action,
new Date(log.timestamp).toISOString()
]);

const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');

const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `audit-log-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
URL.revokeObjectURL(url);
}, [filteredLogs]);

const handlePrint = useCallback(() => {
window.print();
}, []);

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div>
Expand All @@ -329,50 +369,139 @@
</p>
</div>

<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={handleExportCsv}
style={{
padding: '8px 12px',
background: 'var(--bg-elevated)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
color: 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<Download size={14} />
<span>Export CSV</span>
</button>
<button
onClick={handlePrint}
style={{
padding: '8px 12px',
background: 'var(--bg-elevated)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
color: 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<Printer size={14} />
<span>Print</span>
</button>
</div>

<FilterBar onFilterChange={(f: FilterState) => setFilters(f)} />

<div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: '14px' }}>
<div className="card" style={{
background: 'var(--bg-elevated)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
maxHeight: '600px',
<EnhancedTable
columns={AUDIT_LOG_COLUMNS}
visibleColumns={auditPresets.visibleColumns}
onVisibleColumnsChange={auditPresets.setVisibleColumns}
density={auditPresets.density}
onDensityChange={auditPresets.setDensity}
presets={auditPresets.presets}
onPresetSave={auditPresets.onPresetSave}
onPresetDelete={auditPresets.onPresetDelete}
onPresetApply={auditPresets.onPresetApply}
stickyHeader={true}
maxHeight="600px"
>
<div style={{
padding: '10px 14px',
borderBottom: '1px solid var(--border)',
fontSize: '12px',
color: 'var(--text-muted)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
background: 'var(--bg-card)',
}}>
<div style={{
padding: '10px 14px',
borderBottom: '1px solid var(--border)',
fontSize: '12px',
color: 'var(--text-muted)',
display: 'flex',
justifyContent: 'space-between',
}}>
<span>{filteredLogs.length} entries</span>
<span>{logs.length} total</span>
<span>{filteredLogs.length} entries</span>
<span>{logs.length} total</span>
</div>
{isLoading ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'var(--text-muted)' }}>
Loading audit log...
</div>
<div ref={listRef} style={{ overflowY: 'auto', flex: 1 }}>
{isLoading ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'var(--text-muted)' }}>
Loading audit log...
</div>
) : filteredLogs.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'var(--text-muted)' }}>
{logs.length === 0 ? 'No audit entries yet.' : 'No entries match the current filters.'}
</div>
) : (
filteredLogs.map((entry: AuditEntry) => (
<LogRow
key={entry.id}
entry={entry}
isSelected={selectedId === entry.id}
onSelect={setSelectedId}
/>
))
)}
) : filteredLogs.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'var(--text-muted)' }}>
{logs.length === 0 ? 'No audit entries yet.' : 'No entries match the current filters.'}
</div>
</div>
) : (
filteredLogs.map((entry: AuditEntry) => (
<div
key={entry.id}
onClick={() => setSelectedId(entry.id)}
style={{
display: 'grid',
gridTemplateColumns: auditPresets.visibleColumns.map((id) => {
const col = AUDIT_LOG_COLUMNS.find((c) => c.id === id);
return col?.width || '1fr';
}).join(' '),
gap: '12px',
padding: auditPresets.density === 'compact' ? '8px 12px' : auditPresets.density === 'comfortable' ? '10px 14px' : '12px 16px',
borderBottom: '1px solid var(--border)',
cursor: 'pointer',
background: selectedId === entry.id ? 'var(--cyan-glow-sm)' : 'transparent',
transition: 'var(--transition)',
alignItems: 'center',
fontSize: auditPresets.density === 'compact' ? '11px' : auditPresets.density === 'comfortable' ? '12px' : '13px',
}}
>
{auditPresets.visibleColumns.includes('severity') && (
<SeverityBadge severity={entry.severity} />
)}
{auditPresets.visibleColumns.includes('category') && (
<div style={{
color: 'var(--text-muted)',
fontSize: auditPresets.density === 'compact' ? '10px' : auditPresets.density === 'comfortable' ? '11px' : '12px',
fontFamily: 'var(--font-mono)',
textTransform: 'capitalize',
}}>
{entry.category}
</div>
)}
{auditPresets.visibleColumns.includes('action') && (
<div style={{
color: 'var(--text-primary)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontFamily: 'var(--font-mono)',
}}>
{entry.action}
</div>
)}
{auditPresets.visibleColumns.includes('timestamp') && (
<div style={{
color: 'var(--text-muted)',
fontSize: auditPresets.density === 'compact' ? '10px' : auditPresets.density === 'comfortable' ? '11px' : '12px',
fontFamily: 'var(--font-mono)',
textAlign: 'right',
}}>
{formatDistanceToNow(new Date(entry.timestamp), { addSuffix: true })}
</div>
)}
</div>
))
)}
</EnhancedTable>

<div className="card" style={{
background: 'var(--bg-elevated)',
Expand Down Expand Up @@ -463,6 +592,6 @@
)}
</div>
</div>
</div>

Check failure on line 595 in src/components/dashboard/AuditLog.tsx

View workflow job for this annotation

GitHub Actions / TypeScript Type Check

')' expected.
);

Check failure on line 596 in src/components/dashboard/AuditLog.tsx

View workflow job for this annotation

GitHub Actions / TypeScript Type Check

Expression expected.
}
1 change: 1 addition & 0 deletions src/components/dashboard/ComplianceDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { format } from 'date-fns';
import { Download, FileText, Printer } from 'lucide-react';
import Card, { StatCard } from './Card';
import {
useComplianceReport,
Expand Down
Loading
Loading