@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.
- package/app/api/health/route.ts +60 -46
- package/components/dashboard/audit-log-table.tsx +209 -130
- package/components/dashboard/chart-card.tsx +151 -76
- package/components/dashboard/header.tsx +131 -50
- package/components/dashboard/mcp-config-modal.tsx +1 -1
- package/components/dashboard/metric-card.tsx +46 -10
- package/components/dashboard/metrics-chart.tsx +137 -110
- package/components/dashboard/metrics-grid.tsx +138 -73
- package/components/dashboard/settings-modal.tsx +519 -116
- package/e2e/ui-elements.spec.ts +267 -0
- package/package.json +1 -1
package/app/api/health/route.ts
CHANGED
|
@@ -1,94 +1,108 @@
|
|
|
1
|
-
import { checkRateLimit, rateLimitResponse } from
|
|
2
|
-
import { NextRequest, NextResponse } from
|
|
1
|
+
import { checkRateLimit, rateLimitResponse } from "@/lib/rate-limit";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
3
|
|
|
4
|
-
export const dynamic =
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
5
|
|
|
6
6
|
interface ServiceStatus {
|
|
7
|
-
name: string
|
|
8
|
-
status:
|
|
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:
|
|
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(
|
|
20
|
-
|
|
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:
|
|
31
|
+
cache: "no-store",
|
|
28
32
|
headers: {
|
|
29
|
-
|
|
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:
|
|
41
|
+
return { name, status: "ok", latencyMs };
|
|
38
42
|
}
|
|
39
43
|
return {
|
|
40
44
|
name,
|
|
41
|
-
status:
|
|
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:
|
|
49
|
-
message:
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
68
|
-
|
|
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(
|
|
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 ===
|
|
80
|
-
const hasWarning = services.some(s => s.status ===
|
|
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 ?
|
|
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
|
-
|
|
91
|
-
|
|
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 {
|
|
5
|
-
|
|
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:
|
|
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<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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) =>
|
|
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 = [
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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:
|
|
72
|
-
const url = URL.createObjectURL(blob)
|
|
73
|
-
const a = document.createElement(
|
|
74
|
-
a.href = url
|
|
75
|
-
a.download = `cybermem-audit-${new Date().toISOString().split(
|
|
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(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
91
|
-
const url = URL.createObjectURL(blob)
|
|
92
|
-
const a = document.createElement(
|
|
93
|
-
a.href = url
|
|
94
|
-
a.download = `cybermem-audit-${new Date().toISOString().split(
|
|
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
|
|
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 &&
|
|
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-[
|
|
153
|
-
{ label: "Client", key: "client", width: "w-[
|
|
154
|
-
{ label: "Operation", key: "operation", width: "w-[
|
|
155
|
-
{ label: "
|
|
156
|
-
{ label: "
|
|
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-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
228
|
+
{loading && logs.length === 0
|
|
229
|
+
? // Loading skeleton
|
|
182
230
|
Array.from({ length: 5 }).map((_, i) => (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
</
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
const clientConf = getClientConfig(log.client)
|
|
195
|
-
const displayName = clientConf
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
}
|