Skip to content

Commit d167596

Browse files
authored
fix: admin UI round 3 — errors component, tool descriptions, docs (#99)
* fix: admin UI — reusable errors component, tool descriptions, editable insights, connection param - Extract RecentErrorsList into shared component used by Dashboard and Audit pages - Show tool description below tool name in Tool Inventory table - Make insight drawer always editable (review notes + approve/reject for all statuses) - Only send connection param when tool schema accepts it (fixes datahub_list_connections error) * docs: add Admin Portal page with screenshots - Add 6 screenshots to docs/images/screenshots/ (dashboard, tools overview, tools explore, audit events, knowledge overview, knowledge insights) - Create docs/server/admin-portal.md with visual walkthrough of all portal sections - Add Admin Portal to mkdocs.yml navigation under Features - Update admin-api.md Admin Portal section with screenshot and link - Add Admin Portal section to README.md with dashboard and explore screenshots - Update llms.txt and llms-full.txt with admin portal documentation * docs: add Admin Portal to homepage grid, reorganize nav sections * docs: update Admin Portal screenshots
1 parent 4521d97 commit d167596

18 files changed

+330
-107
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,22 @@ Add custom authentication, rate limiting, or logging. Swap providers to integrat
118118

119119
---
120120

121+
## Admin Portal
122+
123+
A built-in web dashboard for monitoring, auditing, and exploring the platform. Enable with `admin.portal: true`.
124+
125+
![Admin Dashboard](docs/images/screenshots/admin-dashboard.png)
126+
127+
**Dashboard** — Real-time activity timelines, top tools/users, performance percentiles, error monitoring, knowledge insight summary, and connection health.
128+
129+
![Tools Explore](docs/images/screenshots/admin-tools-explore.png)
130+
131+
**Tools Explore** — Interactive tool execution with auto-generated parameter forms, rendered results, and full semantic enrichment context (owners, tags, glossary terms, column metadata, lineage).
132+
133+
See the [Admin Portal documentation](https://txn2.github.io/mcp-data-platform/server/admin-portal/) for the complete visual guide.
134+
135+
---
136+
121137
## Use Cases
122138

123139
### Enterprise Data Governance
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState } from "react";
2+
import type { AuditEvent } from "@/api/types";
3+
import { StatusBadge } from "@/components/cards/StatusBadge";
4+
import { EventDrawer } from "@/components/EventDrawer";
5+
6+
interface RecentErrorsListProps {
7+
events: AuditEvent[] | undefined;
8+
}
9+
10+
export function RecentErrorsList({ events }: RecentErrorsListProps) {
11+
const [selectedEvent, setSelectedEvent] = useState<AuditEvent | null>(null);
12+
13+
if (!events || events.length === 0) {
14+
return <p className="text-sm text-muted-foreground">No recent errors</p>;
15+
}
16+
17+
return (
18+
<>
19+
<div className="space-y-2">
20+
{events.map((e) => (
21+
<div
22+
key={e.id}
23+
onClick={() => setSelectedEvent(e)}
24+
className="flex cursor-pointer items-start gap-2 rounded p-1 text-xs transition-colors hover:bg-muted/50"
25+
>
26+
<StatusBadge variant="error">Error</StatusBadge>
27+
<div className="min-w-0 flex-1">
28+
<p className="font-medium">{e.tool_name}</p>
29+
<p className="truncate text-muted-foreground">
30+
{e.error_message || "Unknown error"}
31+
</p>
32+
</div>
33+
<span className="shrink-0 text-muted-foreground">
34+
{new Date(e.timestamp).toLocaleTimeString()}
35+
</span>
36+
</div>
37+
))}
38+
</div>
39+
{selectedEvent && (
40+
<EventDrawer
41+
event={selectedEvent}
42+
onClose={() => setSelectedEvent(null)}
43+
/>
44+
)}
45+
</>
46+
);
47+
}

admin-ui/src/pages/audit/AuditLogPage.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { StatusBadge } from "@/components/cards/StatusBadge";
1212
import { TimeseriesChart } from "@/components/charts/TimeseriesChart";
1313
import { BreakdownBarChart } from "@/components/charts/BarChart";
1414
import { EventDrawer } from "@/components/EventDrawer";
15+
import { RecentErrorsList } from "@/components/RecentErrorsList";
1516
import { useTimeRangeStore, type TimeRangePreset } from "@/stores/timerange";
1617
import type { AuditEvent, AuditSortColumn, SortOrder, Resolution } from "@/api/types";
1718
import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
@@ -208,26 +209,7 @@ function OverviewTab() {
208209
{/* Recent Errors */}
209210
<div className="rounded-lg border bg-card p-4">
210211
<h2 className="mb-3 text-sm font-medium">Recent Errors</h2>
211-
{recentErrors.data?.data.length === 0 ? (
212-
<p className="text-sm text-muted-foreground">No recent errors</p>
213-
) : (
214-
<div className="space-y-2">
215-
{recentErrors.data?.data.map((e) => (
216-
<div key={e.id} className="flex items-start gap-2 text-xs">
217-
<StatusBadge variant="error">Error</StatusBadge>
218-
<div className="min-w-0 flex-1">
219-
<p className="font-medium">{e.tool_name}</p>
220-
<p className="truncate text-muted-foreground">
221-
{e.error_message || "Unknown error"}
222-
</p>
223-
</div>
224-
<span className="shrink-0 text-muted-foreground">
225-
{new Date(e.timestamp).toLocaleTimeString()}
226-
</span>
227-
</div>
228-
))}
229-
</div>
230-
)}
212+
<RecentErrorsList events={recentErrors.data?.data} />
231213
</div>
232214
</div>
233215
</div>

admin-ui/src/pages/dashboard/DashboardPage.tsx

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo } from "react";
1+
import { useMemo } from "react";
22
import { useTimeRangeStore, type TimeRangePreset } from "@/stores/timerange";
33
import {
44
useSystemInfo,
@@ -11,12 +11,12 @@ import {
1111
useInsightStats,
1212
useInsights,
1313
} from "@/api/hooks";
14-
import type { AuditEvent, Resolution } from "@/api/types";
14+
import type { Resolution } from "@/api/types";
1515
import { StatCard } from "@/components/cards/StatCard";
1616
import { StatusBadge } from "@/components/cards/StatusBadge";
1717
import { TimeseriesChart } from "@/components/charts/TimeseriesChart";
1818
import { BreakdownBarChart } from "@/components/charts/BarChart";
19-
import { EventDrawer } from "@/components/EventDrawer";
19+
import { RecentErrorsList } from "@/components/RecentErrorsList";
2020
import { formatDuration } from "@/lib/formatDuration";
2121

2222
const presets: { value: TimeRangePreset; label: string }[] = [
@@ -44,8 +44,6 @@ export function DashboardPage() {
4444
[preset],
4545
);
4646

47-
const [selectedEvent, setSelectedEvent] = useState<AuditEvent | null>(null);
48-
4947
const systemInfo = useSystemInfo();
5048
const overview = useAuditOverview({ startTime, endTime });
5149
const timeseries = useAuditTimeseries({ resolution: getResolution(preset), startTime, endTime });
@@ -204,30 +202,7 @@ export function DashboardPage() {
204202
{/* Recent Errors */}
205203
<div className="rounded-lg border bg-card p-4">
206204
<h2 className="mb-3 text-sm font-medium">Recent Errors</h2>
207-
{recentErrors.data?.data.length === 0 ? (
208-
<p className="text-sm text-muted-foreground">No recent errors</p>
209-
) : (
210-
<div className="space-y-2">
211-
{recentErrors.data?.data.map((e) => (
212-
<div
213-
key={e.id}
214-
onClick={() => setSelectedEvent(e)}
215-
className="flex cursor-pointer items-start gap-2 rounded p-1 text-xs transition-colors hover:bg-muted/50"
216-
>
217-
<StatusBadge variant="error">Error</StatusBadge>
218-
<div className="min-w-0 flex-1">
219-
<p className="font-medium">{e.tool_name}</p>
220-
<p className="truncate text-muted-foreground">
221-
{e.error_message || "Unknown error"}
222-
</p>
223-
</div>
224-
<span className="shrink-0 text-muted-foreground">
225-
{new Date(e.timestamp).toLocaleTimeString()}
226-
</span>
227-
</div>
228-
))}
229-
</div>
230-
)}
205+
<RecentErrorsList events={recentErrors.data?.data} />
231206
</div>
232207
</div>
233208

@@ -333,13 +308,6 @@ export function DashboardPage() {
333308
</div>
334309
)}
335310

336-
{/* Event Detail Drawer */}
337-
{selectedEvent && (
338-
<EventDrawer
339-
event={selectedEvent}
340-
onClose={() => setSelectedEvent(null)}
341-
/>
342-
)}
343311
</div>
344312
);
345313
}

admin-ui/src/pages/knowledge/KnowledgePage.tsx

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ function InsightDrawer({
699699
insight: Insight;
700700
onClose: () => void;
701701
}) {
702-
const [reviewNotes, setReviewNotes] = useState("");
702+
const [reviewNotes, setReviewNotes] = useState(insight.review_notes ?? "");
703703
const updateStatus = useUpdateInsightStatus();
704704

705705
const handleAction = useCallback(
@@ -880,12 +880,6 @@ function InsightDrawer({
880880
: "-"}
881881
</p>
882882
</div>
883-
{insight.review_notes && (
884-
<div className="col-span-2">
885-
<p className="text-xs text-muted-foreground">Review Notes</p>
886-
<p className="text-sm">{insight.review_notes}</p>
887-
</div>
888-
)}
889883
</div>
890884
)}
891885

@@ -912,39 +906,37 @@ function InsightDrawer({
912906
</div>
913907
)}
914908

915-
{/* Action buttons (only for pending insights) */}
916-
{insight.status === "pending" && (
917-
<div className="space-y-3 border-t pt-3">
918-
<div>
919-
<label className="mb-1 block text-xs text-muted-foreground">
920-
Review Notes
921-
</label>
922-
<textarea
923-
value={reviewNotes}
924-
onChange={(e) => setReviewNotes(e.target.value)}
925-
placeholder="Optional review notes..."
926-
rows={3}
927-
className="w-full rounded-md border bg-background px-3 py-2 text-sm outline-none ring-ring focus:ring-2"
928-
/>
929-
</div>
930-
<div className="flex gap-2">
931-
<button
932-
onClick={() => handleAction("approved")}
933-
disabled={updateStatus.isPending}
934-
className="rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 disabled:opacity-50"
935-
>
936-
Approve
937-
</button>
938-
<button
939-
onClick={() => handleAction("rejected")}
940-
disabled={updateStatus.isPending}
941-
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
942-
>
943-
Reject
944-
</button>
945-
</div>
909+
{/* Action buttons */}
910+
<div className="space-y-3 border-t pt-3">
911+
<div>
912+
<label className="mb-1 block text-xs text-muted-foreground">
913+
Review Notes
914+
</label>
915+
<textarea
916+
value={reviewNotes}
917+
onChange={(e) => setReviewNotes(e.target.value)}
918+
placeholder="Optional review notes..."
919+
rows={3}
920+
className="w-full rounded-md border bg-background px-3 py-2 text-sm outline-none ring-ring focus:ring-2"
921+
/>
946922
</div>
947-
)}
923+
<div className="flex gap-2">
924+
<button
925+
onClick={() => handleAction("approved")}
926+
disabled={updateStatus.isPending}
927+
className="rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 disabled:opacity-50"
928+
>
929+
Approve
930+
</button>
931+
<button
932+
onClick={() => handleAction("rejected")}
933+
disabled={updateStatus.isPending}
934+
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
935+
>
936+
Reject
937+
</button>
938+
</div>
939+
</div>
948940
</div>
949941
</div>
950942
</div>

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useToolSchemas,
66
useCallTool,
77
} from "@/api/hooks";
8+
import type { ToolSchema } from "@/api/types";
89
import type {
910
ToolCallResponse,
1011
ConnectionInfo,
@@ -58,9 +59,11 @@ export function ToolsPage({ initialTab }: { initialTab?: string }) {
5859
function OverviewTab() {
5960
const { data: toolsData } = useTools();
6061
const { data: connectionsData } = useConnections();
62+
const { data: schemasData } = useToolSchemas();
6163

6264
const tools = toolsData?.tools ?? [];
6365
const connections = connectionsData?.connections ?? [];
66+
const schemas: Record<string, ToolSchema> = schemasData?.schemas ?? {};
6467

6568
const hiddenToolSet = useMemo(
6669
() => new Set(connections.flatMap((c) => c.hidden_tools ?? [])),
@@ -117,17 +120,23 @@ function OverviewTab() {
117120
<tbody>
118121
{tools.map((tool, idx) => {
119122
const isHidden = hiddenToolSet.has(tool.name);
123+
const desc = schemas[tool.name]?.description;
120124
return (
121125
<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">
126+
<td className="px-3 py-2">
127+
<span className="flex items-center gap-1.5 font-mono text-xs">
124128
{isHidden ? (
125129
<EyeOff className="h-3 w-3 shrink-0 opacity-40" />
126130
) : (
127131
<Eye className="h-3 w-3 shrink-0 opacity-40" />
128132
)}
129133
<span className={isHidden ? "opacity-50" : ""}>{tool.name}</span>
130134
</span>
135+
{desc && (
136+
<p className="mt-0.5 pl-[18px] text-[11px] leading-snug text-muted-foreground">
137+
{desc}
138+
</p>
139+
)}
131140
</td>
132141
<td className="px-3 py-2">
133142
<StatusBadge variant="neutral">{tool.kind}</StatusBadge>
@@ -265,10 +274,15 @@ function ExploreTab() {
265274
setHistory((prev) => [entry, ...prev]);
266275
setLatestResult(null);
267276

277+
// Only send connection when the tool's schema accepts it — tools like
278+
// datahub_list_connections have an empty input schema and reject unexpected
279+
// properties.
280+
const sendConnection = "connection" in properties ? selectedConnection : "";
281+
268282
callTool.mutate(
269283
{
270284
tool_name: selectedTool,
271-
connection: selectedConnection,
285+
connection: sendConnection,
272286
parameters: params,
273287
},
274288
{
99.7 KB
Loading
321 KB
Loading
212 KB
Loading
580 KB
Loading

0 commit comments

Comments
 (0)