@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
|
@@ -1,248 +1,102 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import { Input } from "@/components/ui/input";
|
|
5
|
-
import { Label } from "@/components/ui/label";
|
|
6
4
|
import { useDashboard } from "@/lib/data/dashboard-context";
|
|
7
|
-
import {
|
|
8
|
-
Check,
|
|
9
|
-
Copy,
|
|
10
|
-
Eye,
|
|
11
|
-
EyeOff,
|
|
12
|
-
FileCode,
|
|
13
|
-
Info,
|
|
14
|
-
Monitor,
|
|
15
|
-
X,
|
|
16
|
-
} from "lucide-react";
|
|
17
|
-
import Image from "next/image";
|
|
5
|
+
import { X } from "lucide-react";
|
|
18
6
|
import { useEffect, useState } from "react";
|
|
19
|
-
import
|
|
7
|
+
import ConfigPreview from "./mcp/config-preview";
|
|
8
|
+
import PlatformSelector from "./mcp/platform-selector";
|
|
20
9
|
|
|
21
10
|
export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
22
11
|
const { clientConfigs } = useDashboard();
|
|
23
12
|
const clients = clientConfigs;
|
|
13
|
+
|
|
14
|
+
// State
|
|
24
15
|
const [selectedClient, setSelectedClient] = useState("claude");
|
|
25
|
-
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
26
16
|
const [apiKey, setApiKey] = useState("");
|
|
27
17
|
const [baseUrl, setBaseUrl] = useState("http://localhost:8080");
|
|
28
|
-
const [
|
|
18
|
+
const [isManaged, setIsManaged] = useState(false);
|
|
29
19
|
const [isKeyVisible, setIsKeyVisible] = useState(false);
|
|
30
|
-
const [showRegenConfirm, setShowRegenConfirm] = useState(false);
|
|
31
|
-
const [regenInputValue, setRegenInputValue] = useState("");
|
|
32
|
-
const [isManaged, setIsManaged] = useState(false); // true = local mode, no API key needed
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (localKey) {
|
|
38
|
-
setApiKey(localKey);
|
|
39
|
-
// We still fetch settings for the endpoint
|
|
40
|
-
fetch("/api/settings")
|
|
41
|
-
.then((res) => res.json())
|
|
42
|
-
.then((data) => {
|
|
43
|
-
let srvEndpoint = data.endpoint;
|
|
44
|
-
if (
|
|
45
|
-
srvEndpoint.includes("localhost") &&
|
|
46
|
-
typeof window !== "undefined" &&
|
|
47
|
-
!window.location.hostname.includes("localhost")
|
|
48
|
-
) {
|
|
49
|
-
const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
|
|
50
|
-
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
|
|
51
|
-
}
|
|
52
|
-
setBaseUrl(srvEndpoint);
|
|
53
|
-
setIsLoading(false);
|
|
54
|
-
})
|
|
55
|
-
.catch((err) => setIsLoading(false));
|
|
56
|
-
} else {
|
|
57
|
-
fetch("/api/settings")
|
|
58
|
-
.then((res) => res.json())
|
|
59
|
-
.then((data) => {
|
|
60
|
-
setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
|
|
61
|
-
setIsManaged(data.isManaged || false);
|
|
62
|
-
let srvEndpoint = data.endpoint;
|
|
63
|
-
if (
|
|
64
|
-
srvEndpoint.includes("localhost") &&
|
|
65
|
-
typeof window !== "undefined" &&
|
|
66
|
-
!window.location.hostname.includes("localhost")
|
|
67
|
-
) {
|
|
68
|
-
const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
|
|
69
|
-
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
|
|
70
|
-
}
|
|
71
|
-
setBaseUrl(srvEndpoint);
|
|
72
|
-
setIsLoading(false);
|
|
73
|
-
})
|
|
74
|
-
.catch((err) => {
|
|
75
|
-
console.error("Failed to fetch settings:", err);
|
|
76
|
-
setIsLoading(false);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}, []);
|
|
21
|
+
// Config State (Fetched from API)
|
|
22
|
+
const [configContent, setConfigContent] = useState("");
|
|
23
|
+
const [configType, setConfigType] = useState("json");
|
|
80
24
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
25
|
+
// selectedConfig object
|
|
26
|
+
const selectedConfig = (clients as any[]).find(
|
|
27
|
+
(c) => c.id === selectedClient,
|
|
28
|
+
);
|
|
84
29
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
30
|
+
// 1. Initial Load: Get Settings (API Key, Base URL, Managed Status)
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const fetchSettings = async () => {
|
|
33
|
+
try {
|
|
34
|
+
const localKey = localStorage.getItem("om_api_key");
|
|
35
|
+
const res = await fetch("/api/settings");
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
|
|
38
|
+
// Resolving Endpoint (Localhost Fix)
|
|
39
|
+
let srvEndpoint = data.endpoint;
|
|
40
|
+
if (
|
|
41
|
+
srvEndpoint.includes("localhost") &&
|
|
42
|
+
typeof window !== "undefined" &&
|
|
43
|
+
!window.location.hostname.includes("localhost")
|
|
44
|
+
) {
|
|
45
|
+
const port = srvEndpoint.split(":").pop()?.split("/")[0] || "8626";
|
|
46
|
+
srvEndpoint = `${window.location.protocol}//${window.location.hostname}:${port}`;
|
|
47
|
+
}
|
|
90
48
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
localStorage.setItem("om_api_key", newKey);
|
|
94
|
-
setIsKeyVisible(true);
|
|
95
|
-
setShowRegenConfirm(false);
|
|
96
|
-
setRegenInputValue("");
|
|
97
|
-
toast.success("Token Regenerated!", {
|
|
98
|
-
description: "All existing client connections will need to be updated.",
|
|
99
|
-
});
|
|
100
|
-
} catch (e) {
|
|
101
|
-
console.error(e);
|
|
102
|
-
toast.error("Failed to regenerate token", {
|
|
103
|
-
description: "Please check if the server is running.",
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
};
|
|
49
|
+
setBaseUrl(srvEndpoint);
|
|
50
|
+
setIsManaged(data.isManaged || false);
|
|
107
51
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Remote mode: use stdio with --url and --token
|
|
122
|
-
return {
|
|
123
|
-
mcpServers: {
|
|
124
|
-
cybermem: {
|
|
125
|
-
command: "npx",
|
|
126
|
-
args: [
|
|
127
|
-
"-y",
|
|
128
|
-
"@cybermem/mcp",
|
|
129
|
-
"--url",
|
|
130
|
-
baseUrl,
|
|
131
|
-
"--token",
|
|
132
|
-
apiKey || "sk-your-generated-token",
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
},
|
|
52
|
+
if (localKey) {
|
|
53
|
+
setApiKey(localKey);
|
|
54
|
+
} else {
|
|
55
|
+
setApiKey(data.apiKey !== "not-set" ? data.apiKey : "");
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error("Failed to fetch settings:", err);
|
|
59
|
+
}
|
|
136
60
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const getConfigContent = (maskKey = false) => {
|
|
140
|
-
const config = (clients as any[]).find((c) => c.id === selectedClient);
|
|
141
|
-
const displayKey = maskKey
|
|
142
|
-
? "••••••••••••••••"
|
|
143
|
-
: apiKey || "sk-your-generated-token";
|
|
144
|
-
const actualKey = apiKey || "sk-your-generated-token";
|
|
61
|
+
fetchSettings();
|
|
62
|
+
}, []);
|
|
145
63
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
64
|
+
// 2. Fetch Config when Dependencies Change
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const fetchConfig = async () => {
|
|
67
|
+
try {
|
|
68
|
+
const params = new URLSearchParams({
|
|
69
|
+
client: selectedClient,
|
|
70
|
+
mask: (!isKeyVisible).toString(),
|
|
71
|
+
baseUrl: baseUrl,
|
|
72
|
+
});
|
|
153
73
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Select command based on mode
|
|
157
|
-
let cmd = isManaged ? config?.localCommand : config?.remoteCommand;
|
|
74
|
+
const res = await fetch(`/api/mcp-config?${params.toString()}`);
|
|
75
|
+
const data = await res.json();
|
|
158
76
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
77
|
+
if (data.config) {
|
|
78
|
+
setConfigContent(
|
|
79
|
+
typeof data.config === "object"
|
|
80
|
+
? JSON.stringify(data.config, null, 2)
|
|
81
|
+
: data.config,
|
|
82
|
+
);
|
|
83
|
+
setConfigType(data.configType);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error("Failed to fetch MCP config:", err);
|
|
87
|
+
setConfigContent("// Failed to load config");
|
|
162
88
|
}
|
|
89
|
+
};
|
|
163
90
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
cmd = cmd.replace("{{API_KEY}}", maskKey ? displayKey : actualKey);
|
|
167
|
-
cmd = cmd.replace("{{TOKEN}}", maskKey ? displayKey : actualKey);
|
|
168
|
-
|
|
169
|
-
return cmd;
|
|
91
|
+
if (baseUrl) {
|
|
92
|
+
fetchConfig();
|
|
170
93
|
}
|
|
171
|
-
|
|
172
|
-
// Default to JSON config
|
|
173
|
-
const jsonConfig = getMcpConfig(selectedClient);
|
|
174
|
-
return JSON.stringify(jsonConfig, null, 2);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const copyToClipboard = (text: string, id: string) => {
|
|
178
|
-
navigator.clipboard.writeText(text);
|
|
179
|
-
setCopiedId(id);
|
|
180
|
-
setTimeout(() => setCopiedId(null), 2000);
|
|
181
|
-
toast.success("Copied to clipboard!");
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const highlightJSON = (obj: any) => {
|
|
185
|
-
const json = JSON.stringify(obj, null, 2);
|
|
186
|
-
return json.replace(
|
|
187
|
-
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
188
|
-
(match) => {
|
|
189
|
-
let cls = "text-orange-300";
|
|
190
|
-
if (/^"/.test(match)) {
|
|
191
|
-
if (/:$/.test(match)) {
|
|
192
|
-
cls = "text-emerald-300";
|
|
193
|
-
} else {
|
|
194
|
-
cls = "text-yellow-200";
|
|
195
|
-
}
|
|
196
|
-
} else if (/true|false/.test(match)) {
|
|
197
|
-
cls = "text-blue-300";
|
|
198
|
-
} else if (/null/.test(match)) {
|
|
199
|
-
cls = "text-gray-400";
|
|
200
|
-
}
|
|
201
|
-
return `<span class="${cls}">${match}</span>`;
|
|
202
|
-
},
|
|
203
|
-
);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const selectedConfig = (clients as any[]).find(
|
|
207
|
-
(c) => c.id === selectedClient,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const renderInstructions = () => {
|
|
211
|
-
if (!selectedConfig) return null;
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<div className="space-y-4 text-sm text-neutral-300">
|
|
215
|
-
<p>{selectedConfig.description}</p>
|
|
216
|
-
{selectedConfig.steps.length > 0 && (
|
|
217
|
-
<ol className="list-decimal list-inside space-y-2 ml-2 text-neutral-400">
|
|
218
|
-
{selectedConfig.steps.map((step: string, i: number) => (
|
|
219
|
-
<li
|
|
220
|
-
key={i}
|
|
221
|
-
dangerouslySetInnerHTML={{
|
|
222
|
-
__html: step
|
|
223
|
-
.replace(
|
|
224
|
-
/\*\*(.*?)\*\*/g,
|
|
225
|
-
'<span class="text-white font-medium">$1</span>',
|
|
226
|
-
)
|
|
227
|
-
.replace(
|
|
228
|
-
/`([^`]+)`/g,
|
|
229
|
-
'<code class="text-emerald-400 bg-emerald-500/10 px-1 py-0.5 rounded">$1</code>',
|
|
230
|
-
),
|
|
231
|
-
}}
|
|
232
|
-
/>
|
|
233
|
-
))}
|
|
234
|
-
</ol>
|
|
235
|
-
)}
|
|
236
|
-
</div>
|
|
237
|
-
);
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const configContent = getConfigContent();
|
|
94
|
+
}, [selectedClient, isKeyVisible, baseUrl]);
|
|
241
95
|
|
|
242
96
|
return (
|
|
243
97
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
|
244
98
|
<div
|
|
245
|
-
className="w-full max-w-
|
|
99
|
+
className="w-full max-w-6xl bg-[#05100F] backdrop-blur-xl border-[0.5px] border-white/10 rounded-3xl shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col max-h-[90vh] relative overflow-hidden"
|
|
246
100
|
style={{
|
|
247
101
|
backgroundImage: `
|
|
248
102
|
radial-gradient(circle at 0% 0%, oklch(0.7 0 0 / 0.05) 0%, transparent 50%),
|
|
@@ -252,293 +106,33 @@ export default function MCPConfigModal({ onClose }: { onClose: () => void }) {
|
|
|
252
106
|
`,
|
|
253
107
|
}}
|
|
254
108
|
>
|
|
255
|
-
{/*
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
<div className="p-6 space-y-6 overflow-y-auto flex-1 min-h-0">
|
|
283
|
-
{/* Client Selector */}
|
|
284
|
-
<section>
|
|
285
|
-
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
|
|
286
|
-
<Monitor className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
|
|
287
|
-
Select Client
|
|
288
|
-
</h3>
|
|
289
|
-
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
290
|
-
{clients.map((client) => (
|
|
291
|
-
<button
|
|
292
|
-
key={client.id}
|
|
293
|
-
onClick={() => setSelectedClient(client.id)}
|
|
294
|
-
className={`
|
|
295
|
-
relative group flex flex-col items-center justify-center p-3 rounded-xl transition-all duration-300 border
|
|
296
|
-
${
|
|
297
|
-
selectedClient === client.id
|
|
298
|
-
? "bg-emerald-500/10 border-emerald-500/50 shadow-[0_0_15px_rgba(16,185,129,0.1)] backdrop-blur-sm"
|
|
299
|
-
: "bg-white/5 border-white/5 hover:bg-white/10 hover:border-white/20 backdrop-blur-sm"
|
|
300
|
-
}
|
|
301
|
-
`}
|
|
302
|
-
>
|
|
303
|
-
<div className="mb-2 transition-transform duration-300 group-hover:scale-110">
|
|
304
|
-
{client.icon ? (
|
|
305
|
-
<Image
|
|
306
|
-
src={client.icon}
|
|
307
|
-
alt={client.name}
|
|
308
|
-
width={32}
|
|
309
|
-
height={32}
|
|
310
|
-
className="object-contain drop-shadow-lg"
|
|
311
|
-
/>
|
|
312
|
-
) : (
|
|
313
|
-
<div className="w-8 h-8 flex items-center justify-center text-white/50 bg-white/5 rounded-full border border-white/10 transition-transform duration-300">
|
|
314
|
-
<span className="text-sm font-bold">?</span>
|
|
315
|
-
</div>
|
|
316
|
-
)}
|
|
317
|
-
</div>
|
|
318
|
-
<span
|
|
319
|
-
className={`text-[10px] font-medium text-center transition-colors ${selectedClient === client.id ? "text-emerald-400 text-shadow-emerald" : "text-neutral-400 group-hover:text-white"}`}
|
|
320
|
-
>
|
|
321
|
-
{client.name}
|
|
322
|
-
</span>
|
|
323
|
-
</button>
|
|
324
|
-
))}
|
|
325
|
-
</div>
|
|
326
|
-
</section>
|
|
327
|
-
|
|
328
|
-
{/* Instructions */}
|
|
329
|
-
<section>
|
|
330
|
-
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2 text-shadow-sm">
|
|
331
|
-
<FileCode className="w-5 h-5 text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
|
|
332
|
-
{selectedClient === "codex"
|
|
333
|
-
? "Configuration"
|
|
334
|
-
: selectedClient === "other"
|
|
335
|
-
? "Configuration JSON"
|
|
336
|
-
: "Integration Instructions"}
|
|
337
|
-
</h3>
|
|
338
|
-
|
|
339
|
-
<div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm">
|
|
340
|
-
{selectedClient === "chatgpt" && (
|
|
341
|
-
<div className="px-3 py-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-xs text-emerald-200">
|
|
342
|
-
<p>
|
|
343
|
-
Requires Developer Mode.{" "}
|
|
344
|
-
<a
|
|
345
|
-
href="https://platform.openai.com/docs/guides/developer-mode"
|
|
346
|
-
target="_blank"
|
|
347
|
-
rel="noopener noreferrer"
|
|
348
|
-
className="underline hover:text-white"
|
|
349
|
-
>
|
|
350
|
-
Read OpenAI Documentation
|
|
351
|
-
</a>
|
|
352
|
-
</p>
|
|
353
|
-
</div>
|
|
354
|
-
)}
|
|
355
|
-
|
|
356
|
-
{renderInstructions()}
|
|
357
|
-
|
|
358
|
-
{/* API Key Control Row - Only show in remote mode */}
|
|
359
|
-
{!isManaged ? (
|
|
360
|
-
<div className="bg-white/5 border border-white/10 rounded-lg p-5 space-y-4 shadow-[inset_0_0_20px_rgba(255,255,255,0.02)] backdrop-blur-sm mb-4">
|
|
361
|
-
<div className="space-y-2">
|
|
362
|
-
<Label htmlFor="mcp-api-key" className="text-neutral-200">
|
|
363
|
-
Security Token
|
|
364
|
-
</Label>
|
|
365
|
-
<div className="flex gap-2">
|
|
366
|
-
<div className="relative flex-1">
|
|
367
|
-
<Input
|
|
368
|
-
id="mcp-api-key"
|
|
369
|
-
value={apiKey || "sk-not-generated-yet"}
|
|
370
|
-
readOnly
|
|
371
|
-
className="bg-black/40 border-white/10 text-white focus-visible:border-emerald-500/30 focus-visible:ring-emerald-500/10 placeholder:text-neutral-600 shadow-inner pr-10 font-mono"
|
|
372
|
-
type={isKeyVisible ? "text" : "password"}
|
|
373
|
-
/>
|
|
374
|
-
<button
|
|
375
|
-
type="button"
|
|
376
|
-
onClick={() => setIsKeyVisible(!isKeyVisible)}
|
|
377
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white transition-colors"
|
|
378
|
-
>
|
|
379
|
-
{isKeyVisible ? (
|
|
380
|
-
<EyeOff className="w-4 h-4" />
|
|
381
|
-
) : (
|
|
382
|
-
<Eye className="w-4 h-4" />
|
|
383
|
-
)}
|
|
384
|
-
</button>
|
|
385
|
-
</div>
|
|
386
|
-
<Button
|
|
387
|
-
size="icon"
|
|
388
|
-
variant="ghost"
|
|
389
|
-
className="h-10 w-10 border border-white/10 bg-white/5 hover:bg-white/10 text-neutral-400 hover:text-white"
|
|
390
|
-
onClick={() => copyToClipboard(apiKey, "apikey")}
|
|
391
|
-
title="Copy API Key"
|
|
392
|
-
>
|
|
393
|
-
{copiedId === "apikey" ? (
|
|
394
|
-
<Check className="h-4 w-4 text-emerald-400" />
|
|
395
|
-
) : (
|
|
396
|
-
<Copy className="h-4 w-4" />
|
|
397
|
-
)}
|
|
398
|
-
</Button>
|
|
399
|
-
</div>
|
|
400
|
-
|
|
401
|
-
{/* Regeneration Controls */}
|
|
402
|
-
<div className="flex justify-end pt-2">
|
|
403
|
-
{showRegenConfirm ? (
|
|
404
|
-
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-4 duration-200">
|
|
405
|
-
<span className="text-xs text-red-400 font-medium">
|
|
406
|
-
Warning: Disconnects clients.
|
|
407
|
-
</span>
|
|
408
|
-
<Input
|
|
409
|
-
value={regenInputValue}
|
|
410
|
-
onChange={(e) => setRegenInputValue(e.target.value)}
|
|
411
|
-
placeholder="Type 'agree'"
|
|
412
|
-
className="h-8 w-28 bg-red-500/10 border-red-500/30 text-red-200 text-xs placeholder:text-red-500/30 focus-visible:border-red-500/50"
|
|
413
|
-
/>
|
|
414
|
-
<Button
|
|
415
|
-
size="sm"
|
|
416
|
-
variant="ghost"
|
|
417
|
-
className="h-8 px-3 text-neutral-400 hover:text-white hover:bg-white/10"
|
|
418
|
-
onClick={() => {
|
|
419
|
-
setShowRegenConfirm(false);
|
|
420
|
-
setRegenInputValue("");
|
|
421
|
-
}}
|
|
422
|
-
>
|
|
423
|
-
Cancel
|
|
424
|
-
</Button>
|
|
425
|
-
<Button
|
|
426
|
-
size="sm"
|
|
427
|
-
disabled={regenInputValue !== "agree"}
|
|
428
|
-
className="h-8 px-3 bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
429
|
-
onClick={confirmRegenerate}
|
|
430
|
-
>
|
|
431
|
-
Confirm
|
|
432
|
-
</Button>
|
|
433
|
-
</div>
|
|
434
|
-
) : (
|
|
435
|
-
<Button
|
|
436
|
-
size="sm"
|
|
437
|
-
variant="ghost"
|
|
438
|
-
className="h-8 px-2 text-neutral-400 hover:text-white hover:bg-white/10"
|
|
439
|
-
onClick={() => setShowRegenConfirm(true)}
|
|
440
|
-
>
|
|
441
|
-
Regenerate Key
|
|
442
|
-
</Button>
|
|
443
|
-
)}
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
</div>
|
|
447
|
-
) : (
|
|
448
|
-
<div className="bg-emerald-500/5 border border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
449
|
-
<div className="flex items-start gap-3">
|
|
450
|
-
<Info className="h-4 w-4 shrink-0 text-emerald-400 mt-0.5" />
|
|
451
|
-
<div className="space-y-1">
|
|
452
|
-
<p className="text-sm font-medium text-emerald-200">
|
|
453
|
-
Local Mode Active
|
|
454
|
-
</p>
|
|
455
|
-
<p className="text-xs text-emerald-200/60">
|
|
456
|
-
No token required for connection from your laptop. Just
|
|
457
|
-
copy the config below.
|
|
458
|
-
</p>
|
|
459
|
-
</div>
|
|
460
|
-
</div>
|
|
461
|
-
</div>
|
|
462
|
-
)}
|
|
463
|
-
|
|
464
|
-
<div className="relative group">
|
|
465
|
-
<div className="relative pl-5 py-5 pr-24 rounded-lg bg-[#0F161C] border border-white/10 font-mono text-xs md:text-sm text-white overflow-x-auto shadow-[0_0_20px_rgba(0,0,0,0.3)] inset-shadow">
|
|
466
|
-
<pre className="text-shadow-sm">
|
|
467
|
-
{(() => {
|
|
468
|
-
const config = (clients as any[]).find(
|
|
469
|
-
(c) => c.id === selectedClient,
|
|
470
|
-
);
|
|
471
|
-
if (config?.configType === "json") {
|
|
472
|
-
return (
|
|
473
|
-
<code
|
|
474
|
-
dangerouslySetInnerHTML={{
|
|
475
|
-
__html: highlightJSON(
|
|
476
|
-
JSON.parse(getConfigContent(!isKeyVisible)),
|
|
477
|
-
),
|
|
478
|
-
}}
|
|
479
|
-
/>
|
|
480
|
-
);
|
|
481
|
-
} else {
|
|
482
|
-
return getConfigContent(!isKeyVisible);
|
|
483
|
-
}
|
|
484
|
-
})()}
|
|
485
|
-
</pre>
|
|
486
|
-
</div>
|
|
487
|
-
<div className="absolute top-5 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
488
|
-
<Button
|
|
489
|
-
size="sm"
|
|
490
|
-
variant="ghost"
|
|
491
|
-
className="h-8 px-3 text-white bg-black/40 backdrop-blur border border-white/5 shadow-[0_0_10px_rgba(255,255,255,0.05)] hover:bg-white/10 hover:text-white font-medium"
|
|
492
|
-
onClick={() =>
|
|
493
|
-
copyToClipboard(getConfigContent(false), "config")
|
|
494
|
-
}
|
|
495
|
-
>
|
|
496
|
-
{copiedId === "config" ? (
|
|
497
|
-
<Check className="h-4 w-4 stroke-[2.5] text-emerald-400 mr-2 drop-shadow-[0_0_5px_rgba(52,211,153,0.5)]" />
|
|
498
|
-
) : (
|
|
499
|
-
<Copy className="h-4 w-4 stroke-[2.5] mr-2 text-white drop-shadow-[0_0_5px_rgba(255,255,255,0.3)]" />
|
|
500
|
-
)}
|
|
501
|
-
{copiedId === "config" ? (
|
|
502
|
-
<span className="text-emerald-400 text-shadow-sm">
|
|
503
|
-
Copied
|
|
504
|
-
</span>
|
|
505
|
-
) : (
|
|
506
|
-
<span className="text-white text-shadow-sm">Copy</span>
|
|
507
|
-
)}
|
|
508
|
-
</Button>
|
|
509
|
-
</div>
|
|
510
|
-
</div>
|
|
511
|
-
|
|
512
|
-
{!isManaged && (
|
|
513
|
-
<div className="flex items-start gap-3 p-3 rounded-lg bg-emerald-500/5 border border-emerald-500/10 text-emerald-200/70 text-xs mt-4">
|
|
514
|
-
<Info className="h-4 w-4 shrink-0 text-white mt-0.5" />
|
|
515
|
-
<p>
|
|
516
|
-
This configuration includes your generated Security Token.
|
|
517
|
-
Keep it secure and do not share it publicly.
|
|
518
|
-
</p>
|
|
519
|
-
</div>
|
|
520
|
-
)}
|
|
521
|
-
</div>
|
|
522
|
-
</section>
|
|
523
|
-
</div>
|
|
524
|
-
|
|
525
|
-
{/* Footer */}
|
|
526
|
-
<div className="border-t border-emerald-500/20 px-6 py-4 flex justify-end gap-3 flex-none bg-[#0B1116]/30">
|
|
527
|
-
<Button
|
|
528
|
-
asChild
|
|
529
|
-
variant="ghost"
|
|
530
|
-
className="bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 text-emerald-400 hover:text-emerald-300 mr-auto"
|
|
531
|
-
>
|
|
532
|
-
<a href="https://docs.cybermem.dev" target="_blank">
|
|
533
|
-
Read Documentation
|
|
534
|
-
</a>
|
|
535
|
-
</Button>
|
|
536
|
-
<Button
|
|
537
|
-
onClick={onClose}
|
|
538
|
-
className="bg-white/5 hover:bg-white/10 border border-white/10 text-neutral-300 transition-colors"
|
|
539
|
-
>
|
|
540
|
-
Close
|
|
541
|
-
</Button>
|
|
109
|
+
{/* Close Button */}
|
|
110
|
+
<Button
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="icon"
|
|
113
|
+
onClick={onClose}
|
|
114
|
+
className="absolute top-6 right-6 z-10 text-neutral-400 hover:text-white hover:bg-white/10 rounded-full"
|
|
115
|
+
>
|
|
116
|
+
<X className="w-5 h-5" />
|
|
117
|
+
</Button>
|
|
118
|
+
|
|
119
|
+
{/* Layout */}
|
|
120
|
+
<div className="flex flex-1 min-h-0">
|
|
121
|
+
<PlatformSelector
|
|
122
|
+
clients={clients}
|
|
123
|
+
selectedClient={selectedClient}
|
|
124
|
+
onSelect={setSelectedClient}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
<ConfigPreview
|
|
128
|
+
selectedConfig={selectedConfig}
|
|
129
|
+
configContent={configContent}
|
|
130
|
+
configType={configType}
|
|
131
|
+
isManaged={isManaged}
|
|
132
|
+
isKeyVisible={isKeyVisible}
|
|
133
|
+
onToggleKeyVisibility={() => setIsKeyVisible(!isKeyVisible)}
|
|
134
|
+
onClose={onClose}
|
|
135
|
+
/>
|
|
542
136
|
</div>
|
|
543
137
|
</div>
|
|
544
138
|
</div>
|
|
@@ -12,7 +12,7 @@ interface MetricCardProps {
|
|
|
12
12
|
loading?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export default function
|
|
15
|
+
export default function StatCard({
|
|
16
16
|
label,
|
|
17
17
|
value,
|
|
18
18
|
loading = false,
|
|
@@ -49,7 +49,9 @@ export default function MetricCard({
|
|
|
49
49
|
<Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden relative group hover:bg-white/[0.07] transition-colors">
|
|
50
50
|
<CardContent className="p-6 relative z-10">
|
|
51
51
|
<div className="text-sm font-medium text-slate-400 mb-2">{label}</div>
|
|
52
|
-
<div className="text-4xl font-bold text-white"
|
|
52
|
+
<div className="text-4xl font-bold text-white" data-testid="stat-value">
|
|
53
|
+
{value}
|
|
54
|
+
</div>
|
|
53
55
|
</CardContent>
|
|
54
56
|
</Card>
|
|
55
57
|
);
|