Repository navigation
Say which seat this viewer has, once, instead of one refusal at a time (#2479) - #2483
Conversation
#2479) #2400 asked why a viewer on a jumpbox refuses "Current Active Queries", and #2403 answered by rewriting the individual refusals. Four better messages, and the same discovery model: you learn your seat at the moment a write is denied, which is the worst moment to learn it. The viewer has known the answer since V8 - one has_table_privilege probe at connect - and never showed it anywhere you could look on purpose. This gets worse in UAT rather than better, which is item 4. The two defaults are deliberately opposite: a local seat takes postgres.connectAs, which defaults to admin (read-write), while a seat reached over the LAN takes postgres.network.role, which defaults to viewer (read-only). A UAT tester connecting from their own desktop is the second case by construction, so the read-only seat is the EXPECTED default rather than a misconfiguration - and nothing in the product said so. The onboarding now states it; the product still did not. So the status bar gains a "Seat:" field, first among the fixed fields because it is the only one describing THIS CONNECTION rather than the fleet, with the whole answer in its tooltip: what a "write" means here (a row in the MONITORING store, never anything sent at a monitored SQL Server), which config key decided it, that the two defaults are opposite on purpose, and how to change it. Same probe, same V8 fail-safe - this only makes the verdict visible before a write is attempted. Unreachable is a state of its own and never collapses into read-only. DetectReadOnlyAsync fails safe to read-only for an unexpected RESPONSE but raises ViewerStoreUnreachableException for a failure to REACH the store, which #2117 built deliberately, because collapsing them told an operator whose service was simply down to go and change a database role. A status field is exactly where that gets quietly re-collapsed, so the state machine carries four values (Unknown, Unreachable, ReadOnly, ReadWrite), the unreachable tooltip actively denies the read-only reading and names no role at all, and the schema-skew arm - which reached the store but never asked the seat question - leaves the field at "Seat: --" rather than claiming a verdict nothing measured. StoreIsOnThisMachine only ORDERS the two sentences by which default is likelier to have decided this seat. It cannot claim which one did: a non-loopback store host does not prove a remote seat (#2279 - a bring-your-own store on another host, reached from the service host, reads identically), so the likelier default leads and both are always stated. Colours reuse the status bar's existing vocabulary rather than inventing one: ErrorBrush for an unreachable store, the same severity an erroring collector gets; WarningBrush for a read-only seat, visible because a tester who does not notice it is back to discovering the seat one refusal at a time, but not alarming because on a remote seat it is correct; muted for read-write and not-yet-probed, which are the unremarkable states. The wording lives in a pure helper so it is pinned by tests rather than by reading a XAML file, and the connect flow's arms are pinned structurally - the helper cannot be wrong about a state nobody sets, and a later refactor that adds an early return is exactly how the field goes stale and shows the previous verdict, which is worse than showing none. The status bar's column count is pinned against its field count for the same reason: a TextBlock at Grid.Column="5" with five columns defined stacks silently on column 4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed the diff ( Correctness
Lite/Darling parity
Style / tests
No security, injection, or performance concerns — this is pure UI text/state plumbing with no external input. |
ApplySeatState is called from inside the catch that handles an unreachable store, and a ResourceReferenceKeyNotFoundException thrown from a catch block is unhandled. All three brushes exist in all three shipped themes, so this is not doubt about the keys - it is the call site. A missing key in some future theme would take the viewer down at the one moment it most needs to stay up and explain itself, which is a poor trade for a colour. TryFindResource with the muted foreground as the fallback, and the field's existing brush as the last resort. The sibling status fields keep the hard FindResource because none of them is written from an exception handler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewed the diff ( Scope check: this is Darling-only (the read-only/read-write seat concept comes from the Postgres Correctness:
Style: C# conventions (naming, async/await, XML doc on the new public/internal surface, one class per file) look consistent with CONTRIBUTING.md's C# section. No T-SQL touched by this PR. No correctness, parity, or security issues found. Nothing blocking from this pass. |
Items 3 and 4 of #2479, shipped together because they are one user experience.
The gap
#2400 asked why a viewer on a jumpbox refuses "Current Active Queries". #2403 answered it by rewriting the individual refusals — four better messages, and the same discovery model: you learn your seat at the moment a write is denied, which is the worst moment to learn it. The viewer has known the answer since V8 (one
has_table_privilege('config_mute_rules','INSERT')probe at connect,ViewerDataService.DetectReadOnlyAsync) and never showed it anywhere you could look on purpose.Item 4 is why this gets worse in UAT rather than better. The two defaults are deliberately opposite:
postgres.connectAsadminpostgres.network.roleviewerA UAT tester connecting from their own desktop is the second row by construction, so the read-only seat is the expected default rather than a misconfiguration — and nothing in the product said so.
What this adds
A
Seat:field in the status bar, first among the fixed fields because it is the only one describing this connection rather than the fleet. The whole answer is in its tooltip: what a "write" means here (a row in the monitoring store, never anything sent at a monitored SQL Server), which config key decided it, that the two defaults are opposite on purpose, and how to change it. Same probe, same V8 fail-safe — this only makes the verdict visible before a write is attempted.Unreachable is its own state, and never collapses into read-only
DetectReadOnlyAsyncfails safe to read-only for an unexpected response but raisesViewerStoreUnreachableExceptionfor a failure to reach the store. #2117 built that split deliberately, because collapsing them told an operator whose service was simply down to go and change a database role. A status field is exactly where that gets quietly re-collapsed, so:Unknown,Unreachable,ReadOnly,ReadWrite;connectAs;Seat: --rather than claiming a verdict nothing measured.The
catch (ViewerStoreUnreachableException)arm's body is pinned to containViewerSeatState.Unreachableand to contain noViewerSeatState.ReadOnlyat all.Honest about what it can and cannot know
StoreIsOnThisMachineonly orders the two sentences by which default is likelier to have decided this seat. It cannot claim which one did — a non-loopback store host does not prove a remote seat (#2279: a bring-your-own store on another host, reached from the service host, reads identically), so the likelier default leads and both are always stated.Colours reuse the status bar's existing vocabulary rather than inventing one:
ErrorBrushfor an unreachable store (the same severity an erroring collector gets fromCollectorHealthText),WarningBrushfor a read-only seat — visible, because a tester who does not notice it is back to discovering the seat one refusal at a time, but not alarming, because on a remote seat it is correct — and the muted foreground for read-write and not-yet-probed.Verification
ViewerSeatIndicator.csitself.Darling.Testsisnet10.0-windowsand cannot run on macOS, so the file — which has no WPF dependency — was<Compile Include>'d into a throwawaynet10.0console harness and all 33 assertions executed against the real source: 33/33 pass, including that the four labels are distinct, that the unreachable label and tooltip contain no read-only claim, that both defaults are named in both placements, and that the ordering flips withStoreIsOnThisMachinewhile neither is claimed.Darling.Testsboth build clean, 0 errors.SeatTextahead ofCollectorHealthText.devthe whole test class fails to compile —ViewerSeatIndicatorandViewerSeatStatedo not exist there.viewer-role) seat and confirmSeat: read-onlyin the warning colour with the tooltip namingpostgres.network.role; stop the service and reconnect, and confirmSeat: not connectedrather thanSeat: read-only.Onboarding
Makes
docs/uat-onboarding.md§2.3's "How to tell which seat you have" subsection unnecessary (#2476) — the paragraph beginning "There is no global banner — it shows up per surface", along with its Settings-window recipe and the list of per-surface tells. §2.3's table of the two defaults, the "nothing is written to your monitored SQL Server" explanation and the "How to change it" recipe are all now in the tooltip too, so that section can shrink to a pointer at the status bar; keeping the prose is still reasonable, but it stops being the only place the answer exists.Co-Authored-By: Claude Opus 5 noreply@anthropic.com