@cybermem/dashboard 0.5.12 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,94 +1,108 @@
1
- import { checkRateLimit, rateLimitResponse } from '@/lib/rate-limit'
2
- import { NextRequest, NextResponse } from 'next/server'
1
+ import { checkRateLimit, rateLimitResponse } from "@/lib/rate-limit";
2
+ import { NextRequest, NextResponse } from "next/server";
3
3
 
4
- export const dynamic = 'force-dynamic'
4
+ export const dynamic = "force-dynamic";
5
5
 
6
6
  interface ServiceStatus {
7
- name: string
8
- status: 'ok' | 'error' | 'warning'
9
- message?: string
10
- latencyMs?: number
7
+ name: string;
8
+ status: "ok" | "error" | "warning";
9
+ message?: string;
10
+ latencyMs?: number;
11
11
  }
12
12
 
13
13
  interface SystemHealth {
14
- overall: 'ok' | 'degraded' | 'error'
15
- services: ServiceStatus[]
16
- timestamp: string
14
+ overall: "ok" | "degraded" | "error";
15
+ services: ServiceStatus[];
16
+ timestamp: string;
17
17
  }
18
18
 
19
- async function checkService(name: string, url: string, timeout = 3000): Promise<ServiceStatus> {
20
- const start = Date.now()
19
+ async function checkService(
20
+ name: string,
21
+ url: string,
22
+ timeout = 3000,
23
+ ): Promise<ServiceStatus> {
24
+ const start = Date.now();
21
25
  try {
22
- const controller = new AbortController()
23
- const timeoutId = setTimeout(() => controller.abort(), timeout)
26
+ const controller = new AbortController();
27
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
24
28
 
25
29
  const res = await fetch(url, {
26
30
  signal: controller.signal,
27
- cache: 'no-store',
31
+ cache: "no-store",
28
32
  headers: {
29
- 'X-Client-Name': 'CyberMem-Dashboard'
30
- }
31
- })
32
- clearTimeout(timeoutId)
33
+ "X-Client-Name": "CyberMem-Dashboard",
34
+ },
35
+ });
36
+ clearTimeout(timeoutId);
33
37
 
34
- const latencyMs = Date.now() - start
38
+ const latencyMs = Date.now() - start;
35
39
 
36
40
  if (res.ok) {
37
- return { name, status: 'ok', latencyMs }
41
+ return { name, status: "ok", latencyMs };
38
42
  }
39
43
  return {
40
44
  name,
41
- status: 'warning',
45
+ status: "warning",
42
46
  message: `HTTP ${res.status}`,
43
- latencyMs
44
- }
47
+ latencyMs,
48
+ };
45
49
  } catch (error: any) {
46
50
  return {
47
51
  name,
48
- status: 'error',
49
- message: error.name === 'AbortError' ? 'Timeout' : (error.message || 'Connection failed')
50
- }
52
+ status: "error",
53
+ message:
54
+ error.name === "AbortError"
55
+ ? "Timeout"
56
+ : error.message || "Connection failed",
57
+ };
51
58
  }
52
59
  }
53
60
 
54
61
  export async function GET(request: NextRequest) {
55
62
  // Rate limiting
56
- const rateLimit = checkRateLimit(request)
63
+ const rateLimit = checkRateLimit(request);
57
64
  if (!rateLimit.allowed) {
58
- return rateLimitResponse(rateLimit.resetIn)
65
+ return rateLimitResponse(rateLimit.resetIn);
59
66
  }
60
67
 
61
68
  // Use environment variables with sensible defaults for local Docker stack
62
- const prometheusUrl = process.env.PROMETHEUS_URL || 'http://localhost:9092'
63
- const openMemoryUrl = process.env.CYBERMEM_URL || 'http://localhost:8626'
64
- const vectorUrl = process.env.VECTOR_URL // Vector is optional
69
+ const dbExporterUrl = process.env.DB_EXPORTER_URL || "http://localhost:8000";
70
+ // OpenMemory API is optional in SDK-based architecture
71
+ // Only check if explicitly configured (SDK mode doesn't have HTTP API)
72
+ const openMemoryUrl = process.env.OPENMEMORY_URL || process.env.CYBERMEM_URL;
73
+ const vectorUrl = process.env.VECTOR_URL; // Vector is optional
65
74
 
66
75
  const checks: Promise<ServiceStatus>[] = [
67
- checkService('OpenMemory API', `${openMemoryUrl}/health`),
68
- checkService('Prometheus', `${prometheusUrl}/-/ready`),
69
- ]
76
+ checkService("Database", `${dbExporterUrl}/health`),
77
+ ];
78
+
79
+ // Only check OpenMemory API if explicitly configured
80
+ // In SDK mode, there's no HTTP API - memory is handled via MCP
81
+ if (openMemoryUrl) {
82
+ checks.push(checkService("OpenMemory API", `${openMemoryUrl}/health`));
83
+ }
70
84
 
71
85
  // Only check Vector if configured
72
86
  if (vectorUrl) {
73
- checks.push(checkService('Vector', `${vectorUrl}/health`))
87
+ checks.push(checkService("Vector", `${vectorUrl}/health`));
74
88
  }
75
89
 
76
- const services = await Promise.all(checks)
90
+ const services = await Promise.all(checks);
77
91
 
78
92
  // Determine overall status
79
- const hasError = services.some(s => s.status === 'error')
80
- const hasWarning = services.some(s => s.status === 'warning')
93
+ const hasError = services.some((s) => s.status === "error");
94
+ const hasWarning = services.some((s) => s.status === "warning");
81
95
 
82
96
  const health: SystemHealth = {
83
- overall: hasError ? 'error' : hasWarning ? 'degraded' : 'ok',
97
+ overall: hasError ? "error" : hasWarning ? "degraded" : "ok",
84
98
  services,
85
- timestamp: new Date().toISOString()
86
- }
99
+ timestamp: new Date().toISOString(),
100
+ };
87
101
 
88
102
  return NextResponse.json(health, {
89
103
  headers: {
90
- 'Cache-Control': 'no-cache, no-store, must-revalidate',
91
- 'X-RateLimit-Remaining': String(rateLimit.remaining)
92
- }
93
- })
104
+ "Cache-Control": "no-cache, no-store, must-revalidate",
105
+ "X-RateLimit-Remaining": String(rateLimit.remaining),
106
+ },
107
+ });
94
108
  }
@@ -1,26 +1,54 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import { useDashboard } from "@/lib/data/dashboard-context"
4
- import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronLeft, ChevronRight, Download, RefreshCw } from "lucide-react"
5
- import { useState } from "react"
3
+ import { useDashboard } from "@/lib/data/dashboard-context";
4
+ import {
5
+ ArrowDown,
6
+ ArrowUp,
7
+ ArrowUpDown,
8
+ ChevronDown,
9
+ ChevronLeft,
10
+ ChevronRight,
11
+ Download,
12
+ RefreshCw,
13
+ } from "lucide-react";
14
+ import { useState } from "react";
6
15
 
7
16
  interface AuditLogTableProps {
8
- logs: any[]
9
- loading: boolean
10
- currentPage: number
11
- totalPages: number
12
- onPageChange: (page: number) => void
13
- sortField: string
14
- sortDirection: 'asc' | 'desc'
15
- onSort: (field: string) => void
17
+ logs: any[];
18
+ loading: boolean;
19
+ currentPage: number;
20
+ totalPages: number;
21
+ onPageChange: (page: number) => void;
22
+ sortField: string;
23
+ sortDirection: "asc" | "desc";
24
+ onSort: (field: string) => void;
16
25
  }
17
26
 
18
- const statusConfig: Record<string, { bg: string; text: string; border: string }> = {
19
- Success: { bg: "bg-emerald-500/10", text: "text-emerald-400", border: "border-emerald-500/30" },
20
- Warning: { bg: "bg-amber-500/10", text: "text-amber-400", border: "border-amber-500/30" },
21
- Error: { bg: "bg-red-500/10", text: "text-red-400", border: "border-red-500/30" },
22
- Canceled: { bg: "bg-slate-500/10", text: "text-slate-400", border: "border-slate-500/30" },
23
- }
27
+ const statusConfig: Record<
28
+ string,
29
+ { bg: string; text: string; border: string }
30
+ > = {
31
+ Success: {
32
+ bg: "bg-emerald-500/10",
33
+ text: "text-emerald-400",
34
+ border: "border-emerald-500/30",
35
+ },
36
+ Warning: {
37
+ bg: "bg-amber-500/10",
38
+ text: "text-amber-400",
39
+ border: "border-amber-500/30",
40
+ },
41
+ Error: {
42
+ bg: "bg-red-500/10",
43
+ text: "text-red-400",
44
+ border: "border-red-500/30",
45
+ },
46
+ Canceled: {
47
+ bg: "bg-slate-500/10",
48
+ text: "text-slate-400",
49
+ border: "border-slate-500/30",
50
+ },
51
+ };
24
52
 
25
53
  const periods = [
26
54
  { label: "1 Hour", value: "1h" },
@@ -28,7 +56,7 @@ const periods = [
28
56
  { label: "7 Days", value: "7d" },
29
57
  { label: "30 Days", value: "30d" },
30
58
  { label: "All Time", value: "all" },
31
- ]
59
+ ];
32
60
 
33
61
  export default function AuditLogTable({
34
62
  logs,
@@ -38,64 +66,78 @@ export default function AuditLogTable({
38
66
  onPageChange,
39
67
  sortField,
40
68
  sortDirection,
41
- onSort
69
+ onSort,
42
70
  }: AuditLogTableProps) {
43
- const [period, setPeriod] = useState("all")
44
- const [showExportMenu, setShowExportMenu] = useState(false)
45
- const { clientConfigs } = useDashboard()
71
+ const [period, setPeriod] = useState("all");
72
+ const [showExportMenu, setShowExportMenu] = useState(false);
73
+ const { clientConfigs } = useDashboard();
46
74
 
47
75
  const getClientConfig = (rawName: string) => {
48
- if (!rawName) return undefined
49
- const nameLower = rawName.toLowerCase()
50
- return clientConfigs.find((c: any) => new RegExp(c.match, 'i').test(nameLower))
51
- }
76
+ if (!rawName) return undefined;
77
+ const nameLower = rawName.toLowerCase();
78
+ return clientConfigs.find((c: any) =>
79
+ new RegExp(c.match, "i").test(nameLower),
80
+ );
81
+ };
52
82
 
53
83
  const getClientDisplayName = (rawName: string) => {
54
- const config = getClientConfig(rawName)
55
- return config ? config.name : rawName
56
- }
84
+ const config = getClientConfig(rawName);
85
+ return config ? config.name : rawName;
86
+ };
57
87
 
58
88
  const exportToCSV = () => {
59
- const headers = ['Timestamp', 'Client', 'Operation', 'Status', 'Description']
89
+ const headers = [
90
+ "Timestamp",
91
+ "Client",
92
+ "Operation",
93
+ "Description",
94
+ "Status",
95
+ ];
60
96
  const csvContent = [
61
- headers.join(','),
62
- ...logs.map(log => [
63
- `"${log.date}"`,
64
- `"${getClientDisplayName(log.client)}"`,
65
- `"${log.operation}"`,
66
- `"${log.status}"`,
67
- `"${log.description}"`
68
- ].join(','))
69
- ].join('\n')
97
+ headers.join(","),
98
+ ...logs.map((log) =>
99
+ [
100
+ `"${log.date}"`,
101
+ `"${getClientDisplayName(log.client)}"`,
102
+ `"${log.operation}"`,
103
+ `"${log.description}"`,
104
+ `"${log.status}"`,
105
+ ].join(","),
106
+ ),
107
+ ].join("\n");
70
108
 
71
- const blob = new Blob([csvContent], { type: 'text/csv' })
72
- const url = URL.createObjectURL(blob)
73
- const a = document.createElement('a')
74
- a.href = url
75
- a.download = `cybermem-audit-${new Date().toISOString().split('T')[0]}.csv`
76
- a.click()
77
- URL.revokeObjectURL(url)
78
- setShowExportMenu(false)
79
- }
109
+ const blob = new Blob([csvContent], { type: "text/csv" });
110
+ const url = URL.createObjectURL(blob);
111
+ const a = document.createElement("a");
112
+ a.href = url;
113
+ a.download = `cybermem-audit-${new Date().toISOString().split("T")[0]}.csv`;
114
+ a.click();
115
+ URL.revokeObjectURL(url);
116
+ setShowExportMenu(false);
117
+ };
80
118
 
81
119
  const exportToJSON = () => {
82
- const jsonContent = JSON.stringify(logs.map(log => ({
83
- timestamp: log.date,
84
- client: getClientDisplayName(log.client),
85
- operation: log.operation,
86
- status: log.status,
87
- description: log.description
88
- })), null, 2)
120
+ const jsonContent = JSON.stringify(
121
+ logs.map((log) => ({
122
+ timestamp: log.date,
123
+ client: getClientDisplayName(log.client),
124
+ operation: log.operation,
125
+ description: log.description,
126
+ status: log.status,
127
+ })),
128
+ null,
129
+ 2,
130
+ );
89
131
 
90
- const blob = new Blob([jsonContent], { type: 'application/json' })
91
- const url = URL.createObjectURL(blob)
92
- const a = document.createElement('a')
93
- a.href = url
94
- a.download = `cybermem-audit-${new Date().toISOString().split('T')[0]}.json`
95
- a.click()
96
- URL.revokeObjectURL(url)
97
- setShowExportMenu(false)
98
- }
132
+ const blob = new Blob([jsonContent], { type: "application/json" });
133
+ const url = URL.createObjectURL(blob);
134
+ const a = document.createElement("a");
135
+ a.href = url;
136
+ a.download = `cybermem-audit-${new Date().toISOString().split("T")[0]}.json`;
137
+ a.click();
138
+ URL.revokeObjectURL(url);
139
+ setShowExportMenu(false);
140
+ };
99
141
 
100
142
  return (
101
143
  <div className="group relative overflow-hidden rounded-2xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg p-6 transition-all duration-300">
@@ -105,7 +147,12 @@ export default function AuditLogTable({
105
147
  {/* Period Selector - Badge Style - Absolute positioned in top-right (ignoring padding) */}
106
148
  <div className="absolute top-0 right-0 z-20 group/period">
107
149
  <button className="h-8 px-3 rounded-tl-none rounded-tr-2xl rounded-bl-2xl rounded-br-none bg-white/5 border-b border-l border-white/10 hover:bg-white/10 text-white text-xs font-medium flex items-center gap-2 transition-all">
108
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
150
+ <svg
151
+ className="w-3 h-3"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ viewBox="0 0 24 24"
155
+ >
109
156
  <path
110
157
  strokeLinecap="round"
111
158
  strokeLinejoin="round"
@@ -138,89 +185,119 @@ export default function AuditLogTable({
138
185
  <div className="relative z-10">
139
186
  <div className="flex items-center gap-3 mb-6">
140
187
  <h3 className="text-lg font-semibold text-white">Audit Log</h3>
141
- {loading && <RefreshCw className="w-4 h-4 text-emerald-500 animate-spin" />}
188
+ {loading && (
189
+ <RefreshCw className="w-4 h-4 text-emerald-500 animate-spin" />
190
+ )}
142
191
  </div>
143
192
 
144
-
145
-
146
-
147
193
  <div className="overflow-x-auto min-h-[400px]">
148
194
  <table className="w-full text-sm">
149
195
  <thead>
150
196
  <tr className="border-b border-white/10">
151
197
  {[
152
- { label: "Timestamp", key: "date", width: "w-[200px]" },
153
- { label: "Client", key: "client", width: "w-[260px]" },
154
- { label: "Operation", key: "operation", width: "w-[120px]" },
155
- { label: "Status", key: "status", width: "w-[120px]" },
156
- { label: "Description", key: "description", width: "" },
198
+ { label: "Timestamp", key: "date", width: "w-[180px]" },
199
+ { label: "Client", key: "client", width: "w-[200px]" },
200
+ { label: "Operation", key: "operation", width: "w-[100px]" },
201
+ { label: "Description", key: "description", width: "flex-1" },
202
+ { label: "Status", key: "status", width: "w-[100px]" },
157
203
  ].map((header) => (
158
204
  <th
159
205
  key={header.key}
160
206
  onClick={() => onSort(header.key)}
161
- className={`text-left py-4 px-4 font-medium text-neutral-400 cursor-pointer hover:text-white transition-colors select-none group/th ${header.width}`}
207
+ className={`text-left py-4 px-3 font-medium text-neutral-400 cursor-pointer hover:text-white transition-colors select-none group/th ${header.width}`}
162
208
  >
163
209
  <div className="flex items-center gap-2">
164
- {header.label}
165
- <div className="flex flex-col">
166
- {sortField === header.key ? (
167
- sortDirection === 'asc' ?
168
- <ArrowUp className="w-3 h-3 text-emerald-400" /> :
169
- <ArrowDown className="w-3 h-3 text-emerald-400" />
170
- ) : (
171
- <ArrowUpDown className="w-3 h-3 text-neutral-700 group-hover/th:text-neutral-500 transition-colors" />
172
- )}
173
- </div>
210
+ {header.label}
211
+ <div className="flex flex-col">
212
+ {sortField === header.key ? (
213
+ sortDirection === "asc" ? (
214
+ <ArrowUp className="w-3 h-3 text-emerald-400" />
215
+ ) : (
216
+ <ArrowDown className="w-3 h-3 text-emerald-400" />
217
+ )
218
+ ) : (
219
+ <ArrowUpDown className="w-3 h-3 text-neutral-700 group-hover/th:text-neutral-500 transition-colors" />
220
+ )}
221
+ </div>
174
222
  </div>
175
223
  </th>
176
224
  ))}
177
225
  </tr>
178
226
  </thead>
179
227
  <tbody>
180
- {loading && logs.length === 0 ? (
181
- // Loading skeleton
228
+ {loading && logs.length === 0
229
+ ? // Loading skeleton
182
230
  Array.from({ length: 5 }).map((_, i) => (
183
- <tr key={i} className="border-b border-white/5">
184
- <td className="py-4 px-4"><div className="h-4 w-32 bg-white/5 rounded animate-pulse" /></td>
185
- <td className="py-4 px-4"><div className="h-4 w-24 bg-white/5 rounded animate-pulse" /></td>
186
- <td className="py-4 px-4"><div className="h-4 w-16 bg-white/5 rounded animate-pulse" /></td>
187
- <td className="py-4 px-4"><div className="h-6 w-20 bg-white/5 rounded-full animate-pulse" /></td>
188
- <td className="py-4 px-4"><div className="h-4 w-40 bg-white/5 rounded animate-pulse" /></td>
189
- </tr>
231
+ <tr key={i} className="border-b border-white/5">
232
+ <td className="py-4 px-4">
233
+ <div className="h-4 w-32 bg-white/5 rounded animate-pulse" />
234
+ </td>
235
+ <td className="py-4 px-4">
236
+ <div className="h-4 w-24 bg-white/5 rounded animate-pulse" />
237
+ </td>
238
+ <td className="py-4 px-4">
239
+ <div className="h-4 w-16 bg-white/5 rounded animate-pulse" />
240
+ </td>
241
+ <td className="py-4 px-4">
242
+ <div className="h-4 w-40 bg-white/5 rounded animate-pulse" />
243
+ </td>
244
+ <td className="py-4 px-4">
245
+ <div className="h-6 w-20 bg-white/5 rounded-full animate-pulse" />
246
+ </td>
247
+ </tr>
190
248
  ))
191
- ) : (
192
- logs.map((log) => {
193
- const config = statusConfig[log.status] || statusConfig.Success
194
- const clientConf = getClientConfig(log.client)
195
- const displayName = clientConf ? clientConf.name : log.client
196
- const icon = clientConf?.icon
249
+ : logs.map((log) => {
250
+ const config =
251
+ statusConfig[log.status] || statusConfig.Success;
252
+ const clientConf = getClientConfig(log.client);
253
+ const displayName = clientConf
254
+ ? clientConf.name
255
+ : log.client;
256
+ const icon = clientConf?.icon;
197
257
 
198
258
  return (
199
- <tr key={log.id} className="border-b border-white/5 hover:bg-white/10 transition-colors even:bg-white/[0.02] group/row">
200
- <td className="py-4 px-4 text-neutral-300 group-hover/row:text-white transition-colors">{log.date}</td>
201
- <td className="py-4 px-4 text-white font-medium">
202
- <div className="flex items-center gap-2">
203
- {icon && <img src={icon} alt={displayName} className="w-5 h-5 object-contain" />}
204
- <span>{displayName}</span>
205
- </div>
206
- </td>
207
- <td className="py-4 px-4 text-neutral-300">{log.operation}</td>
208
- <td className="py-4 px-4">
209
- <span
210
- className={`px-3 py-1 rounded-full text-xs font-medium border ${config.bg} ${config.text} ${config.border}`}
211
- >
212
- {log.status}
213
- </span>
214
- </td>
215
- <td className="py-4 px-4 text-neutral-400">{log.description}</td>
216
- </tr>
217
- )
218
- })
219
- )}
259
+ <tr
260
+ key={log.id}
261
+ className="border-b border-white/5 hover:bg-white/10 transition-colors even:bg-white/[0.02] group/row"
262
+ >
263
+ <td className="py-4 px-3 text-neutral-300 group-hover/row:text-white transition-colors">
264
+ {log.date}
265
+ </td>
266
+ <td className="py-4 px-3 text-white font-medium">
267
+ <div className="flex items-center gap-2">
268
+ {icon && (
269
+ <img
270
+ src={icon}
271
+ alt={displayName}
272
+ className="w-5 h-5 object-contain"
273
+ />
274
+ )}
275
+ <span>{displayName}</span>
276
+ </div>
277
+ </td>
278
+ <td className="py-4 px-3 text-neutral-300">
279
+ {log.operation}
280
+ </td>
281
+ <td className="py-4 px-3 text-neutral-400">
282
+ {log.description}
283
+ </td>
284
+ <td className="py-4 px-3">
285
+ <span
286
+ className={`px-3 py-1 rounded-full text-xs font-medium border ${config.bg} ${config.text} ${config.border}`}
287
+ >
288
+ {log.status}
289
+ </span>
290
+ </td>
291
+ </tr>
292
+ );
293
+ })}
220
294
 
221
295
  {!loading && logs.length === 0 && (
222
296
  <tr>
223
- <td colSpan={5} className="py-12 text-center text-neutral-500">
297
+ <td
298
+ colSpan={5}
299
+ className="py-12 text-center text-neutral-500"
300
+ >
224
301
  No logs found for this period
225
302
  </td>
226
303
  </tr>
@@ -265,17 +342,19 @@ export default function AuditLogTable({
265
342
 
266
343
  <div className="flex gap-2">
267
344
  <button
268
- onClick={() => onPageChange(Math.max(1, currentPage - 1))}
269
- disabled={currentPage === 1}
270
- className="px-3 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
345
+ onClick={() => onPageChange(Math.max(1, currentPage - 1))}
346
+ disabled={currentPage === 1}
347
+ className="px-3 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
271
348
  >
272
349
  <ChevronLeft className="w-4 h-4" />
273
350
  Previous
274
351
  </button>
275
352
  <button
276
- onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
277
- disabled={currentPage === totalPages || totalPages === 0}
278
- className="px-3 py-2 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 text-emerald-400 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
353
+ onClick={() =>
354
+ onPageChange(Math.min(totalPages, currentPage + 1))
355
+ }
356
+ disabled={currentPage === totalPages || totalPages === 0}
357
+ className="px-3 py-2 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-500/20 text-emerald-400 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
279
358
  >
280
359
  Next
281
360
  <ChevronRight className="w-4 h-4" />
@@ -284,5 +363,5 @@ export default function AuditLogTable({
284
363
  </div>
285
364
  </div>
286
365
  </div>
287
- )
366
+ );
288
367
  }