feat(v1.1/W3): read-paths wired — 8 page surfaces on live hooks#7
Conversation
… / Resources / Prompts / Breakers
Wires the 8 read-path page surfaces in resources/js/pages/ to the W2
TanStack Query hooks. Every page surface enforces R14 (surface failures
loudly: visible loading / error / empty + retry), R11 (testid contract:
<page>-loading|error|empty|ready), and R15 (a11y: aria-busy / role=alert
/ role=status).
Page surfaces migrated:
- DashboardPage — useServers + useAudit + useBreakers
- ServersListPage — useServers({filters})
- ServerDetailPage — useServer + per-tab useServerTools / useResources /
usePrompts / useAudit({server_id})
- ToolsPage — useTools + useServers; per-tool ToolRecentCalls →
useAudit({tool_name})
- AuditPage — useAudit({filters}); AuditDrilldown → useAuditDetail
- BreakersPage — useBreakers (normalises wire half_open → UI half)
- ResourcesPage — useServers + useResources + useResource
- PromptsPage — useServers + usePrompts + usePrompt
New helper: resources/js/lib/data-state.tsx — <DataState> wrapper that
translates a UseQueryResult into the four R14 states with R11 testids +
R15 a11y attrs baked in. Reused across every page tab.
Excluded by design (stay on fixtures until W4): PlaygroundPage,
SettingsPage, HelpPage. lib/data.ts retained for those surfaces.
Tests: 66 → 131 (+65 new). 11 new files under tests/js/pages/ +
tests/js/lib/data-state.test.tsx. Smoke tests updated to provide
QueryClient + MSW handlers so they still pass post-swap. npm test +
npm run typecheck + npm run build all green.
Playwright E2E specs still run on fixture data; updating them to the
live-data layer is W5 scope — local Playwright failures during W3 are
expected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23c14779ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| </div> | ||
| )} | ||
| {!contentQ.isLoading && !contentQ.isError && previewTab === 'rendered' && content && selected.mime === 'text/markdown' && ( | ||
| <MarkdownRender source={content}/> |
There was a problem hiding this comment.
Sanitize live Markdown before rendering
When a live MCP server returns a text/markdown resource, this branch passes the server-controlled contentQ.data.content into MarkdownRender, which ultimately uses dangerouslySetInnerHTML without sanitizing raw HTML. A malicious or compromised server can advertise a Markdown file containing HTML such as an image with an event handler, causing script execution in the admin UI when an operator opens the rendered preview; escape or sanitize Markdown output before injecting it.
Useful? React with 👍 / 👎.
| // fixture so the drawer still renders a useful narrative — flagged in the | ||
| // banner when the auditId doesn't match the fixture id. | ||
| const live = detailQ.data; | ||
| const detail = live ? { ...FALLBACK_AUDIT_DETAIL, ...live } : FALLBACK_AUDIT_DETAIL; |
There was a problem hiding this comment.
Map wire audit fields before rendering the drawer
For a successful GET /audit/:id response using the wire shape (mcp_server_name, tool_name, duration_ms, created_at), this spread does not populate the legacy fields that the drawer renders below (detail.server, detail.tool, detail.dur, detail.ts). In that scenario the drilldown shows the seeded fixture server/tool/timestamp/duration for the selected live audit row, so operators inspect misleading audit metadata; normalize the live detail into the keys used by the UI before merging with fixture-only fields.
Useful? React with 👍 / 👎.
| const filters = React.useMemo(() => { | ||
| const f = { per_page: 100 }; | ||
| if (q) f.q = q; | ||
| if (statusFilter !== 'all') f.status = statusFilter; |
There was a problem hiding this comment.
Don't send UI-only server filters as status
Selecting the Active or Disabled chips now sends /servers?...&status=active or status=disabled before the client-side filter runs, but the server status values in this codebase are ok/warn/err while disabled is represented by enabled. If the backend applies the query filter, active/disabled views will come back empty even when matching servers exist; only send real wire status values here, and use enabled=false or client-side filtering for disabled/active.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR migrates multiple SPA “read-path” pages from fixture-backed data to TanStack Query hook-backed live data, standardizing visible loading/error/empty behaviors and adding significant Vitest coverage to lock in the new state contracts and retry semantics.
Changes:
- Wire major page surfaces (Dashboard/Servers/Tools/Audit/Breakers/Resources/Prompts) to W2 TanStack Query read hooks.
- Introduce a shared
<DataState>wrapper to normalize loading/error/empty UI (and retry) across query-driven sections. - Expand and update JS test coverage (page-level specs + smoke/integration updates with MSW + QueryClient provider).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/js/smoke.test.tsx | Updates smoke tests to mount QueryClient and provide MSW handlers for hook-backed routes. |
| tests/js/pages/tools.test.tsx | Adds page-level ToolsPage state/behavior tests using MSW. |
| tests/js/pages/tools-recent-calls.test.tsx | Adds coverage for per-tool audit query inside ToolsPage. |
| tests/js/pages/servers.test.tsx | Adds ServersListPage + ServerDetailPage state/behavior tests. |
| tests/js/pages/server-detail-tabs.test.tsx | Adds coverage for ServerDetailPage tab queries (tools/prompts/audit). |
| tests/js/pages/resources.test.tsx | Adds ResourcesPage state tests including per-server resource tree states. |
| tests/js/pages/resources-content.test.tsx | Adds coverage for per-resource content preview states. |
| tests/js/pages/prompts.test.tsx | Adds PromptsPage state tests including per-server prompt list states. |
| tests/js/pages/integration.test.tsx | Adds integration sanity checks ensuring fixture-only strings don’t bleed into live-hook UI. |
| tests/js/pages/helpers.tsx | Adds shared render helpers (QueryClient + MemoryRouter + ToastProvider). |
| tests/js/pages/dashboard.test.tsx | Adds DashboardPage state tests and retry behavior verification. |
| tests/js/pages/breakers.test.tsx | Adds BreakersPage state tests and half_open normalization assertion. |
| tests/js/pages/audit.test.tsx | Adds AuditPage + AuditDrilldown state tests including retry coverage. |
| tests/js/lib/data-state.test.tsx | Adds unit tests for <DataState> loading/error/empty/ready + retry semantics. |
| resources/js/pages/tools.tsx | Migrates ToolsPage to useTools/useServers and wires per-tool recent calls via useAudit. |
| resources/js/pages/servers.tsx | Migrates Servers list/detail and detail tabs to hook-backed queries with fixture telemetry fallbacks. |
| resources/js/pages/resources.tsx | Migrates ResourcesPage + PromptsPage to hook-backed queries with per-pane state handling. |
| resources/js/pages/dashboard.tsx | Migrates Dashboard KPIs/failures/health/breakers to hooks with fixture telemetry fallbacks. |
| resources/js/pages/audit.tsx | Migrates AuditPage, AuditDrilldown, and BreakersPage to hook-backed queries and adds state handling. |
| resources/js/lib/data-state.tsx | Adds <DataState> wrapper for consistent query state rendering and retry. |
| CHANGELOG.md | Documents the W3 read-path wiring scope, helper introduction, and test coverage expansion. |
Comments suppressed due to low confidence (1)
resources/js/pages/resources.tsx:245
- The “No preview” empty-state renders whenever
!content, even while the content query is loading or in an error state. This can result in loading/error UI being shown together with the empty-state. Gate this branch on!contentQ.isLoading && !contentQ.isPending && !contentQ.isError(orcontentQ.isSuccess) so only one state renders at a time.
{previewTab === 'rendered' && !content && (
<EmptyState icon={<I.File size={20}/>}
title="No preview"
body={`This is a ${selected.mime} resource. Switch to Raw or Hex to inspect contents.`}/>
)}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {!contentQ.isLoading && !contentQ.isError && previewTab === 'rendered' && content && selected.mime === 'application/json' && ( | ||
| <pre className="code-block" dangerouslySetInnerHTML={{ __html: jsonHighlight(JSON.parse(content || '{}')) }}/> | ||
| )} |
| <tr><td colSpan={10} data-testid="servers-empty"> | ||
| <EmptyState | ||
| icon={<I.Server size={26}/>} | ||
| title="No servers match your filters" | ||
| body="Try clearing some filters or registering a new server." | ||
| action={<button className="btn" onClick={() => { setStatusFilter('all'); setTransportFilter('all'); setQ(''); }}>Clear filters</button>} | ||
| secondary={<button className="btn primary" onClick={() => onNav('servers-new')}><I.Plus size={12}/>New server</button>} | ||
| title={rawServers.length === 0 ? 'No servers yet' : 'No servers match your filters'} |
| <tr><td colSpan={9} data-testid="audit-empty"> | ||
| <EmptyState | ||
| icon={<I.Scroll size={26}/>} | ||
| title={rawRows.length === 0 ? 'No audit rows yet' : 'No rows match your filters'} | ||
| body={rawRows.length === 0 | ||
| ? 'Invoke a tool to generate audit history.' | ||
| : 'Try widening the filters or selecting a different time window.'} | ||
| action={rawRows.length > 0 | ||
| ? <button className="btn" onClick={clearFilters}>Clear filters</button> | ||
| : <button className="btn primary" onClick={() => onNav('tools')}><I.Play size={12}/>Open tools</button>} | ||
| /> |
| // Small, reusable wrappers that translate a TanStack `UseQueryResult` into the | ||
| // three R14-mandated visible states (loading / error / empty / ready) with the | ||
| // R11 testid contract baked in. Every page that consumes a live hook routes | ||
| // through one of these so behaviour stays uniform. | ||
| // | ||
| // <DataState query={q} testIdBase="dashboard" | ||
| // isEmpty={(d) => d.length === 0} | ||
| // ready={(d) => <Grid items={d} />} /> | ||
| // | ||
| // R14: never silently drop a non-success state on the floor — render either a | ||
| // visible loading affordance, an error message + retry, or an empty | ||
| // state. Loading shows `aria-busy`; error uses `role="alert"`; empty | ||
| // uses `role="status"`. | ||
| // R11: the wrapper attaches `data-testid="<base>-loading|error|empty"` so | ||
| // Playwright + Vitest can wait on them without sniffing internal markup. |
…re field mapping, filter shape, a11y, ready-testid Six review findings addressed: - P1 XSS: `MarkdownRender` now HTML-escapes the server-controlled source string before running the markdown regex chain. A malicious or compromised MCP server can no longer execute scripts in the admin UI via a `text/markdown` resource carrying raw HTML. - JSON preview no longer crashes the whole Resources page when the wire payload isn't valid JSON — wrapped in a `<JsonPreview>` with try/catch that surfaces an inline error banner instead. - `AuditDrilldown` now projects the wire `AuditDetail` shape (mcp_server_name / tool_name / duration_ms / created_at / tenant_id) onto the legacy fixture-shaped UI keys BEFORE the fixture spread merge, so the drawer no longer displays misleading fixture metadata for real audit rows. - `ServersListPage` no longer forwards UI-only chip values (`active` / `disabled`) as `?status=` to the BE — translates `disabled` to `enabled=false` and lets `active` fall through to client-side filtering. `err` / `warn` still forward. - R15 a11y: in-table empty-state cells in `servers.tsx` + `audit.tsx` now wrap their `EmptyState` in `<div role="status" aria-live="polite">` so screen readers announce them. - R11 ready-testid: `<DataState>` now wraps the ready branch in a `<div role="presentation" data-testid="<base>-ready">` sentinel, honouring the contract advertised in the header comment. New spec pins it. Vitest: 132/132 green locally. Playwright: still drives fixtures — W5 scope (unchanged from iter-1).
|
@copilot review |
| /> | ||
| </div> | ||
| ); | ||
| } |
| function DashboardPage({ liveEvents, livePaused, onTogglePaused, onClearFeed, onNav, onSelectAudit }) { | ||
| const serversQ = useServers(); | ||
| const auditQ = useAudit({ per_page: 24 }); | ||
| const breakersQ = useBreakers(); | ||
|
|
||
| // Compose three queries into a single ready-gate: render loading until the | ||
| // primary `servers` query lands; surface error if any of the three failed | ||
| // (servers is load-bearing; audit + breakers degrade to []). | ||
| if (serversQ.isLoading || serversQ.isPending) { |
| } from '../lib/ui'; | ||
| import { Breadcrumbs, ROUTES, SECONDARY } from '../components/shell'; | ||
| import { useServers, useAudit, useBreakers } from '../lib/queries/hooks'; | ||
| import { DataState } from '../lib/data-state'; |
| import { | ||
| useAudit, useAuditDetail, useBreakers, useServers, | ||
| } from '../lib/queries/hooks'; | ||
| import { DataState } from '../lib/data-state'; |
| TENANTS, | ||
| } from '../lib/data'; | ||
| import { |
| function ResourcesPage({ onNav }) { | ||
| const [serverId, setServerId] = React.useState('srv_01'); | ||
| const [selectedUri, setSelectedUri] = React.useState('mcp://openai/docs/readme.md'); | ||
| const serversQ = useServers(); | ||
| const liveServers = (serversQ.data?.data ?? []).filter(s => s.enabled !== false); | ||
| const [serverId, setServerId] = React.useState(null); | ||
| const [selectedUri, setSelectedUri] = React.useState(null); | ||
| const [previewTab, setPreviewTab] = React.useState('rendered'); | ||
| const [openDirs, setOpenDirs] = React.useState({ 'mcp://openai/docs/': true, 'mcp://openai/schemas/': false }); | ||
| const [openDirs, setOpenDirs] = React.useState({}); | ||
|
|
||
| // Auto-pick first server when the list lands. | ||
| React.useEffect(() => { | ||
| if (!serverId && liveServers.length > 0) { | ||
| setServerId(liveServers[0].id); | ||
| } | ||
| }, [serverId, liveServers.length]); | ||
|
|
||
| const items = RESOURCES[serverId] || []; | ||
| const resourcesQ = useResources(serverId); | ||
| const contentQ = useResource(serverId, selectedUri); | ||
| const items = resourcesQ.data ?? []; |
Reviewed. The requested review fixes are already present in commit |
…rs + drop dead imports + fix serverId flicker Six findings on iter-2: - tools.tsx: surface serversQ.isError as a page-level error with retry; previously fell through to fixture-derived grouping that misattributed seed names to live tool ids. - dashboard.tsx: secondary auditQ + breakersQ failures now surface via a page-level alert banner + a card-scoped error state inside RecentFailures. Previously silent degradation to [] read as "0 failures / all closed". - dashboard.tsx + audit.tsx: removed dead `DataState` imports (neither module routes through the wrapper). - resources.tsx: removed dead `TENANTS` import. - resources.tsx: derive `serverId` synchronously from liveServers in render instead of useState(null) + useEffect auto-pick. Eliminates the one-render flicker where the tree showed "No resources advertised" empty state for a frame after serversQ resolved. Vitest: 132/132 green locally.
|
@copilot review |
Reviewed. The six follow-up review findings are already addressed in commit |
Summary
Wires the 8 read-path page surfaces in
resources/js/pages/to the W2 TanStack Query hooks. Every page surface enforces R14 (surface failures loudly: visible loading / error / empty + retry), R11 (testid contract:<page>-loading|error|empty|ready), and R15 (a11y:aria-busy/role=alert/role=status).Page surfaces migrated
DashboardPageuseServers()+useAudit({per_page:24})+useBreakers()ServersListPageuseServers({q, status, transport, page, per_page})ServerDetailPageuseServer(id)+ per-tabuseServerTools/useResources/usePrompts/useAudit({server_id})ToolsPageuseTools()+useServers()for grouping; per-tooluseAudit({tool_name})AuditPageuseAudit({...filters}); drawer →useAuditDetail(auditId)BreakersPageuseBreakers()(normalises wirehalf_open→ UIhalf)ResourcesPageuseServers()+useResources(serverId)+useResource(serverId, uri)PromptsPageuseServers()+usePrompts(serverId)+usePrompt(serverId, name)New helper
resources/js/lib/data-state.tsx—<DataState>wrapper that translates aUseQueryResultinto the four R14-mandated visible states with the R11 testid contract + R15 a11y attrs baked in. Reused across every page tab.Excluded by design (stay on fixtures until W4)
PlaygroundPage— OpenAPI explorer, no live-data dependency.SettingsPage— wires in W4 alongsideuseCreateApiKey/useRevokeApiKey.HelpPage— static content.resources/js/lib/data.tsis RETAINED in the bundle because the excluded surfaces still import from it.Test coverage
tests/js/lib/data-state.test.tsx,tests/js/pages/*.test.tsx(10 specs).npm test+npm run typecheck+npm run buildall green locally.Test plan
Hard constraints honoured
X-Tenant-Id(R30).openai-mcp,github-mcp, …) verified absent from the live-data rendered tree inintegration.test.tsx.🤖 Generated with Claude Code