feat(ui): shareable deep links, rendered detail views, component-detail 502 fix - #665
Conversation
…component; map NOT_FOUND to HTTP 404 over serve The AI Service indexes the public component catalog only, so a private or deprecated component the project can run (keboola.mcp-server-tool, keboola.data-apps) 404'd in 'component detail' while 'component list' showed it -- and kbagent serve surfaced that as HTTP 502 Bad Gateway. - ComponentService.get_component_detail: an AI Service NOT_FOUND now falls back to the project's Storage component catalog, same response shape, discriminated by a new documentation_source key present on both paths. NOT_FOUND is still raised when both sources miss; non-404 AI failures are never masked. - serve: KeboolaApiError with ErrorCode.NOT_FOUND maps to HTTP 404 (session codes stay 401, everything else stays 502). - Human-mode component detail prints a source note for the fallback. - Docs: gotchas.md + commands-reference.md + CLAUDE.md + web-server.md, tagged (since vNEXT) per the release process.
…D web UI Every place the SPA answered a click with a raw JSON.stringify dump now renders a structured Overview with the untouched payload one tab away, and the URL finally encodes where you are so a link can be shared. Deep links (hash-based -- the REST API owns root paths, so history-mode routing would collide): #/<page> page without project context #/p/<project>/<page> project-scoped page ?branch=<id> non-default branch ?sel=<encoded> page-owned selected object - New src/router.ts (pure parse/build, 38 vitest tests) + useHashSelection hook; state.tsx seeds from the hash, syncs it back (pushState for page changes so Back works, replaceState for selection), applies hashchange. - A URL alias unknown to this install falls back to the default project and drops the stale branch/sel instead of erroring every page. - Adopted by Projects, Configs, Components, Data Apps, Jobs, Storage (tab + object), Streams, Flows. - @tanstack/react-router removed from package.json -- declared, never imported. Rendered detail views (pattern set by the Jobs drawer): - New shared primitives: DetailTabs, KeyValueGrid, PillList, RawDetail (Overview / Raw JSON tabs + copy-JSON with non-secure-origin fallback). - Drawer: optional wide prop + expand toggle stopping at the sidebar. - Projects: raw dump -> wide drawer with project/token cards, feature pills, limits table, open-in-Keboola link. - Configs: raw dump -> metadata grid, per-key parameter pills over the configuration body, rows table, collapsed state. - Components: raw dump -> metadata, markdown long_description, schema summary, catalog-fallback note driven by documentation_source. - Data Apps: inline list-row dump -> wide drawer fetching the real detail route, Overview / Logs (tail 200, tab-activated) / Raw tabs; delete confirm moved from window.confirm to ConfirmModal. react-query now runs with networkMode 'always': the SPA only ever talks to the localhost origin that served it, so the online/offline heuristic could silently pause queries and freeze the UI while serve kept working. CI: frontend.yml gains the npm test step its own comment reserved for the first real test file.
| // | ||
| // A project pinned by the URL (`#/p/<alias>/...`) WINS: the UI state is | ||
| // seeded from the hash before the first render, so `project` is already set | ||
| // by the time this effect runs and the early return leaves it alone. The one | ||
| // exception is an alias this installation does not know -- a link shared from | ||
| // someone else's machine, or a project since renamed/removed. Every page | ||
| // keys its queries off the alias, so keeping it would render nothing but | ||
| // errors; fall back to the default and drop the branch/selection that were | ||
| // scoped to it. | ||
| useEffect(() => { | ||
| if (project) return; | ||
| const projects = projectsQ.data?.projects; | ||
| if (!projects?.length) return; | ||
| if (project && projects.some((p) => p.alias === project)) return; | ||
| const def = projects.find((p) => p.is_default) ?? projects[0]; | ||
| // `setProject` clears the selection; the branch id is project-scoped too. | ||
| setProject(def.alias); | ||
| }, [project, projectsQ.data, setProject]); | ||
| if (project) setBranchId(null); | ||
| }, [project, projectsQ.data, setProject, setBranchId]); |
There was a problem hiding this comment.
🔍 Deep-linked drawer can briefly open for an unknown project
For a shared link like #/p/<unknown>/jobs?sel=123 with an unregistered alias, TopBar falls back to the default project and clears sel, but each page's one-time restore effect can run first and open a drawer scoped to the unknown alias. The page's local selected is not cleared by the fallback, so a drawer with an errored detail fetch can linger. Timing-dependent (list-query settle order), so not flagged as a definite bug; documented intent is to open with no drawer.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Confirmed and fixed in 44cc249. The Jobs page was the only adopter whose restore path opened a drawer without a list hit (the deliberate synthetic-row fallback for jobs outside the 100-row cap). That fallback is now gated on the list query having actually loaded — an errored list (the unknown-alias case, right before TopBar swaps to the default project) drops the deep link instead of pinning an errored drawer to a project that is being swapped away. Verified live: #/p/<unknown>/jobs?sel=... now lands on the default project's Jobs page with no drawer; a valid-project link to an old job still opens the synthetic drawer as designed.
…st errored Devin review: a shared link with a project alias this install does not know (#/p/<unknown>/jobs?sel=...) could open the synthetic job drawer against the unknown alias before TopBar's fallback swapped to the default project, leaving a drawer with an errored detail fetch behind. The synthetic fallback is now gated on the list having actually loaded (valid project, job merely outside the 100-row cap); an errored list drops the deep link instead. Verified live: the foreign link now lands on the default project's Jobs page with no drawer.
The deep-link restore effect from #665 was mount-oriented: a one-shot ref meant a `?sel=` written by the command palette while Storage was already open (or against a stale, still-filtered tables list) was consumed without opening anything, stranding the drawer shut. Split it into two effects: one adopts an externally-written sel (tab, bucket filter -- dropping a filter that cannot contain the target table), one opens the table once a list containing it has loaded, with no give-up flag (a closed drawer rewrites sel, so it cannot re-open). Verified live: palette table jump with a matching filter (kept), with a conflicting filter (dropped), and cold reload of both URL forms.
The command palette now finds storage objects: the active project's buckets and tables plus every registered project's buckets, fetched on palette open (react-query cache shared with the Storage page; cross-project bucket fan-out at 60s staleTime) and fuzzy-matched locally, so typing never waits on the network. Cross-project tables are deliberately not loaded (reachable via their bucket). Picking an object navigates by writing the Storage page's ?sel= selection via its own grammar helper, so a palette jump produces the same shareable URL as clicking the row. The sel grammar gains a bucket/<bucketId> form (the filter chip now survives reload); a non-empty query always offers a final 'Search across projects' row that jumps to the Search page and auto-runs via a pendingSearchQuery hand-off slot. Also fixes the mount-only assumption in the #665 restore effect so a sel written into an already-mounted Storage page (palette retarget, incompatible bucket filter) applies. New storageSel.test.ts vitest suite (11 tests).
* chore(release): 0.90.0 Bumps pyproject.toml to 0.90.0 and adds the changelog entry covering every PR merged since v0.89.0 (#658, #662, #661, #663, #665, #666, #664, #668, #667, #623), resolves the vNEXT placeholders those PRs left behind, and adds the curated What's new reel for the release. * docs(web-server): keep the What's-new anchor stable across releases The '### What's-new popup *(since vNEXT)*' heading put the version gate in the heading itself, so resolving the placeholder to 0.90.0 changed the generated slug to 'whats-new-popup-since-0900' and broke the in-page link at line 138 -- and would have broken it again on every future release. Moved the '(since 0.90.0)' tag to the first body line: the anchor is now the stable 'whats-new-popup', the gate stays visible, and check_version_gates.py still sees it (it scans the whole file, not just headings).
UI/UX audit follow-up for the NERD web UI (
kbagent serve --ui), driven by a live click-through against real e2e projects. Three complaints, three fixes — plus one server-side bug the audit surfaced.1. Shareable deep links (hash router)
The URL never changed while navigating, so a state seen in the UI could not be sent to a colleague. Now:
GET /projectsis JSON), so history-mode routing would collide with it; the hash needs zero server changes and survives the static SPA mount.src/router.ts— pureparseHash/buildHash, no dependencies, 38 vitest tests (round-trips, encoded/garbage input, unknown-page fallback). First test file inweb/frontend—frontend.ymlnow runsnpm test(the step its own comment reserved for this moment).replaceState(a row click never costs an extra Back press).hashchangeis applied back to state, so Back/Forward and hand-edited URLs work.branch/selinstead of leaving every page erroring.@tanstack/react-routerremoved frompackage.json: declared, never imported anywhere.2. Rendered detail views (Overview / Raw JSON everywhere)
Detail views that answered a click with a bare
JSON.stringifydump now render a structured Overview, with the untouched payload one tab away (nothing the API returned is hidden). Pattern and styling follow the existing Jobs drawer / Streams panel — no new design language.open in Keboola UI →configurationbody, rows table, collapsedstatelong_description, schema summary, catalog-fallback notewindow.confirmdeleteConfirmModaldeleteShared primitives added once and reused:
DetailTabs,KeyValueGrid,PillList,RawDetail(Overview/Raw tabs + copy-JSON with a non-secure-origin fallback).Drawergains an optionalwideprop and an expand toggle that stops at the sidebar — existing consumers are byte-identical in behavior.3. Component detail 502 (server)
GET /components/keboola.mcp-server-tool?project=…answered 502 Bad Gateway for components the project can demonstrably run (they're in the list). Root cause:component detailreads the AI Service, which indexes the public catalog only; private/deprecated components 404 there, and the serve error handler mapped everyKeboolaApiErrorto 502.ComponentService.get_component_detail: an AI Service NOT_FOUND now falls back to the project's Storage component catalog — same response shape, newdocumentation_source: "ai_service" | "storage_catalog"discriminator on both paths. NOT_FOUND still raised when both sources miss; non-404 AI failures are never masked. This fixes the CLI command too.ErrorCode.NOT_FOUNDnow maps to HTTP 404 (session codes stay 401, the rest stays 502) — an upstream "does not exist" is not a gateway fault.Also
react-queryruns withnetworkMode: "always": the SPA only ever talks to the localhost origin that served it, so the online/offline heuristic was never right — a browser that (correctly or spuriously) reports offline silently paused queries and froze the UI with no spinner and no error whilekbagent servekept working.docs/web-server.md(deep links + 404 mapping),gotchas.md+commands-reference.md+CLAUDE.mdfor the component-detail fallback, all tagged(since vNEXT)per the docs: version bumps move out of feature PRs into dedicated release PRs #648 release process. No version bump, no changelog entry (release PR's job).Verification
make check— 6017 passed, 12 skipped (includes new tests: service fallback ×5, route 404/502 mapping, smoke handler).web/frontend:tsc --noEmitclean,vite buildclean,vitest38/38.kbagent serve --uiagainst real e2e projects (light + dark): cold-reload restore of deep links on Projects/Configs/Components/Data Apps/Jobs/Storage/Streams, Back/Forward behavior, catalog-fallback note onkeboola.mcp-server-tool(200 instead of 502), Logs tab error surfacing, copy-JSON fallback.