Rebuild the explorer as an app shell - #8
Merged
Merged
Conversation
The explorer was a scrolling document: a 1080px column, one 1200px breakpoint, and a Monaco host pinned to 360px. At rest two thirds of it was whitespace, and after a run the result table sat below a wire request nobody scrolled past. It is now a viewport-filling shell — a rail, a collapsible schema/history pane, tabs, the editor with its commands beside it, the wire request in a strip under it, and a tabbed output column with a status line. Each split is one ratio, dragged from JS and persisted. App.razor.cs sheds its non-UI half to Scry.Explorer.Core, where Scry.Explorer.Tests can reach it: ~310 lines that were only exercisable through a 60-second Playwright run now have unit tests. New: a schema pane over ScryIntrospection (sources by kind, member tables with the flags the contract carries, navigable types, search, per-source starter queries), tabs with derived titles and persistence, settings and short-keys dialogs, favorites/labels/search in the history, and a status line reporting the outcome, the transport, the row count and the elapsed time. The transport half had no surface before, though docs/explorer.md makes a point of it. MonacoAssets.targets, ported from BlazorQL, drops the assets a C# editor cannot reach — the 14 locale bundles and the ts/css/html workers. That is 9.96 MiB of 15.01, and it lands on the Scry.Server.Explorer package, whose EmbedExplorerUi globs this project's publish output. Three constraints the browser suite already asserted shaped the design: exactly one Monaco editor (so read-only panes stay <pre> + Highlight, and tabs swap the editor's content rather than its model), the wire request readable while the result table shows (so it is a strip, not an output tab — InnerText returns "" for a hidden element), and window.scry staying a classic global object, which InterceptDownloadsAsync patches and which the synchronous rehydrate needs. ExplorerSideBySide is gone with the breakpoint it existed to show; ExplorerSchemaPane takes its slot. The remaining baselines are reseeded.
`dotnet publish` on a single-TFM src project sets TargetFramework as a global property, and its implicit restore carries that down every ProjectReference to Scry.SourceGenerator, which multi-targets netstandard2.0. The generator's assets file comes back single-TFM and the *next samples build* fails with NETSDK1005 — far enough from the cause to be worth writing down, next to the shared-obj warning it belongs with. Recovery is `dotnet restore src/Scry.slnx`; a plain build will not re-restore, the assets file existing and looking current.
MemberKind.Collection is "aggregable but neither traversable nor projectable",
and it is published with IsNavigation *false* — so filtering on navigations,
attachments and sensitive members let it through. The button on Order produced
Query.Order.Select(_ => new { _.Amount, _.Id, _.Region, _.Tags })
which the editor compiled and the server then rejected with "Projection member
must reference a scalar value." Department was the same, through Employees.
Two test gaps let it past: the unit test built its query from a model whose
source had no collection, and the browser test used Holiday, which has neither a
collection nor an attachment. Both now cover a source that has one — the browser
test over Holiday, Order and Department.
Also corrects what the pane and the docs say about sensitive members. The server
does not refuse one: projecting it answers no-store, and comparing against it
sends the request as a body rather than a URL. It is left out of a suggested
query by choice, not by rule.
A projection's members are the result's columns, and a row of them across one line reads as none. Both the query the schema pane offers and a new Format button now write the chain down the page and the projection down the page after it, from one printer — so the generated shape and the formatted shape cannot drift. A line is broken only where breaking it says something. The chain breaks because each operator is a step and a projection because each member is a column; a predicate stays on the one line it reads as. The declarations ahead of the query are left as written, comments and all. The starter query now projects *into* navigations rather than skipping them, which is the only way a query can carry one, one level deep so a self-navigation terminates. Scalars come first: a nested object is several lines tall, and burying the row's own columns between two of them makes the shorter half the harder to read. Text that does not parse is reported rather than rewritten — a formatter guessing at a half-typed query produces a differently half-typed one, and ParseExpression stops at the first token it cannot continue from, so trailing garbage would otherwise parse "successfully" as its own prefix and be silently dropped. The printer emits LF rather than Environment.NewLine: the snippet lives in a Monaco model read with EndOfLinePreference.LF, and output that followed the host would have the schema pane offer different bytes on Windows than on CI. Note the screenshot baselines went stale without failing. UseSsimForPng compares structurally, and a single toolbar icon in a 1400x860 frame stayed under the threshold — so the five explorer captures were reseeded deliberately rather than by a failing test.
A byte[] is bulk bytes whichever way it travels, and opening on one is a poor suggestion: the sample's Department.Logo turned up base64 in the starter query for Employee, reached through the Department navigation. By declared type rather than by a flag, because the contract publishes none: [BinaryTransfer] deliberately does not change the queryable surface — that is the whole of what the attribute claims, and why an attachment moves the schema stamp and a diverted byte[] does not. So the suggestion cannot tell a diverted byte[] from an inline one, and has no reason to; the inline one is the worse of the two to open with anyway. The four exclusions now share one predicate rather than being spelled out twice, once for the row and once for a navigation's members. The test model gains Department.Logo so the exact-output assertions cover it both ways.
CS1573 fires on a member whose XML comment documents some parameters but not all, and only where a documentation file is generated — which ProjectDefaults turns on by packing the src projects on any Release build. A Debug build says nothing, so these four sat undocumented until someone built Release. Also picks up the collection-expression assertions in the two test files.
Two things, and the second is the smaller. The style pass: private consts go camelCase (Step -> step, NewLine -> newLine), patterns merge rather than nest, ProjectDefaults joins Scry.Explorer.Core so the global usings come from the package the rest of the tree gets them from — which retires the hand-declared Cancel/CancelSource aliases and lets BinaryResponseReader spell its parameter Cancel like everywhere else. Tests take collection expressions. The fix: CS1573 fires on a member whose XML comment documents some parameters but not all, and only where a documentation file is generated. Four in Scry.Explorer.Core were undocumented, and a Debug build says nothing about them.
…o explorer-shell
CI died with "MONO_WASM: sbrk failed to allocate", 91 tests into the suite, and everything after it failed to even load the page. Nothing was closing the pages: NewPageAsync opened one per test and the browser kept every one of them alive for the whole fixture. A page holds a fully booted WASM runtime — Roslyn included, untrimmed and interpreted, because the explorer needs the interpreter — so the suite walked the agent out of memory, tens of tests after the ones that spent it. That fitted at 72 tests and does not at 95, but the leak was there either way; the new tests only found it. Pages are now closed at teardown, after the console reporting that reads them. Local runs cannot reproduce the exhaustion — this machine has the headroom the CI agent does not — so the fix is reasoned from the leak rather than from a red test going green. All 95 still pass, in the same four minutes.
Every [Test] here pays for a fresh page, and a page means booting Blazor WebAssembly and the in-browser Roslyn: around nineteen seconds on a CI agent against under four on a developer machine. Split one assertion per test, that bought isolation a linear sequence of assertions against one screen does not need, at four minutes of CI. Tabs, chrome (panes, dialogs, shortcuts), schema browsing, the queries the schema offers, history curation and formatting each collapse to one test. Nothing is dropped — every assertion moves across. What keeps its own page is what needs one: a test that reloads, one that clears the stored state, and one that drags a pane, none of which the others should find already done to them. 83 tests where there were 95, and the same coverage.
Mirrors what src/appveyor.yml did, on the shape BlazorQL uses: windows-latest, setup-dotnet reading global.json, a cached Playwright browser download, and the received snapshots uploaded when something fails so a rejected baseline can be looked at rather than reproduced locally first. Windows for two reasons rather than one: the screenshot baselines were rasterized there, and the SQL-backed suites need LocalDB. The runner images carry it as a VS component (Microsoft.VisualStudio.Component.SQL.LocalDB.Runtime, on both windows-2022 and windows-2025), which is not a guarantee — so a check step names that as the reason up front instead of leaving it to surface fifty tests later as a connection timeout. The three trees build one at a time, as they must: they share the src projects' obj directories, and a concurrent build deletes the global usings ProjectDefaults materializes there. The suites then run with `!cancelled()` so one failing suite does not hide the others, gated on the build having succeeded so a broken build fails once rather than four times.
Tag-driven, on the shape BlazorQL uses: the tag is the version, the suites run before anything is pushed, and nuget.org is reached through trusted publishing so no API key is stored anywhere. One difference worth knowing. BlazorQL packs explicitly after building; Scry does not need to, because ProjectDefaults packs the src projects on any Release build — so `dotnet build src/Scry.slnx -c Release -p:Version=x` *is* the pack, and all six packages land in nugets/ from it. That is also why only that build carries the version: the nupkgs are written and zipped there, so the later trees rebuilding the src assemblies underneath them cannot reach inside one. Verified locally against -p:Version=9.9.9-ci.1: six packages, all named for the version passed. A release runs the suites rather than trusting the ci run on the same commit — a tag is the last point at which finding out is cheap.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The explorer was a scrolling document: a 1080px column, one 1200px breakpoint, and a Monaco host pinned to 360px. At rest two thirds of it was whitespace, and after a run the result table sat below a wire request nobody scrolled past.
It is now a viewport-filling shell — a rail, a collapsible schema/history pane, tabs, the editor with its commands beside it, the wire request in a strip under it, and a tabbed output column with a status line. Each split is one ratio, dragged from JS and persisted. App.razor.cs sheds its non-UI half to Scry.Explorer.Core, where Scry.Explorer.Tests can reach it: ~310 lines that were only exercisable through a 60-second Playwright run now have unit tests.
New: a schema pane over ScryIntrospection (sources by kind, member tables with the flags the contract carries, navigable types, search, per-source starter queries), tabs with derived titles and persistence, settings and short-keys dialogs, favorites/labels/search in the history, and a status line reporting the outcome, the transport, the row count and the elapsed time. The transport half had no surface before, though docs/explorer.md makes a point of it.
MonacoAssets.targets, ported from BlazorQL, drops the assets a C# editor cannot reach — the 14 locale bundles and the ts/css/html workers. That is 9.96 MiB of 15.01, and it lands on the Scry.Server.Explorer package, whose EmbedExplorerUi globs this project's publish output.
Three constraints the browser suite already asserted shaped the design: exactly one Monaco editor (so read-only panes stay