Skip to content

Commit fa45137

Browse files
authored
fix: admin UI polish — password manager, UUID display, persona descriptions, platform_info listing (#97)
- Rename PlatformToolInfo to ToolInfo to fix revive stuttering lint - Omit unused receiver on PlatformTools() method - Regenerate swagger docs - Add TestPlatformTools for patch coverage
1 parent 5cb64cc commit fa45137

File tree

29 files changed

+748
-221
lines changed

29 files changed

+748
-221
lines changed

admin-ui/src/api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface AuditStatsResponse {
130130
export interface AuditFiltersResponse {
131131
users: string[];
132132
tools: string[];
133+
user_labels?: Record<string, string>;
133134
}
134135

135136
export type AuditSortColumn =
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { AuditEvent } from "@/api/types";
2+
import { StatusBadge } from "@/components/cards/StatusBadge";
3+
import { formatDuration } from "@/lib/formatDuration";
4+
import { formatUser } from "@/lib/formatUser";
5+
6+
export function EventDrawer({
7+
event,
8+
onClose,
9+
}: {
10+
event: AuditEvent;
11+
onClose: () => void;
12+
}) {
13+
return (
14+
<div className="fixed inset-0 z-50 flex justify-end">
15+
<div
16+
className="absolute inset-0 bg-black/50"
17+
onClick={onClose}
18+
/>
19+
<div className="relative w-full max-w-lg overflow-auto bg-card p-6 shadow-xl">
20+
<div className="mb-4 flex items-center justify-between">
21+
<h2 className="text-lg font-semibold">Event Detail</h2>
22+
<button
23+
onClick={onClose}
24+
className="rounded-md px-2 py-1 text-sm hover:bg-muted"
25+
>
26+
Close
27+
</button>
28+
</div>
29+
30+
<div className="space-y-4">
31+
<div className="grid grid-cols-2 gap-3 text-sm">
32+
<div>
33+
<p className="text-xs text-muted-foreground">Event ID</p>
34+
<p className="font-mono text-xs">{event.id}</p>
35+
</div>
36+
<div>
37+
<p className="text-xs text-muted-foreground">Timestamp</p>
38+
<p>{new Date(event.timestamp).toLocaleString()}</p>
39+
</div>
40+
<div>
41+
<p className="text-xs text-muted-foreground">User</p>
42+
<p title={event.user_id}>
43+
{formatUser(event.user_id, event.user_email)}
44+
</p>
45+
</div>
46+
<div>
47+
<p className="text-xs text-muted-foreground">Persona</p>
48+
<p>{event.persona || "-"}</p>
49+
</div>
50+
<div>
51+
<p className="text-xs text-muted-foreground">Tool</p>
52+
<p className="font-mono text-xs">{event.tool_name}</p>
53+
</div>
54+
<div>
55+
<p className="text-xs text-muted-foreground">Toolkit</p>
56+
<p>
57+
{event.toolkit_kind} / {event.toolkit_name}
58+
</p>
59+
</div>
60+
<div>
61+
<p className="text-xs text-muted-foreground">Connection</p>
62+
<p>{event.connection}</p>
63+
</div>
64+
<div>
65+
<p className="text-xs text-muted-foreground">Duration</p>
66+
<p>{formatDuration(event.duration_ms)}</p>
67+
</div>
68+
<div>
69+
<p className="text-xs text-muted-foreground">Status</p>
70+
<StatusBadge
71+
variant={event.success ? "success" : "error"}
72+
>
73+
{event.success ? "Success" : "Error"}
74+
</StatusBadge>
75+
</div>
76+
<div>
77+
<p className="text-xs text-muted-foreground">Enriched</p>
78+
<StatusBadge
79+
variant={
80+
event.enrichment_applied ? "success" : "neutral"
81+
}
82+
>
83+
{event.enrichment_applied ? "Yes" : "No"}
84+
</StatusBadge>
85+
</div>
86+
<div>
87+
<p className="text-xs text-muted-foreground">Transport</p>
88+
<p>{event.transport}</p>
89+
</div>
90+
<div>
91+
<p className="text-xs text-muted-foreground">Session</p>
92+
<p className="font-mono text-xs">{event.session_id}</p>
93+
</div>
94+
</div>
95+
96+
<div className="grid grid-cols-3 gap-3 text-sm">
97+
<div>
98+
<p className="text-xs text-muted-foreground">Request Chars</p>
99+
<p>{event.request_chars.toLocaleString()}</p>
100+
</div>
101+
<div>
102+
<p className="text-xs text-muted-foreground">Response Chars</p>
103+
<p>{event.response_chars.toLocaleString()}</p>
104+
</div>
105+
<div>
106+
<p className="text-xs text-muted-foreground">Content Blocks</p>
107+
<p>{event.content_blocks}</p>
108+
</div>
109+
</div>
110+
111+
{event.error_message && (
112+
<div>
113+
<p className="text-xs text-muted-foreground">Error Message</p>
114+
<p className="mt-1 rounded bg-red-50 p-2 text-sm text-red-800 break-words">
115+
{event.error_message}
116+
</p>
117+
</div>
118+
)}
119+
120+
{event.parameters &&
121+
Object.keys(event.parameters).length > 0 && (
122+
<div>
123+
<p className="mb-1 text-xs text-muted-foreground">
124+
Parameters
125+
</p>
126+
<pre className="overflow-auto whitespace-pre-wrap break-words rounded bg-muted p-3 text-xs">
127+
{JSON.stringify(event.parameters, null, 2)}
128+
</pre>
129+
</div>
130+
)}
131+
</div>
132+
</div>
133+
</div>
134+
);
135+
}

admin-ui/src/components/LoginForm.tsx

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,71 @@ import { useAuthStore } from "@/stores/auth";
33

44
export function LoginForm() {
55
const [key, setKey] = useState("");
6+
const [error, setError] = useState("");
7+
const [loading, setLoading] = useState(false);
68
const setApiKey = useAuthStore((s) => s.setApiKey);
79

8-
function handleSubmit(e: React.FormEvent) {
9-
e.preventDefault();
10-
if (key.trim()) {
11-
setApiKey(key.trim());
10+
async function handleLogin() {
11+
const trimmed = key.trim();
12+
if (!trimmed) return;
13+
14+
setError("");
15+
setLoading(true);
16+
17+
try {
18+
const res = await fetch("/api/v1/admin/system/info", {
19+
headers: { "X-API-Key": trimmed },
20+
});
21+
if (!res.ok) {
22+
setError(res.status === 401 ? "Invalid API key" : `Server error (${res.status})`);
23+
return;
24+
}
25+
setApiKey(trimmed);
26+
} catch {
27+
setError("Unable to reach server");
28+
} finally {
29+
setLoading(false);
1230
}
1331
}
1432

33+
function handleKeyDown(e: React.KeyboardEvent) {
34+
if (e.key === "Enter") handleLogin();
35+
}
36+
1537
return (
1638
<div className="flex min-h-screen items-center justify-center bg-muted/40">
17-
<form
18-
onSubmit={handleSubmit}
19-
className="w-full max-w-sm rounded-lg border bg-card p-6 shadow-sm"
20-
>
39+
<div className="w-full max-w-sm rounded-lg border bg-card p-6 shadow-sm">
2140
<h1 className="mb-1 text-xl font-semibold">MCP Data Platform</h1>
2241
<p className="mb-4 text-sm text-muted-foreground">
2342
Enter your API key to access the admin dashboard.
2443
</p>
44+
{error && (
45+
<p className="mb-3 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700">
46+
{error}
47+
</p>
48+
)}
2549
<input
26-
type="password"
50+
type="text"
51+
autoComplete="off"
52+
data-1p-ignore
53+
data-lpignore="true"
2754
value={key}
2855
onChange={(e) => setKey(e.target.value)}
56+
onKeyDown={handleKeyDown}
2957
placeholder="API Key"
58+
style={{ WebkitTextSecurity: "disc" } as React.CSSProperties}
3059
className="mb-3 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none ring-ring focus:ring-2"
3160
autoFocus
3261
/>
3362
<button
34-
type="submit"
35-
disabled={!key.trim()}
63+
type="button"
64+
disabled={!key.trim() || loading}
65+
onClick={handleLogin}
3666
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
3767
>
38-
Sign In
68+
{loading ? "Validating..." : "Sign In"}
3969
</button>
40-
</form>
70+
</div>
4171
</div>
4272
);
4373
}

admin-ui/src/components/charts/BarChart.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "recharts";
99
import type { BreakdownEntry } from "@/api/types";
1010
import { ChartSkeleton } from "./ChartSkeleton";
11+
import { formatDuration } from "@/lib/formatDuration";
1112

1213
interface BarChartProps {
1314
data: BreakdownEntry[] | undefined;
@@ -16,6 +17,13 @@ interface BarChartProps {
1617
color?: string;
1718
}
1819

20+
function truncateLabel(label: string, max = 16): string {
21+
if (label.length <= max) return label;
22+
// UUID pattern: truncate to first 8 chars
23+
if (/^[0-9a-f]{8}-/.test(label)) return label.slice(0, 8) + "\u2026";
24+
return label.slice(0, max - 1) + "\u2026";
25+
}
26+
1927
export function BreakdownBarChart({
2028
data,
2129
isLoading,
@@ -38,6 +46,7 @@ export function BreakdownBarChart({
3846
className="text-xs"
3947
tick={{ fill: "hsl(var(--muted-foreground))" }}
4048
width={80}
49+
tickFormatter={truncateLabel}
4150
/>
4251
<Tooltip
4352
contentStyle={{
@@ -46,7 +55,10 @@ export function BreakdownBarChart({
4655
borderRadius: "0.375rem",
4756
fontSize: "0.75rem",
4857
}}
49-
formatter={(value: number) => [value, "Count"]}
58+
formatter={(value: number, name: string) => {
59+
if (name === "avg_duration_ms") return [formatDuration(value), "Avg Duration"];
60+
return [value, "Count"];
61+
}}
5062
/>
5163
<Bar dataKey="count" fill={color} radius={[0, 4, 4, 0]} />
5264
</RechartsBarChart>

admin-ui/src/components/charts/TimeseriesChart.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "recharts";
1010
import type { TimeseriesBucket } from "@/api/types";
1111
import { ChartSkeleton } from "./ChartSkeleton";
12+
import { formatDuration } from "@/lib/formatDuration";
1213

1314
interface TimeseriesChartProps {
1415
data: TimeseriesBucket[] | undefined;
@@ -44,6 +45,10 @@ export function TimeseriesChart({
4445
/>
4546
<Tooltip
4647
labelFormatter={(v) => new Date(v as string).toLocaleString()}
48+
formatter={(value: number, name: string) => {
49+
if (name === "avg_duration_ms") return [formatDuration(value), "Avg Duration"];
50+
return [value, name];
51+
}}
4752
contentStyle={{
4853
backgroundColor: "hsl(var(--card))",
4954
border: "1px solid hsl(var(--border))",

admin-ui/src/lib/formatDuration.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function formatDuration(ms: number): string {
2+
if (ms < 1000) return `${Math.round(ms)}ms`;
3+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
4+
const m = Math.floor(ms / 60_000);
5+
const s = Math.round((ms % 60_000) / 1000);
6+
if (m < 60) return s > 0 ? `${m}m ${s}s` : `${m}m`;
7+
const h = Math.floor(m / 60);
8+
const rm = m % 60;
9+
return rm > 0 ? `${h}h ${rm}m` : `${h}h`;
10+
}

admin-ui/src/lib/formatUser.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** Show email/name if available, otherwise truncate UUIDs to 8 chars. */
2+
export function formatUser(userId: string, email?: string): string {
3+
if (email) return email;
4+
// UUID pattern: truncate to first 8 chars
5+
if (/^[0-9a-f]{8}-/.test(userId)) return userId.slice(0, 8) + "\u2026";
6+
return userId;
7+
}

0 commit comments

Comments
 (0)