@cybermem/dashboard 0.9.12 → 0.13.4
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/Dockerfile +3 -3
- package/app/api/audit-logs/route.ts +12 -6
- package/app/api/health/route.ts +2 -1
- package/app/api/mcp-config/route.ts +128 -0
- package/app/api/metrics/route.ts +22 -70
- package/app/api/settings/route.ts +125 -30
- package/app/page.tsx +105 -127
- package/components/dashboard/{chart-card.tsx → charts/chart-card.tsx} +13 -19
- package/components/dashboard/{metrics-chart.tsx → charts/memory-chart.tsx} +1 -1
- package/components/dashboard/charts-section.tsx +3 -3
- package/components/dashboard/header.tsx +177 -176
- package/components/dashboard/{audit-log-table.tsx → logs/log-viewer.tsx} +12 -7
- package/components/dashboard/mcp/config-preview.tsx +246 -0
- package/components/dashboard/mcp/platform-selector.tsx +96 -0
- package/components/dashboard/mcp-config-modal.tsx +97 -503
- package/components/dashboard/{metric-card.tsx → metrics/stat-card.tsx} +4 -2
- package/components/dashboard/metrics-grid.tsx +10 -2
- package/components/dashboard/settings/access-token-section.tsx +131 -0
- package/components/dashboard/settings/data-management-section.tsx +122 -0
- package/components/dashboard/settings/system-info-section.tsx +98 -0
- package/components/dashboard/settings-modal.tsx +55 -299
- package/e2e/api.spec.ts +219 -0
- package/e2e/routing.spec.ts +39 -0
- package/e2e/ui.spec.ts +373 -0
- package/lib/data/dashboard-context.tsx +96 -29
- package/lib/data/types.ts +32 -38
- package/middleware.ts +31 -13
- package/package.json +6 -1
- package/playwright.config.ts +23 -58
- package/public/clients.json +5 -3
- package/release-reports/assets/local/1_dashboard.png +0 -0
- package/release-reports/assets/local/2_audit_logs.png +0 -0
- package/release-reports/assets/local/3_charts.png +0 -0
- package/release-reports/assets/local/4_mcp_modal.png +0 -0
- package/release-reports/assets/local/5_settings_modal.png +0 -0
- package/lib/data/demo-strategy.ts +0 -110
- package/lib/data/production-strategy.ts +0 -191
- package/lib/prometheus/client.ts +0 -58
- package/lib/prometheus/index.ts +0 -6
- package/lib/prometheus/metrics.ts +0 -234
- package/lib/prometheus/sparklines.ts +0 -71
- package/lib/prometheus/timeseries.ts +0 -305
- package/lib/prometheus/utils.ts +0 -176
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Check, Copy, Eye, EyeOff, Terminal } from "lucide-react";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
|
|
8
|
+
interface ConfigPreviewProps {
|
|
9
|
+
selectedConfig: any | undefined;
|
|
10
|
+
configContent: string;
|
|
11
|
+
configType: string;
|
|
12
|
+
isManaged: boolean;
|
|
13
|
+
isKeyVisible: boolean;
|
|
14
|
+
onToggleKeyVisibility: () => void;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ConfigPreview({
|
|
19
|
+
selectedConfig,
|
|
20
|
+
configContent,
|
|
21
|
+
configType,
|
|
22
|
+
isManaged,
|
|
23
|
+
isKeyVisible,
|
|
24
|
+
onToggleKeyVisibility,
|
|
25
|
+
onClose,
|
|
26
|
+
}: ConfigPreviewProps) {
|
|
27
|
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const copyToClipboard = (text: string, id: string) => {
|
|
30
|
+
navigator.clipboard.writeText(text);
|
|
31
|
+
setCopiedId(id);
|
|
32
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const highlightJSON = (jsonStr: string) => {
|
|
36
|
+
try {
|
|
37
|
+
const obj = JSON.parse(jsonStr);
|
|
38
|
+
const json = JSON.stringify(obj, null, 2);
|
|
39
|
+
return json.replace(
|
|
40
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
41
|
+
(match) => {
|
|
42
|
+
let cls = "text-orange-300";
|
|
43
|
+
if (/^"/.test(match)) {
|
|
44
|
+
if (/:$/.test(match)) {
|
|
45
|
+
cls = "text-emerald-300";
|
|
46
|
+
} else {
|
|
47
|
+
cls = "text-yellow-200";
|
|
48
|
+
}
|
|
49
|
+
} else if (/true|false/.test(match)) {
|
|
50
|
+
cls = "text-blue-300";
|
|
51
|
+
} else if (/null/.test(match)) {
|
|
52
|
+
cls = "text-gray-400";
|
|
53
|
+
}
|
|
54
|
+
return `<span class="${cls}">${match}</span>`;
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return jsonStr;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!selectedConfig) return null;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex-1 flex flex-col bg-white/[0.032]">
|
|
66
|
+
<div className="flex-1 overflow-y-auto overscroll-none p-8 space-y-4">
|
|
67
|
+
{/* Client Header */}
|
|
68
|
+
<div className="flex items-center gap-4">
|
|
69
|
+
{selectedConfig?.icon && (
|
|
70
|
+
<div className="w-14 h-14 flex-shrink-0 relative overflow-hidden">
|
|
71
|
+
<Image
|
|
72
|
+
src={selectedConfig.icon}
|
|
73
|
+
alt={selectedConfig.name}
|
|
74
|
+
fill
|
|
75
|
+
className={
|
|
76
|
+
[
|
|
77
|
+
"claude-desktop",
|
|
78
|
+
"claude-code",
|
|
79
|
+
"chatgpt",
|
|
80
|
+
"gemini-cli",
|
|
81
|
+
"perplexity",
|
|
82
|
+
].includes(selectedConfig.id)
|
|
83
|
+
? "object-cover"
|
|
84
|
+
: "object-contain"
|
|
85
|
+
}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
<div>
|
|
90
|
+
<h3 className="text-2xl font-bold text-white">
|
|
91
|
+
{selectedConfig?.name}
|
|
92
|
+
</h3>
|
|
93
|
+
<p className="text-sm text-neutral-400 uppercase tracking-wider">
|
|
94
|
+
Integrate MCP Client
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="space-y-0 pt-2">
|
|
100
|
+
{selectedConfig?.steps?.map((step: string, index: number) => (
|
|
101
|
+
<div
|
|
102
|
+
key={index}
|
|
103
|
+
className="relative flex items-start gap-4 pb-4 group/step"
|
|
104
|
+
>
|
|
105
|
+
{/* Vertical Line */}
|
|
106
|
+
{index < (selectedConfig?.steps?.length || 0) - 1 && (
|
|
107
|
+
<div className="absolute top-6 bottom-0 left-[11.5px] w-px bg-emerald-500/20" />
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{/* Step Number */}
|
|
111
|
+
<div className="flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/20 border border-emerald-500/50 text-emerald-400 text-xs font-bold z-10">
|
|
112
|
+
{index + 1}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Step Text */}
|
|
116
|
+
<div
|
|
117
|
+
className="flex-1 text-sm text-neutral-300 leading-relaxed"
|
|
118
|
+
dangerouslySetInnerHTML={{
|
|
119
|
+
__html: step
|
|
120
|
+
.replace(
|
|
121
|
+
/\*\*(.+?)\*\*/g,
|
|
122
|
+
'<strong class="text-white font-semibold">$1</strong>',
|
|
123
|
+
)
|
|
124
|
+
.replace(
|
|
125
|
+
/`(.+?)`/g,
|
|
126
|
+
'<code class="px-1.5 py-0.5 rounded bg-white/10 text-emerald-400 font-mono text-xs">$1</code>',
|
|
127
|
+
),
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Config Code Block */}
|
|
135
|
+
<div className="relative group w-full overflow-hidden border border-white/10 rounded-lg shadow-[0_20px_50px_rgba(0,0,0,0.5)]">
|
|
136
|
+
<div className="relative rounded-t-lg bg-[#05100F] px-4 py-4 flex items-center justify-between border-b border-transparent">
|
|
137
|
+
<div className="flex items-center gap-4">
|
|
138
|
+
<div className="text-sm text-white font-semibold pl-2">
|
|
139
|
+
{(() => {
|
|
140
|
+
if (["claude-code", "gemini-cli"].includes(selectedConfig.id))
|
|
141
|
+
return "Terminal";
|
|
142
|
+
if (selectedConfig.filename) return selectedConfig.filename;
|
|
143
|
+
if (configType === "command" || configType === "cmd")
|
|
144
|
+
return "Terminal";
|
|
145
|
+
return "mcp.json";
|
|
146
|
+
})()}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
{!isManaged && (
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={onToggleKeyVisibility}
|
|
155
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all text-xs font-medium text-neutral-300 hover:text-white"
|
|
156
|
+
title="Unhide Token"
|
|
157
|
+
>
|
|
158
|
+
{isKeyVisible ? (
|
|
159
|
+
<EyeOff className="w-3.5 h-3.5" />
|
|
160
|
+
) : (
|
|
161
|
+
<Eye className="w-3.5 h-3.5" />
|
|
162
|
+
)}
|
|
163
|
+
<span>Unhide Token</span>
|
|
164
|
+
<div
|
|
165
|
+
className={`relative inline-flex h-3.5 w-6.5 items-center rounded-full transition-colors ml-1 ${
|
|
166
|
+
isKeyVisible ? "bg-emerald-500" : "bg-neutral-600"
|
|
167
|
+
}`}
|
|
168
|
+
>
|
|
169
|
+
<span
|
|
170
|
+
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white transition-transform ${
|
|
171
|
+
isKeyVisible ? "translate-x-3.5" : "translate-x-0.5"
|
|
172
|
+
}`}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
</button>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<button
|
|
179
|
+
onClick={() => copyToClipboard(configContent, "config")}
|
|
180
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-all text-xs font-medium text-neutral-300 hover:text-white"
|
|
181
|
+
title="Copy"
|
|
182
|
+
>
|
|
183
|
+
{copiedId === "config" ? (
|
|
184
|
+
<>
|
|
185
|
+
<Check className="w-3.5 h-3.5 text-emerald-400" />
|
|
186
|
+
<span className="text-emerald-400">Copied</span>
|
|
187
|
+
</>
|
|
188
|
+
) : (
|
|
189
|
+
<>
|
|
190
|
+
<Copy className="w-3.5 h-3.5" />
|
|
191
|
+
<span>Copy</span>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div className="relative">
|
|
199
|
+
<div
|
|
200
|
+
className={`relative rounded-b-lg bg-[#05100F] font-mono text-xs md:text-sm text-white overflow-x-auto ${
|
|
201
|
+
configType === "command" || configType === "cmd"
|
|
202
|
+
? "px-4 py-3"
|
|
203
|
+
: "pl-5 py-5 pr-5"
|
|
204
|
+
}`}
|
|
205
|
+
>
|
|
206
|
+
<pre className="text-shadow-sm">
|
|
207
|
+
{configType === "json" ? (
|
|
208
|
+
<code
|
|
209
|
+
dangerouslySetInnerHTML={{
|
|
210
|
+
__html: highlightJSON(configContent),
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
) : (
|
|
214
|
+
<code className="whitespace-pre-wrap break-all flex items-start gap-2">
|
|
215
|
+
{(configType === "command" || configType === "cmd") && (
|
|
216
|
+
<Terminal className="w-4 h-4 mt-0.5 flex-shrink-0 text-emerald-400" />
|
|
217
|
+
)}
|
|
218
|
+
<span>{configContent}</span>
|
|
219
|
+
</code>
|
|
220
|
+
)}
|
|
221
|
+
</pre>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div className="border-t-[0.5px] border-white/10 px-8 py-4 flex justify-end gap-3 flex-none">
|
|
228
|
+
<Button
|
|
229
|
+
asChild
|
|
230
|
+
variant="ghost"
|
|
231
|
+
className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 hover:text-emerald-300 mr-auto"
|
|
232
|
+
>
|
|
233
|
+
<a href="https://docs.cybermem.dev" target="_blank">
|
|
234
|
+
Read Documentation
|
|
235
|
+
</a>
|
|
236
|
+
</Button>
|
|
237
|
+
<Button
|
|
238
|
+
onClick={onClose}
|
|
239
|
+
className="bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 transition-colors"
|
|
240
|
+
>
|
|
241
|
+
Close
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
|
|
5
|
+
interface PlatformSelectorProps {
|
|
6
|
+
clients: any[];
|
|
7
|
+
selectedClient: string;
|
|
8
|
+
onSelect: (id: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function PlatformSelector({
|
|
12
|
+
clients,
|
|
13
|
+
selectedClient,
|
|
14
|
+
onSelect,
|
|
15
|
+
}: PlatformSelectorProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="w-80 border-r-[0.5px] border-white/10 flex-none overflow-y-auto overscroll-none bg-white/[0.015] rounded-l-3xl">
|
|
18
|
+
<div className="p-6">
|
|
19
|
+
<div className="flex items-center gap-2 mb-3">
|
|
20
|
+
<div className="h-2 w-2 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
|
|
21
|
+
<h3 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider">
|
|
22
|
+
Clients
|
|
23
|
+
</h3>
|
|
24
|
+
</div>
|
|
25
|
+
<h2 className="text-2xl font-bold text-white mb-6">Integrations</h2>
|
|
26
|
+
<div className="space-y-2">
|
|
27
|
+
{clients.map((client) => (
|
|
28
|
+
<button
|
|
29
|
+
key={client.id}
|
|
30
|
+
onClick={() => onSelect(client.id)}
|
|
31
|
+
className={`
|
|
32
|
+
w-full flex items-center gap-4 px-4 py-3.5 rounded-xl transition-all duration-200 relative
|
|
33
|
+
${
|
|
34
|
+
selectedClient === client.id
|
|
35
|
+
? "bg-white/[0.03] border-[0.5px] border-white/[0.05]"
|
|
36
|
+
: "hover:bg-white/5"
|
|
37
|
+
}
|
|
38
|
+
`}
|
|
39
|
+
>
|
|
40
|
+
{selectedClient === client.id && (
|
|
41
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-5 bg-emerald-500 rounded-full shadow-[0_0_8px_rgba(16,185,129,0.6)]" />
|
|
42
|
+
)}
|
|
43
|
+
{client.icon ? (
|
|
44
|
+
<div
|
|
45
|
+
className={`w-10 h-10 flex-shrink-0 relative overflow-hidden rounded-lg border border-white/5 ${
|
|
46
|
+
[
|
|
47
|
+
"claude-desktop",
|
|
48
|
+
"claude-code",
|
|
49
|
+
"chatgpt",
|
|
50
|
+
"gemini-cli",
|
|
51
|
+
"perplexity",
|
|
52
|
+
].includes(client.id)
|
|
53
|
+
? ""
|
|
54
|
+
: "bg-white/5 p-1.5"
|
|
55
|
+
}`}
|
|
56
|
+
>
|
|
57
|
+
<Image
|
|
58
|
+
src={client.icon}
|
|
59
|
+
alt={client.name}
|
|
60
|
+
fill
|
|
61
|
+
className={`
|
|
62
|
+
${
|
|
63
|
+
[
|
|
64
|
+
"claude-desktop",
|
|
65
|
+
"claude-code",
|
|
66
|
+
"chatgpt",
|
|
67
|
+
"gemini-cli",
|
|
68
|
+
"perplexity",
|
|
69
|
+
].includes(client.id)
|
|
70
|
+
? "object-cover"
|
|
71
|
+
: "object-contain"
|
|
72
|
+
} ${selectedClient === client.id ? "opacity-100" : "opacity-50 grayscale"}
|
|
73
|
+
`}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
) : (
|
|
77
|
+
<div className="w-7 h-7 flex items-center justify-center text-white/50 bg-white/5 rounded-full border border-white/10 flex-shrink-0">
|
|
78
|
+
<span className="text-xs font-bold">?</span>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
<span
|
|
82
|
+
className={`text-sm font-medium truncate ${
|
|
83
|
+
selectedClient === client.id
|
|
84
|
+
? "text-white"
|
|
85
|
+
: "text-neutral-400"
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
{client.name}
|
|
89
|
+
</span>
|
|
90
|
+
</button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|