Skip to content

Commit 821daa8

Browse files
committed
Initialize Next.js Dashboard
1 parent 3302465 commit 821daa8

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
5+
// Type definitions
6+
export type SeverityLevel = 'high' | 'medium' | 'low';
7+
8+
export interface WatchdogEvent {
9+
id: string;
10+
timestamp: string;
11+
chain: string;
12+
severity: SeverityLevel;
13+
description: string;
14+
}
15+
16+
const severityStyles: Record<SeverityLevel, string> = {
17+
high: 'bg-orange-950 text-orange-400 border border-orange-800',
18+
medium: 'bg-yellow-950 text-yellow-400 border border-yellow-800',
19+
low: 'bg-blue-950 text-blue-400 border border-blue-800',
20+
};
21+
22+
// Mock data — replaced by API data in production
23+
const mockEvents: WatchdogEvent[] = [
24+
{
25+
id: 'evt-001',
26+
timestamp: '2026-06-11 14:32:01 UTC',
27+
chain: 'Soroban',
28+
severity: 'high',
29+
description: 'Unauthorized set_admin call detected on Vault Contract',
30+
},
31+
{
32+
id: 'evt-002',
33+
timestamp: '2026-06-11 12:15:45 UTC',
34+
chain: 'Ethereum',
35+
severity: 'medium',
36+
description: 'Emergency Pause triggered by multisig address',
37+
},
38+
{
39+
id: 'evt-003',
40+
timestamp: '2026-06-10 09:05:12 UTC',
41+
chain: 'Soroban',
42+
severity: 'low',
43+
description: 'New verified contract deployment matching signature',
44+
},
45+
{
46+
id: 'evt-004',
47+
timestamp: '2026-06-09 18:22:30 UTC',
48+
chain: 'Polygon',
49+
severity: 'high',
50+
description: 'Sudden large-scale transfer (25% liquidity drain)',
51+
},
52+
{
53+
id: 'evt-005',
54+
timestamp: '2026-06-09 08:11:05 UTC',
55+
chain: 'Ethereum',
56+
severity: 'medium',
57+
description: 'Unusual high-frequency minting behavior',
58+
},
59+
];
60+
61+
interface Props {
62+
events?: WatchdogEvent[];
63+
}
64+
65+
export function AlertHistoryTable({ events = mockEvents }: Props) {
66+
const [filterChain, setFilterChain] = useState<string>('All');
67+
68+
const uniqueChains = ['All', ...Array.from(new Set(events.map(e => e.chain)))];
69+
70+
const filtered =
71+
filterChain === 'All' ? events : events.filter(e => e.chain === filterChain);
72+
73+
return (
74+
<div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
75+
{/* Header */}
76+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-800">
77+
<h2 className="text-base font-semibold text-white flex items-center gap-2">
78+
<svg
79+
className="w-5 h-5 text-gray-400"
80+
fill="none"
81+
stroke="currentColor"
82+
viewBox="0 0 24 24"
83+
aria-hidden="true"
84+
>
85+
<path
86+
strokeLinecap="round"
87+
strokeLinejoin="round"
88+
strokeWidth={2}
89+
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
90+
/>
91+
</svg>
92+
Alert History
93+
</h2>
94+
95+
<div className="flex items-center gap-2">
96+
<label htmlFor="chain-filter" className="text-sm text-gray-400 sr-only">
97+
Filter by Chain
98+
</label>
99+
<select
100+
id="chain-filter"
101+
className="bg-gray-800 border border-gray-700 text-sm text-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-sentinel-600"
102+
value={filterChain}
103+
onChange={e => setFilterChain(e.target.value)}
104+
>
105+
{uniqueChains.map(chain => (
106+
<option key={chain} value={chain}>
107+
{chain}
108+
</option>
109+
))}
110+
</select>
111+
</div>
112+
</div>
113+
114+
{/* Table */}
115+
<div className="overflow-x-auto">
116+
<table className="w-full text-sm">
117+
<thead>
118+
<tr className="border-b border-gray-800">
119+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
120+
Timestamp
121+
</th>
122+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
123+
Chain
124+
</th>
125+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
126+
Severity
127+
</th>
128+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
129+
Description
130+
</th>
131+
</tr>
132+
</thead>
133+
<tbody className="divide-y divide-gray-800/60">
134+
{filtered.length > 0 ? (
135+
filtered.map(event => (
136+
<tr
137+
key={event.id}
138+
className="hover:bg-gray-800/40 transition-colors"
139+
>
140+
<td className="px-6 py-4 text-gray-400 font-mono text-xs whitespace-nowrap">
141+
{event.timestamp}
142+
</td>
143+
<td className="px-6 py-4">
144+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-800 text-gray-300 border border-gray-700">
145+
{event.chain}
146+
</span>
147+
</td>
148+
<td className="px-6 py-4">
149+
<span
150+
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${severityStyles[event.severity]}`}
151+
>
152+
{event.severity}
153+
</span>
154+
</td>
155+
<td
156+
className="px-6 py-4 text-gray-300 max-w-sm truncate"
157+
title={event.description}
158+
>
159+
{event.description}
160+
</td>
161+
</tr>
162+
))
163+
) : (
164+
<tr>
165+
<td colSpan={4} className="px-6 py-10 text-center text-gray-500">
166+
No alerts found for the selected chain.
167+
</td>
168+
</tr>
169+
)}
170+
</tbody>
171+
</table>
172+
</div>
173+
</div>
174+
);
175+
}
176+
177+
export default AlertHistoryTable;

0 commit comments

Comments
 (0)