Tower UI sweep: variables/audit redesign, config key edits, scope-key canonicalization - #99
Merged
Conversation
Group rows expose a "+" action that opens the entry modal with the prefix pre-filled. Inherited entry rows now render a "Template" link button in place of the empty action slot, restoring value-column alignment and giving a one-click jump to the source template. Entry value, type badge, and scope count now share a single flex row so the value flows left-aligned right after the type badge.
Adds a required Key field to UpdateConfigEntryRequest with the same shape validation as create. The handler captures the previous key for audit, applies the rename, and translates a unique-index collision from the persistence store into a 409 Conflict mirroring the create-side duplicate handling. The MongoDB store now persists Key on update and lifts MongoWriteException(DuplicateKey) into DuplicateKeyException. This is a breaking API change: callers that previously omitted key on PUT will now receive 400.
Adds a --key option to "config-entry update" that, when supplied, asks the API to rename the entry; omitting it keeps the existing key. The TUI ViewModel forwards the current key on edits so its updates remain compatible with the now-required Key contract.
Enables the Key input in edit mode and threads the value through to the PUT body. Tightens the Zod regex to match the backend pattern (leading letter required) so the UI rejects the same shapes the API rejects rather than surfacing a 400 round-trip. Adds vitest coverage for the rename path and the leading-non-letter rejection.
Renames the flat view in the project and template config tabs to "List" so the label matches the icon already in use. Bumps the persisted tweaks store to version 1 with a migration that rewrites the previous 'flat' value to 'list' on rehydrate, so returning users keep the selected view instead of falling back to the default.
Wires up TanStack's column resizing on the list view: each column declares a default size and minSize, the table renders with table-fixed plus min-w-full so it stretches to fill the wrapper when the column sum is smaller and overflows with horizontal scroll when larger, and a thin grab handle on each header drives the live resize. The Owner column's badge now truncates with ellipsis (icon stays visible, full template name remains in the tooltip) so narrowing it no longer spills into the next column.
…rite Lookup each request scope key via IScopeStore and rewrite it to the stored Scope.Dimension casing before persisting, so legacy lowercase keys converge on the canonical form and clients can match by exact string. Unknown keys are kept verbatim.
…only by case Match scope dimensions case-insensitively so legacy entries written with lowercase keys still resolve to their canonical scope, and render the SelectItem with the form's stored value when it differs only by case so Radix's strict value comparison still picks the right option. Gate the "(deleted)" / "(no longer allowed)" labels behind isSuccess so the brief loading window does not flash stored values as deleted.
…pare picker Collapse the snapshot view segmented control to JSON + a Compare dropdown that lists every published snapshot with a search input, so users can diff the selected snapshot against any version instead of just the active or previous one. The picker defaults to the active snapshot (or the next one in the list when the active one is being viewed) and persists user preference through a v2 tweaks-store migration that rewrites the legacy 'diff'/'json-diff' modes to 'compare'.
…-loaded clients The global /clients page aggregates per-project queries via useQueries, so data fills in as each project resolves while isLoading stays true until the last one finishes. The skeleton and the partially-loaded list were rendering at the same time. Suppress the skeleton once any data has arrived. Also keep the per-project clients table container stable across loading so the body swaps between skeleton rows and real rows in place instead of replacing a fixed-height block.
Remove placeholder hints that just restate the field label, the variable-editor tier description preamble, and add top padding to the dialog footer so action buttons aren't flush against form fields.
Replace the stacked card layout with a two-pane view that mirrors the configuration tree: a single bordered list on the left with collapsible Project / Inherited sections and a detail panel on the right showing the selected variable's badges, default value, scoped overrides, and links. Move the filter input and the new-variable button into a Toolbar, drop the introductory caption, surface a hover-revealed Edit button on project rows and an Open-in-Globals link on inherited rows, and hide the Inherited section entirely when no globals apply to the project.
Mirror the project variables tab: two-pane layout with a left list split into Global / Group-owned sections, a detail panel on the right, and hover-revealed Edit actions on each row. The page header carries the search-filter popover and the new-variable button (matching the Scopes page), while the All / Global / Group-owned tier toggle sits in a Toolbar below.
…, and details modal - Move the page from /audit to /admin/audit and into the sidebar Admin section, renaming the label to "Audit". - Replace the inline entity-chip filters with a SearchFilter-style popover containing a multi-select entity dropdown and a date range, defaulting to the last 7 days. - Switch the table to resizable columns that scale to fill the available width on initial load. - Drop the Entity ID column, reveal "View changes" on row hover, and open an AuditDetailsDialog with the JSON diff when a row is clicked.
…ls fields - Brighten the dark-mode line backgrounds and gutter markers in tokens.json so additions and deletions read as vivid emerald and coral instead of dull washes. - Drop the InlineCode wrappers from the Actor and Entity ID fields in the audit details dialog so the values match the other inline fields.
…cription The handler's WithDescription text is authoritative; OpenApi.json had drifted to include ", including its key" which would not survive the next regen.
…lization ValidateScopesAsync and NormalizeScopeKeysAsync each made one IScopeStore lookup per unique dimension, so create/update paid 2N lookups. They are now a single ValidateAndCanonicalizeScopesAsync helper invoked from the handler; the validator drops its IScopeStore dependency and only checks request shape. Scope errors still surface as 400 ValidationProblem on the Values field — handler returns TypedResults.ValidationProblem with the same error messages the validator used to emit.
…ntil manual resize Date inputs and the default 7-day window now use UTC components, and from/to are sent as ISO timestamps bounded to UTC midnight so the calendar range matches server-side UTC timestamps regardless of the user's local timezone. The column fit-to-container effect keeps refitting on every container resize until the user drags a divider, instead of bailing after the first paint.
requestAnimationFrame ran before Radix's portaled PopoverContent mounted, so focus would silently miss the search input. Hand the focus over to Radix's onOpenAutoFocus hook and reset the query in onOpenChange so the input is always ready when the popover opens.
Terminal.Gui 2.1 replaced TabView/Tab with a single Tabs container where each tab is a View. Switch the field type, add tabs by adding the container view (with Title acting as the tab label), and track refresh state by View instead of the removed Tab type. SelectedTab/AddTab become Value/Add.
The 14.7.0 -> 14.7.1 client regen reordered pagination parameters from (limit, after, before, sortField, sortOrder) to (after, before, limit, sortField, sortOrder). NSubstitute Arg.Any<>() matchers are positional, so the existing mocks no longer compiled against the regenerated IGroundControlClient. Reshuffle the types to match each List handler's new signature; ListProjectsHandlerAsync also gained an ungrouped bool?.
Convert the static helper to a sealed partial class with IScopeStore injected via the constructor and register it as a singleton. The validators and handlers now resolve ConfigEntryValidation from DI and call its instance methods, so the scope store dependency lives in one place instead of being plumbed through every call site. KeyPattern and KeyPatternErrorMessage stay public const for the DataAnnotations attributes that consume them. CA1822 is downgraded to info in .globalconfig so the shape methods can live on the same service for API cohesion even though they do not touch instance state.
…or to transient Canonicalization in ValidateAndCanonicalizeScopesAsync used to silently overwrite when two keys in the same scoped value resolved to the same canonical dimension (e.g. "ENV" and "env"). Use TryAdd so the second key returns a ScopeValidationResult.Failure instead of dropping data. Also drop ConfigEntryValidation from Singleton to Transient to match the lifetime of its IAsyncValidator consumers.
… auto-fit Extract the duplicated Intl.DateTimeFormat helper from audit.tsx and AuditDetailsDialog into a shared lib/date-time.ts. Guard fitColumnsToContainer with lastFittedWidthRef so the ResizeObserver callback skips when the container width is unchanged. Previously the setColumnSizing-induced re-render produced an extra observer tick that only early-returned via the available <= baseTotal check. Drop the entityTypes.length > 1 gate on the audit prefetch trigger so single-filter sparse first pages also fill the viewport.
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.
Summary