Skip to content

Commit 4521d97

Browse files
authored
fix: admin UI — visibility icons, login branding, chart labels, toolkit tools (#98)
- Add Eye/EyeOff visibility icons to Explore tab and Tool Inventory based on global tools.allow/deny config - Export IsToolVisible from middleware; add hidden_tools to connections API - Login screen fetches platform name from public /branding endpoint, falls back to "MCP Data Platform" - Add unauthenticated /api/v1/admin/public/branding route - Rewrite BreakdownBarChart with dynamic YAxis width computed from actual label lengths — no more truncation or unreadable labels - Audit breakdown shows user_email instead of UUID when available - Fix datahub toolkit missing datahub_get_column_lineage in Tools() - Fix S3 toolkit registering write tools on MCP server when read_only
1 parent fa45137 commit 4521d97

File tree

19 files changed

+364
-61
lines changed

19 files changed

+364
-61
lines changed

admin-ui/src/api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ConnectionInfo {
3737
name: string;
3838
connection: string;
3939
tools: string[];
40+
hidden_tools: string[];
4041
}
4142

4243
export interface ConnectionListResponse {

admin-ui/src/components/LoginForm.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22
import { useAuthStore } from "@/stores/auth";
33

4+
const DEFAULT_PLATFORM_NAME = "MCP Data Platform";
5+
46
export function LoginForm() {
57
const [key, setKey] = useState("");
68
const [error, setError] = useState("");
79
const [loading, setLoading] = useState(false);
10+
const [platformName, setPlatformName] = useState(DEFAULT_PLATFORM_NAME);
811
const setApiKey = useAuthStore((s) => s.setApiKey);
912

13+
useEffect(() => {
14+
fetch("/api/v1/admin/public/branding")
15+
.then((res) => (res.ok ? res.json() : null))
16+
.then((data: { name?: string; portal_title?: string } | null) => {
17+
const name = data?.portal_title || data?.name;
18+
if (name) setPlatformName(name);
19+
})
20+
.catch(() => {
21+
// Silently fall back to default name
22+
});
23+
}, []);
24+
1025
async function handleLogin() {
1126
const trimmed = key.trim();
1227
if (!trimmed) return;
@@ -37,7 +52,7 @@ export function LoginForm() {
3752
return (
3853
<div className="flex min-h-screen items-center justify-center bg-muted/40">
3954
<div className="w-full max-w-sm rounded-lg border bg-card p-6 shadow-sm">
40-
<h1 className="mb-1 text-xl font-semibold">MCP Data Platform</h1>
55+
<h1 className="mb-1 text-xl font-semibold">{platformName}</h1>
4156
<p className="mb-4 text-sm text-muted-foreground">
4257
Enter your API key to access the admin dashboard.
4358
</p>

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

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMemo } from "react";
12
import {
23
BarChart as RechartsBarChart,
34
Bar,
@@ -17,11 +18,17 @@ interface BarChartProps {
1718
color?: string;
1819
}
1920

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";
21+
/** Shorten an email to just the local part (before @). */
22+
function shortenEmail(label: string): string {
23+
const at = label.indexOf("@");
24+
if (at > 0) return label.slice(0, at);
25+
return label;
26+
}
27+
28+
/** Estimate pixel width for a label at text-xs (12px) in a proportional font. */
29+
function estimateLabelWidth(label: string): number {
30+
// ~6.5px per character at 12px sans-serif, plus 16px padding for tick/gap.
31+
return label.length * 6.5 + 16;
2532
}
2633

2734
export function BreakdownBarChart({
@@ -30,23 +37,40 @@ export function BreakdownBarChart({
3037
height = 250,
3138
color = "hsl(var(--primary))",
3239
}: BarChartProps) {
33-
if (isLoading || !data) return <ChartSkeleton height={height} />;
40+
// Pre-process: shorten emails to local part for display.
41+
const chartData = useMemo(
42+
() =>
43+
data?.map((d) => ({
44+
...d,
45+
label: shortenEmail(d.dimension),
46+
})),
47+
[data],
48+
);
49+
50+
// Compute YAxis width from the longest label — no truncation needed.
51+
const yAxisWidth = useMemo(() => {
52+
if (!chartData || chartData.length === 0) return 80;
53+
const maxLen = Math.max(...chartData.map((d) => estimateLabelWidth(d.label)));
54+
// Clamp between 80 and 260 to keep bars visible.
55+
return Math.min(260, Math.max(80, maxLen));
56+
}, [chartData]);
57+
58+
if (isLoading || !chartData) return <ChartSkeleton height={height} />;
3459

3560
return (
3661
<ResponsiveContainer width="100%" height={height}>
37-
<RechartsBarChart data={data} layout="vertical" margin={{ left: 80 }}>
62+
<RechartsBarChart data={chartData} layout="vertical" margin={{ left: 0, right: 12 }}>
3863
<XAxis
3964
type="number"
4065
className="text-xs"
4166
tick={{ fill: "hsl(var(--muted-foreground))" }}
4267
/>
4368
<YAxis
4469
type="category"
45-
dataKey="dimension"
70+
dataKey="label"
4671
className="text-xs"
4772
tick={{ fill: "hsl(var(--muted-foreground))" }}
48-
width={80}
49-
tickFormatter={truncateLabel}
73+
width={yAxisWidth}
5074
/>
5175
<Tooltip
5276
contentStyle={{
@@ -55,6 +79,11 @@ export function BreakdownBarChart({
5579
borderRadius: "0.375rem",
5680
fontSize: "0.75rem",
5781
}}
82+
labelFormatter={(label: string) => {
83+
// Show the full original dimension in the tooltip.
84+
const entry = chartData.find((d) => d.label === label);
85+
return entry?.dimension ?? label;
86+
}}
5887
formatter={(value: number, name: string) => {
5988
if (name === "avg_duration_ms") return [formatDuration(value), "Avg Duration"];
6089
return [value, "Count"];

admin-ui/src/mocks/data/system.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,35 +57,41 @@ export const mockConnections: ConnectionInfo[] = [
5757
name: "acme-warehouse",
5858
connection: "acme-warehouse",
5959
tools: ["trino_query", "trino_describe_table", "trino_list_catalogs", "trino_list_schemas", "trino_list_tables", "trino_explain"],
60+
hidden_tools: ["trino_explain"],
6061
},
6162
{
6263
kind: "trino",
6364
name: "acme-staging",
6465
connection: "acme-staging",
6566
tools: ["trino_query", "trino_describe_table"],
67+
hidden_tools: [],
6668
},
6769
{
6870
kind: "datahub",
6971
name: "acme-catalog",
7072
connection: "acme-catalog",
7173
tools: ["datahub_search", "datahub_get_entity", "datahub_get_schema", "datahub_get_lineage", "datahub_get_column_lineage"],
74+
hidden_tools: [],
7275
},
7376
{
7477
kind: "datahub",
7578
name: "acme-catalog-staging",
7679
connection: "acme-catalog-staging",
7780
tools: ["datahub_search", "datahub_get_entity"],
81+
hidden_tools: ["datahub_search", "datahub_get_entity"],
7882
},
7983
{
8084
kind: "s3",
8185
name: "acme-data-lake",
8286
connection: "acme-data-lake",
8387
tools: ["s3_list_objects", "s3_get_object", "s3_list_buckets"],
88+
hidden_tools: [],
8489
},
8590
{
8691
kind: "s3",
8792
name: "acme-reports",
8893
connection: "acme-reports",
8994
tools: ["s3_list_objects", "s3_get_object"],
95+
hidden_tools: [],
9096
},
9197
];

admin-ui/src/mocks/handlers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,14 @@ function computeInsightStats(insights: Insight[]): InsightStats {
192192
// ---------------------------------------------------------------------------
193193

194194
export const handlers = [
195+
// Public (unauthenticated)
196+
http.get(`${BASE}/public/branding`, () =>
197+
HttpResponse.json({
198+
name: mockSystemInfo.name,
199+
portal_title: mockSystemInfo.portal_title,
200+
}),
201+
),
202+
195203
// System
196204
http.get(`${BASE}/system/info`, () => HttpResponse.json(mockSystemInfo)),
197205

admin-ui/src/pages/tools/ToolsPage.tsx

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
} from "@/api/types";
1212
import { StatusBadge } from "@/components/cards/StatusBadge";
1313
import { formatDuration } from "@/lib/formatDuration";
14+
import { Eye, EyeOff } from "lucide-react";
1415

1516
type Tab = "overview" | "explore" | "help";
1617

@@ -61,6 +62,11 @@ function OverviewTab() {
6162
const tools = toolsData?.tools ?? [];
6263
const connections = connectionsData?.connections ?? [];
6364

65+
const hiddenToolSet = useMemo(
66+
() => new Set(connections.flatMap((c) => c.hidden_tools ?? [])),
67+
[connections],
68+
);
69+
6470
return (
6571
<div className="space-y-6">
6672
{/* Connections grid */}
@@ -109,16 +115,28 @@ function OverviewTab() {
109115
</tr>
110116
</thead>
111117
<tbody>
112-
{tools.map((tool, idx) => (
113-
<tr key={`${tool.name}-${tool.connection}-${idx}`} className="border-b">
114-
<td className="px-3 py-2 font-mono text-xs">{tool.name}</td>
115-
<td className="px-3 py-2">
116-
<StatusBadge variant="neutral">{tool.kind}</StatusBadge>
117-
</td>
118-
<td className="px-3 py-2 text-xs">{tool.connection}</td>
119-
<td className="px-3 py-2 text-xs">{tool.toolkit}</td>
120-
</tr>
121-
))}
118+
{tools.map((tool, idx) => {
119+
const isHidden = hiddenToolSet.has(tool.name);
120+
return (
121+
<tr key={`${tool.name}-${tool.connection}-${idx}`} className="border-b">
122+
<td className="px-3 py-2 font-mono text-xs">
123+
<span className="flex items-center gap-1.5">
124+
{isHidden ? (
125+
<EyeOff className="h-3 w-3 shrink-0 opacity-40" />
126+
) : (
127+
<Eye className="h-3 w-3 shrink-0 opacity-40" />
128+
)}
129+
<span className={isHidden ? "opacity-50" : ""}>{tool.name}</span>
130+
</span>
131+
</td>
132+
<td className="px-3 py-2">
133+
<StatusBadge variant="neutral">{tool.kind}</StatusBadge>
134+
</td>
135+
<td className="px-3 py-2 text-xs">{tool.connection}</td>
136+
<td className="px-3 py-2 text-xs">{tool.toolkit}</td>
137+
</tr>
138+
);
139+
})}
122140
{tools.length === 0 && (
123141
<tr>
124142
<td
@@ -191,6 +209,12 @@ function ExploreTab() {
191209
.sort();
192210
}, [connections, schemas, search]);
193211

212+
// Set of tool names hidden by global visibility filter
213+
const hiddenToolSet = useMemo(
214+
() => new Set(connections.flatMap((c) => c.hidden_tools ?? [])),
215+
[connections],
216+
);
217+
194218
const selectTool = useCallback(
195219
(toolName: string, connection: ConnectionInfo | null) => {
196220
setSelectedTool(toolName);
@@ -313,20 +337,28 @@ function ExploreTab() {
313337
{conn.name}
314338
</span>
315339
</div>
316-
{conn.tools.map((toolName) => (
317-
<button
318-
key={`${conn.connection}-${toolName}`}
319-
onClick={() => selectTool(toolName, conn)}
320-
className={`flex w-full rounded-md px-3 py-1.5 text-left text-xs font-medium transition-colors ${
321-
selectedTool === toolName &&
322-
selectedConnection === conn.connection
323-
? "bg-primary/10 text-primary"
324-
: "text-muted-foreground hover:bg-muted hover:text-foreground"
325-
}`}
326-
>
327-
{toolName}
328-
</button>
329-
))}
340+
{conn.tools.map((toolName) => {
341+
const isHidden = hiddenToolSet.has(toolName);
342+
return (
343+
<button
344+
key={`${conn.connection}-${toolName}`}
345+
onClick={() => selectTool(toolName, conn)}
346+
className={`flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-xs font-medium transition-colors ${
347+
selectedTool === toolName &&
348+
selectedConnection === conn.connection
349+
? "bg-primary/10 text-primary"
350+
: "text-muted-foreground hover:bg-muted hover:text-foreground"
351+
}`}
352+
>
353+
{isHidden ? (
354+
<EyeOff className="h-3 w-3 shrink-0 opacity-40" />
355+
) : (
356+
<Eye className="h-3 w-3 shrink-0 opacity-40" />
357+
)}
358+
<span className={isHidden ? "opacity-50" : ""}>{toolName}</span>
359+
</button>
360+
);
361+
})}
330362
</div>
331363
))}
332364
{platformTools.length > 0 && (
@@ -341,12 +373,13 @@ function ExploreTab() {
341373
<button
342374
key={`platform-${toolName}`}
343375
onClick={() => selectTool(toolName, null)}
344-
className={`flex w-full rounded-md px-3 py-1.5 text-left text-xs font-medium transition-colors ${
376+
className={`flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-xs font-medium transition-colors ${
345377
selectedTool === toolName && selectedConnection === ""
346378
? "bg-primary/10 text-primary"
347379
: "text-muted-foreground hover:bg-muted hover:text-foreground"
348380
}`}
349381
>
382+
<Eye className="h-3 w-3 shrink-0 opacity-40" />
350383
{toolName}
351384
</button>
352385
))}

internal/apidocs/docs.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,12 @@ const docTemplate = `{
19261926
"connection": {
19271927
"type": "string"
19281928
},
1929+
"hidden_tools": {
1930+
"type": "array",
1931+
"items": {
1932+
"type": "string"
1933+
}
1934+
},
19291935
"kind": {
19301936
"type": "string"
19311937
},

internal/apidocs/swagger.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,6 +1920,12 @@
19201920
"connection": {
19211921
"type": "string"
19221922
},
1923+
"hidden_tools": {
1924+
"type": "array",
1925+
"items": {
1926+
"type": "string"
1927+
}
1928+
},
19231929
"kind": {
19241930
"type": "string"
19251931
},

internal/apidocs/swagger.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ definitions:
108108
properties:
109109
connection:
110110
type: string
111+
hidden_tools:
112+
items:
113+
type: string
114+
type: array
111115
kind:
112116
type: string
113117
name:

pkg/admin/handler.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ type Deps struct {
8585
// docsPrefix is the path prefix for the public Swagger UI.
8686
const docsPrefix = "/api/v1/admin/docs/"
8787

88+
// publicPrefix is the path prefix for unauthenticated public endpoints.
89+
const publicPrefix = "/api/v1/admin/public/"
90+
8891
// Handler provides admin REST API endpoints.
8992
type Handler struct {
9093
mux *http.ServeMux
@@ -128,7 +131,7 @@ func NewHandler(deps Deps, authMiddle func(http.Handler) http.Handler) *Handler
128131

129132
// ServeHTTP implements http.Handler.
130133
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
131-
if strings.HasPrefix(r.URL.Path, docsPrefix) {
134+
if strings.HasPrefix(r.URL.Path, docsPrefix) || strings.HasPrefix(r.URL.Path, publicPrefix) {
132135
h.publicMux.ServeHTTP(w, r)
133136
return
134137
}
@@ -174,6 +177,7 @@ func (h *Handler) registerSystemRoutes() {
174177
h.mux.HandleFunc("GET /api/v1/admin/tools/schemas", h.getToolSchemas)
175178
h.mux.HandleFunc("POST /api/v1/admin/tools/call", h.callTool)
176179
h.mux.HandleFunc("GET /api/v1/admin/connections", h.listConnections)
180+
h.publicMux.HandleFunc("GET /api/v1/admin/public/branding", h.getPublicBranding)
177181
h.publicMux.Handle(docsPrefix, httpswagger.Handler(
178182
httpswagger.URL(docsPrefix+"doc.json"),
179183
))

0 commit comments

Comments
 (0)