@cybermem/dashboard 0.5.10 → 0.5.14
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 +548 -114
- package/e2e/ui-elements.spec.ts +267 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
3
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
4
4
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
@@ -10,7 +10,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
10
10
|
const MetricsChart = dynamic(() => import("./metrics-chart"), { ssr: false });
|
|
11
11
|
|
|
12
12
|
interface ChartCardProps {
|
|
13
|
-
service: string
|
|
13
|
+
service: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Fallback color generator
|
|
@@ -19,10 +19,10 @@ function stringToColor(str: string): string {
|
|
|
19
19
|
for (let i = 0; i < str.length; i++) {
|
|
20
20
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
21
21
|
}
|
|
22
|
-
let color =
|
|
22
|
+
let color = "#";
|
|
23
23
|
for (let i = 0; i < 3; i++) {
|
|
24
|
-
const value = (hash >> (i * 8)) &
|
|
25
|
-
color += (
|
|
24
|
+
const value = (hash >> (i * 8)) & 0xff;
|
|
25
|
+
color += ("00" + value.toString(16)).substr(-2);
|
|
26
26
|
}
|
|
27
27
|
return color;
|
|
28
28
|
}
|
|
@@ -33,108 +33,133 @@ const periods = [
|
|
|
33
33
|
{ value: "7d", label: "7 Days" },
|
|
34
34
|
{ value: "30d", label: "30 Days" },
|
|
35
35
|
{ value: "90d", label: "90 Days" },
|
|
36
|
-
]
|
|
36
|
+
];
|
|
37
37
|
|
|
38
38
|
export default function ChartCard({ service }: ChartCardProps) {
|
|
39
|
-
const { strategy, refreshSignal, clientConfigs } = useDashboard()
|
|
40
|
-
const [period, setPeriod] = useState("24h")
|
|
41
|
-
const [hovered, setHovered] = useState<string | null>(null)
|
|
42
|
-
const [data, setData] = useState<any[]>([])
|
|
43
|
-
const [clientNames, setClientNames] = useState<string[]>([])
|
|
44
|
-
const [clientMetadata, setClientMetadata] = useState<Record<string, any>>({})
|
|
45
|
-
const [loading, setLoading] = useState(true)
|
|
46
|
-
|
|
47
|
-
const isInitialLoad = useRef(true)
|
|
39
|
+
const { strategy, refreshSignal, clientConfigs } = useDashboard();
|
|
40
|
+
const [period, setPeriod] = useState("24h");
|
|
41
|
+
const [hovered, setHovered] = useState<string | null>(null);
|
|
42
|
+
const [data, setData] = useState<any[]>([]);
|
|
43
|
+
const [clientNames, setClientNames] = useState<string[]>([]);
|
|
44
|
+
const [clientMetadata, setClientMetadata] = useState<Record<string, any>>({});
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
|
|
47
|
+
const isInitialLoad = useRef(true);
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
async function fetchData() {
|
|
50
50
|
// Only show loading state on initial load, not background refresh
|
|
51
|
-
if (isInitialLoad.current) setLoading(true)
|
|
51
|
+
if (isInitialLoad.current) setLoading(true);
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
|
-
const timeSeriesData = await strategy.getChartData(period)
|
|
54
|
+
const timeSeriesData = await strategy.getChartData(period);
|
|
55
55
|
|
|
56
56
|
// Update client metadata if provided in response
|
|
57
57
|
if (timeSeriesData.metadata) {
|
|
58
|
-
|
|
58
|
+
setClientMetadata((prev) => ({
|
|
59
|
+
...prev,
|
|
60
|
+
...timeSeriesData.metadata,
|
|
61
|
+
}));
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
// Helper to format time based on period
|
|
62
65
|
const formatSeries = (series: any[]) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const date = new Date((point.time as number) * 1000)
|
|
66
|
-
let timeStr = ""
|
|
66
|
+
if (!series) return [];
|
|
67
|
+
return series.map((point) => {
|
|
68
|
+
const date = new Date((point.time as number) * 1000);
|
|
69
|
+
let timeStr = "";
|
|
67
70
|
// Show date if period is longer than 24h
|
|
68
71
|
if (["7d", "30d", "90d", "all"].includes(period)) {
|
|
69
|
-
|
|
72
|
+
timeStr =
|
|
73
|
+
date.toLocaleDateString([], {
|
|
74
|
+
month: "2-digit",
|
|
75
|
+
day: "2-digit",
|
|
76
|
+
}) +
|
|
77
|
+
" " +
|
|
78
|
+
date.toLocaleTimeString([], {
|
|
79
|
+
hour: "2-digit",
|
|
80
|
+
minute: "2-digit",
|
|
81
|
+
});
|
|
70
82
|
} else {
|
|
71
|
-
|
|
83
|
+
timeStr = date.toLocaleTimeString([], {
|
|
84
|
+
hour: "2-digit",
|
|
85
|
+
minute: "2-digit",
|
|
86
|
+
});
|
|
72
87
|
}
|
|
73
88
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
}
|
|
89
|
+
...point,
|
|
90
|
+
time: timeStr,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
};
|
|
79
94
|
|
|
80
95
|
// Extract client names from series and sort by total value (Ascending)
|
|
81
96
|
const getClients = (series: any[]) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
if (!series || series.length === 0) return [];
|
|
98
|
+
const keys = new Set<string>();
|
|
99
|
+
const totals: Record<string, number> = {};
|
|
100
|
+
|
|
101
|
+
series.forEach((point) => {
|
|
102
|
+
Object.keys(point).forEach((k) => {
|
|
103
|
+
if (k !== "time") {
|
|
104
|
+
keys.add(k);
|
|
105
|
+
totals[k] = (totals[k] || 0) + (point[k] as number);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return Array.from(keys).sort(
|
|
111
|
+
(a, b) => (totals[a] || 0) - (totals[b] || 0),
|
|
112
|
+
);
|
|
113
|
+
};
|
|
97
114
|
|
|
98
115
|
// Get data based on service type
|
|
99
|
-
let seriesData: any[] = []
|
|
100
|
-
let clients: string[] = []
|
|
116
|
+
let seriesData: any[] = [];
|
|
117
|
+
let clients: string[] = [];
|
|
101
118
|
|
|
102
119
|
if (service.includes("Creates")) {
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
seriesData = formatSeries(timeSeriesData.creates);
|
|
121
|
+
clients = getClients(timeSeriesData.creates);
|
|
105
122
|
} else if (service.includes("Reads")) {
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
seriesData = formatSeries(timeSeriesData.reads);
|
|
124
|
+
clients = getClients(timeSeriesData.reads);
|
|
108
125
|
} else if (service.includes("Updates")) {
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
seriesData = formatSeries(timeSeriesData.updates);
|
|
127
|
+
clients = getClients(timeSeriesData.updates);
|
|
111
128
|
} else if (service.includes("Deletes")) {
|
|
112
|
-
|
|
113
|
-
|
|
129
|
+
seriesData = formatSeries(timeSeriesData.deletes);
|
|
130
|
+
clients = getClients(timeSeriesData.deletes);
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
setData(seriesData)
|
|
117
|
-
setClientNames(clients)
|
|
118
|
-
|
|
133
|
+
setData(seriesData);
|
|
134
|
+
setClientNames(clients);
|
|
119
135
|
} catch (e) {
|
|
120
|
-
console.error("Failed to fetch chart data:", e)
|
|
136
|
+
console.error("Failed to fetch chart data:", e);
|
|
121
137
|
} finally {
|
|
122
|
-
setLoading(false)
|
|
123
|
-
isInitialLoad.current = false
|
|
138
|
+
setLoading(false);
|
|
139
|
+
isInitialLoad.current = false;
|
|
124
140
|
}
|
|
125
141
|
}
|
|
126
|
-
fetchData()
|
|
127
|
-
}, [period, service, strategy, refreshSignal])
|
|
142
|
+
fetchData();
|
|
143
|
+
}, [period, service, strategy, refreshSignal]);
|
|
128
144
|
|
|
129
|
-
const isMultiSeries = clientNames.length > 0
|
|
145
|
+
const isMultiSeries = clientNames.length > 0;
|
|
130
146
|
|
|
131
147
|
return (
|
|
132
148
|
<Card className="bg-white/5 border-white/10 backdrop-blur-md relative overflow-visible pt-6 pb-2">
|
|
133
149
|
<button
|
|
134
150
|
className="absolute top-0 right-0 z-20 h-8 px-3 rounded-tl-none rounded-tr-xl 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 group"
|
|
135
|
-
onClick={() =>
|
|
151
|
+
onClick={() =>
|
|
152
|
+
document
|
|
153
|
+
.getElementById(`dropdown-${service}`)
|
|
154
|
+
?.classList.toggle("hidden")
|
|
155
|
+
}
|
|
136
156
|
>
|
|
137
|
-
<svg
|
|
157
|
+
<svg
|
|
158
|
+
className="w-3 h-3"
|
|
159
|
+
fill="none"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
viewBox="0 0 24 24"
|
|
162
|
+
>
|
|
138
163
|
<path
|
|
139
164
|
strokeLinecap="round"
|
|
140
165
|
strokeLinejoin="round"
|
|
@@ -147,13 +172,18 @@ export default function ChartCard({ service }: ChartCardProps) {
|
|
|
147
172
|
</button>
|
|
148
173
|
|
|
149
174
|
{/* Dropdown Menu - Positioned relative to the button or card */}
|
|
150
|
-
<div
|
|
175
|
+
<div
|
|
176
|
+
id={`dropdown-${service}`}
|
|
177
|
+
className="hidden absolute top-8 right-0 w-40 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl z-30 backdrop-blur-xl overflow-hidden"
|
|
178
|
+
>
|
|
151
179
|
{periods.map((p) => (
|
|
152
180
|
<button
|
|
153
181
|
key={p.value}
|
|
154
182
|
onClick={() => {
|
|
155
183
|
setPeriod(p.value);
|
|
156
|
-
document
|
|
184
|
+
document
|
|
185
|
+
.getElementById(`dropdown-${service}`)
|
|
186
|
+
?.classList.add("hidden");
|
|
157
187
|
}}
|
|
158
188
|
className={`w-full text-left px-3 py-2 text-xs transition-colors ${
|
|
159
189
|
period === p.value
|
|
@@ -169,29 +199,74 @@ export default function ChartCard({ service }: ChartCardProps) {
|
|
|
169
199
|
<CardHeader className="relative">
|
|
170
200
|
<div className="flex items-center justify-between">
|
|
171
201
|
<div>
|
|
172
|
-
<CardTitle className="text-sm font-medium text-slate-400">
|
|
202
|
+
<CardTitle className="text-sm font-medium text-slate-400">
|
|
203
|
+
Time Series
|
|
204
|
+
</CardTitle>
|
|
173
205
|
<div className="text-2xl font-bold text-white">{service}</div>
|
|
174
206
|
</div>
|
|
175
207
|
</div>
|
|
176
208
|
</CardHeader>
|
|
177
209
|
<CardContent>
|
|
178
210
|
{loading ? (
|
|
211
|
+
<div className="h-[200px] w-full flex flex-col justify-end p-4">
|
|
212
|
+
{/* Skeleton chart bars */}
|
|
213
|
+
<div className="flex items-end justify-around h-full gap-2">
|
|
214
|
+
<div
|
|
215
|
+
className="w-8 bg-white/5 rounded-t animate-pulse"
|
|
216
|
+
style={{ height: "40%" }}
|
|
217
|
+
/>
|
|
218
|
+
<div
|
|
219
|
+
className="w-8 bg-white/5 rounded-t animate-pulse"
|
|
220
|
+
style={{ height: "65%" }}
|
|
221
|
+
/>
|
|
222
|
+
<div
|
|
223
|
+
className="w-8 bg-white/5 rounded-t animate-pulse"
|
|
224
|
+
style={{ height: "30%" }}
|
|
225
|
+
/>
|
|
226
|
+
<div
|
|
227
|
+
className="w-8 bg-white/5 rounded-t animate-pulse"
|
|
228
|
+
style={{ height: "80%" }}
|
|
229
|
+
/>
|
|
230
|
+
<div
|
|
231
|
+
className="w-8 bg-white/5 rounded-t animate-pulse"
|
|
232
|
+
style={{ height: "50%" }}
|
|
233
|
+
/>
|
|
234
|
+
<div
|
|
235
|
+
className="w-8 bg-white/5 rounded-t animate-pulse"
|
|
236
|
+
style={{ height: "70%" }}
|
|
237
|
+
/>
|
|
238
|
+
<div
|
|
239
|
+
className="w-8 bg-white/5 rounded-t animate-pulse"
|
|
240
|
+
style={{ height: "45%" }}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
{/* Skeleton axis */}
|
|
244
|
+
<div className="h-px w-full bg-white/10 mt-2" />
|
|
245
|
+
</div>
|
|
246
|
+
) : data.length === 0 || clientNames.length === 0 ? (
|
|
179
247
|
<div className="h-[200px] w-full flex items-center justify-center">
|
|
180
|
-
<div className="text-
|
|
248
|
+
<div className="text-center">
|
|
249
|
+
<div className="text-neutral-500 text-sm">
|
|
250
|
+
No data for this period
|
|
251
|
+
</div>
|
|
252
|
+
<div className="text-neutral-600 text-xs mt-1">
|
|
253
|
+
Try selecting a different time range
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
181
256
|
</div>
|
|
182
257
|
) : (
|
|
183
258
|
<div className="h-[200px] w-full">
|
|
184
259
|
<MetricsChart
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
260
|
+
data={data}
|
|
261
|
+
isMultiSeries={isMultiSeries}
|
|
262
|
+
clientNames={clientNames}
|
|
263
|
+
clientConfigs={clientConfigs}
|
|
264
|
+
hovered={hovered}
|
|
265
|
+
setHovered={setHovered}
|
|
191
266
|
/>
|
|
192
267
|
</div>
|
|
193
268
|
)}
|
|
194
269
|
</CardContent>
|
|
195
270
|
</Card>
|
|
196
|
-
)
|
|
271
|
+
);
|
|
197
272
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AlertCircle,
|
|
7
|
+
Book,
|
|
8
|
+
CheckCircle2,
|
|
9
|
+
Loader2,
|
|
10
|
+
Settings,
|
|
11
|
+
XCircle,
|
|
12
|
+
} from "lucide-react";
|
|
6
13
|
import Image from "next/image";
|
|
7
14
|
import { useEffect, useState } from "react";
|
|
8
15
|
|
|
@@ -13,53 +20,87 @@ export default function DashboardHeader({
|
|
|
13
20
|
onShowMCPConfig: () => void;
|
|
14
21
|
onShowSettings: () => void;
|
|
15
22
|
}) {
|
|
16
|
-
const [isScrolled, setIsScrolled] = useState(false)
|
|
17
|
-
const [showHealthPopup, setShowHealthPopup] = useState(false)
|
|
18
|
-
const { systemHealth } = useDashboard()
|
|
23
|
+
const [isScrolled, setIsScrolled] = useState(false);
|
|
24
|
+
const [showHealthPopup, setShowHealthPopup] = useState(false);
|
|
25
|
+
const { systemHealth } = useDashboard();
|
|
19
26
|
|
|
20
27
|
useEffect(() => {
|
|
21
28
|
// Check initial scroll position on mount
|
|
22
|
-
setIsScrolled(window.scrollY > 10)
|
|
29
|
+
setIsScrolled(window.scrollY > 10);
|
|
23
30
|
|
|
24
31
|
const handleScroll = () => {
|
|
25
|
-
setIsScrolled(window.scrollY > 10)
|
|
26
|
-
}
|
|
27
|
-
window.addEventListener("scroll", handleScroll)
|
|
28
|
-
return () => window.removeEventListener("scroll", handleScroll)
|
|
29
|
-
}, [])
|
|
32
|
+
setIsScrolled(window.scrollY > 10);
|
|
33
|
+
};
|
|
34
|
+
window.addEventListener("scroll", handleScroll);
|
|
35
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
36
|
+
}, []);
|
|
30
37
|
|
|
31
38
|
const getStatusConfig = () => {
|
|
32
39
|
if (!systemHealth) {
|
|
33
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
bg: "bg-neutral-500/10",
|
|
42
|
+
text: "text-neutral-400",
|
|
43
|
+
border: "border-neutral-500/20",
|
|
44
|
+
icon: Loader2,
|
|
45
|
+
label: "Checking...",
|
|
46
|
+
};
|
|
34
47
|
}
|
|
35
48
|
switch (systemHealth.overall) {
|
|
36
|
-
case
|
|
37
|
-
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
case "ok":
|
|
50
|
+
return {
|
|
51
|
+
bg: "bg-emerald-500/10",
|
|
52
|
+
text: "text-emerald-400",
|
|
53
|
+
border: "border-emerald-500/20",
|
|
54
|
+
icon: CheckCircle2,
|
|
55
|
+
label: "All Systems OK",
|
|
56
|
+
};
|
|
57
|
+
case "degraded":
|
|
58
|
+
return {
|
|
59
|
+
bg: "bg-amber-500/10",
|
|
60
|
+
text: "text-amber-400",
|
|
61
|
+
border: "border-amber-500/20",
|
|
62
|
+
icon: AlertCircle,
|
|
63
|
+
label: "Degraded",
|
|
64
|
+
};
|
|
65
|
+
case "error":
|
|
66
|
+
return {
|
|
67
|
+
bg: "bg-red-500/10",
|
|
68
|
+
text: "text-red-400",
|
|
69
|
+
border: "border-red-500/20",
|
|
70
|
+
icon: XCircle,
|
|
71
|
+
label: "System Error",
|
|
72
|
+
};
|
|
42
73
|
}
|
|
43
|
-
}
|
|
74
|
+
};
|
|
44
75
|
|
|
45
|
-
const statusConfig = getStatusConfig()
|
|
46
|
-
const StatusIcon = statusConfig.icon
|
|
76
|
+
const statusConfig = getStatusConfig();
|
|
77
|
+
const StatusIcon = statusConfig.icon;
|
|
47
78
|
|
|
48
79
|
return (
|
|
49
|
-
<header
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
<header
|
|
81
|
+
className={`sticky top-0 z-50 transition-all duration-300 ${
|
|
82
|
+
isScrolled
|
|
83
|
+
? "border-b border-white/10 backdrop-blur-xl bg-neutral-900/30"
|
|
84
|
+
: "border-b border-transparent bg-transparent"
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
54
87
|
<div className="px-6 py-5 max-w-7xl mx-auto">
|
|
55
88
|
<div className="flex items-center justify-between">
|
|
56
89
|
<div className="flex items-center gap-4">
|
|
57
90
|
<div className="relative w-10 h-10 flex-shrink-0">
|
|
58
|
-
<Image
|
|
91
|
+
<Image
|
|
92
|
+
src="/logo.svg"
|
|
93
|
+
alt="CyberMem Logo"
|
|
94
|
+
width={40}
|
|
95
|
+
height={40}
|
|
96
|
+
className="object-contain"
|
|
97
|
+
/>
|
|
59
98
|
</div>
|
|
60
99
|
<div>
|
|
61
100
|
<div className="flex items-center gap-3">
|
|
62
|
-
<h1 className="text-3xl font-bold tracking-tight text-white leading-none font-exo">
|
|
101
|
+
<h1 className="text-3xl font-bold tracking-tight text-white leading-none font-exo">
|
|
102
|
+
CyberMem
|
|
103
|
+
</h1>
|
|
63
104
|
|
|
64
105
|
{/* System Health Status Badge with Hover Popup */}
|
|
65
106
|
<div
|
|
@@ -67,29 +108,55 @@ export default function DashboardHeader({
|
|
|
67
108
|
onMouseEnter={() => setShowHealthPopup(true)}
|
|
68
109
|
onMouseLeave={() => setShowHealthPopup(false)}
|
|
69
110
|
>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
111
|
+
{!systemHealth ? (
|
|
112
|
+
/* Shimmer loading state */
|
|
113
|
+
<div className="px-3 py-[2px] rounded-full bg-white/5 border border-white/10 animate-pulse">
|
|
114
|
+
<div className="flex items-center gap-1">
|
|
115
|
+
<div className="w-3 h-3 rounded-full bg-white/10" />
|
|
116
|
+
<div className="w-16 h-3 rounded bg-white/10" />
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<div
|
|
121
|
+
className={`px-2 py-[2px] rounded-full text-[10px] font-medium flex items-center gap-1 cursor-pointer ${statusConfig.bg} ${statusConfig.text} border ${statusConfig.border}`}
|
|
122
|
+
>
|
|
123
|
+
<StatusIcon className="w-3 h-3" />
|
|
124
|
+
{statusConfig.label}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
74
127
|
|
|
75
128
|
{/* Hover Popup */}
|
|
76
129
|
{showHealthPopup && systemHealth && (
|
|
77
130
|
<div className="absolute top-full left-0 mt-2 w-64 bg-[#0B1116]/95 border border-white/10 rounded-lg shadow-xl z-50 backdrop-blur-xl overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
|
|
78
131
|
<div className="p-3 border-b border-white/5">
|
|
79
|
-
<p className="text-xs text-neutral-400">
|
|
80
|
-
|
|
132
|
+
<p className="text-xs text-neutral-400">
|
|
133
|
+
System Health
|
|
134
|
+
</p>
|
|
135
|
+
<p className="text-[10px] text-neutral-500 mt-0.5">
|
|
136
|
+
Updated:{" "}
|
|
137
|
+
{new Date(
|
|
138
|
+
systemHealth.timestamp,
|
|
139
|
+
).toLocaleTimeString()}
|
|
140
|
+
</p>
|
|
81
141
|
</div>
|
|
82
142
|
<div className="p-2 space-y-1">
|
|
83
143
|
{systemHealth.services.map((service, i) => (
|
|
84
|
-
<div
|
|
85
|
-
|
|
144
|
+
<div
|
|
145
|
+
key={i}
|
|
146
|
+
className="flex items-center justify-between px-2 py-1.5 rounded hover:bg-white/5"
|
|
147
|
+
>
|
|
148
|
+
<span className="text-xs text-neutral-300">
|
|
149
|
+
{service.name}
|
|
150
|
+
</span>
|
|
86
151
|
<div className="flex items-center gap-2">
|
|
87
152
|
{service.latencyMs && (
|
|
88
|
-
<span className="text-[10px] text-neutral-500">
|
|
153
|
+
<span className="text-[10px] text-neutral-500">
|
|
154
|
+
{service.latencyMs}ms
|
|
155
|
+
</span>
|
|
89
156
|
)}
|
|
90
|
-
{service.status ===
|
|
157
|
+
{service.status === "ok" ? (
|
|
91
158
|
<CheckCircle2 className="w-3 h-3 text-emerald-400" />
|
|
92
|
-
) : service.status ===
|
|
159
|
+
) : service.status === "warning" ? (
|
|
93
160
|
<AlertCircle className="w-3 h-3 text-amber-400" />
|
|
94
161
|
) : (
|
|
95
162
|
<XCircle className="w-3 h-3 text-red-400" />
|
|
@@ -97,12 +164,23 @@ export default function DashboardHeader({
|
|
|
97
164
|
</div>
|
|
98
165
|
</div>
|
|
99
166
|
))}
|
|
100
|
-
{systemHealth.services.some(
|
|
167
|
+
{systemHealth.services.some(
|
|
168
|
+
(s) => s.status !== "ok" && s.message,
|
|
169
|
+
) && (
|
|
101
170
|
<div className="mt-2 p-2 rounded bg-red-500/10 border border-red-500/20">
|
|
102
|
-
<p className="text-xs text-red-300 font-medium">
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
171
|
+
<p className="text-xs text-red-300 font-medium">
|
|
172
|
+
Issues:
|
|
173
|
+
</p>
|
|
174
|
+
{systemHealth.services
|
|
175
|
+
.filter((s) => s.status !== "ok" && s.message)
|
|
176
|
+
.map((s, i) => (
|
|
177
|
+
<p
|
|
178
|
+
key={i}
|
|
179
|
+
className="text-[10px] text-red-400 mt-1"
|
|
180
|
+
>
|
|
181
|
+
• {s.name}: {s.message}
|
|
182
|
+
</p>
|
|
183
|
+
))}
|
|
106
184
|
</div>
|
|
107
185
|
)}
|
|
108
186
|
</div>
|
|
@@ -112,19 +190,22 @@ export default function DashboardHeader({
|
|
|
112
190
|
</div>
|
|
113
191
|
<p className="text-sm text-neutral-400 mt-1">Memory MCP Server</p>
|
|
114
192
|
</div>
|
|
115
|
-
|
|
116
193
|
</div>
|
|
117
194
|
|
|
118
195
|
<div className="flex items-center gap-3">
|
|
119
|
-
|
|
120
|
-
|
|
121
196
|
<Button
|
|
122
197
|
variant="ghost"
|
|
123
198
|
size="sm"
|
|
124
199
|
onClick={onShowMCPConfig}
|
|
125
200
|
className="hidden md:flex h-10 px-4 text-sm font-medium bg-emerald-500/10 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 border border-emerald-500/20 hover:border-emerald-500/40 rounded-lg"
|
|
126
201
|
>
|
|
127
|
-
<Image
|
|
202
|
+
<Image
|
|
203
|
+
src="/icons/mcp.png"
|
|
204
|
+
alt="MCP"
|
|
205
|
+
width={16}
|
|
206
|
+
height={16}
|
|
207
|
+
className="mr-2"
|
|
208
|
+
/>
|
|
128
209
|
Connect MCP
|
|
129
210
|
</Button>
|
|
130
211
|
|
|
@@ -134,7 +215,7 @@ export default function DashboardHeader({
|
|
|
134
215
|
asChild
|
|
135
216
|
className="hidden md:flex h-10 px-4 text-sm font-medium text-neutral-400 hover:text-white bg-white/5 border border-white/10 hover:bg-white/10 rounded-lg"
|
|
136
217
|
>
|
|
137
|
-
<a href="https://cybermem.dev
|
|
218
|
+
<a href="https://docs.cybermem.dev" target="_blank">
|
|
138
219
|
<Book className="w-4 h-4 mr-2" />
|
|
139
220
|
Docs
|
|
140
221
|
</a>
|
|
@@ -152,5 +233,5 @@ export default function DashboardHeader({
|
|
|
152
233
|
</div>
|
|
153
234
|
</div>
|
|
154
235
|
</header>
|
|
155
|
-
)
|
|
236
|
+
);
|
|
156
237
|
}
|
|
@@ -423,7 +423,7 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
423
423
|
variant="ghost"
|
|
424
424
|
className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 hover:text-emerald-300 mr-auto"
|
|
425
425
|
>
|
|
426
|
-
<a href="https://cybermem.dev
|
|
426
|
+
<a href="https://docs.cybermem.dev" target="_blank">Read Documentation</a>
|
|
427
427
|
</Button>
|
|
428
428
|
<Button
|
|
429
429
|
onClick={onClose}
|