Skip to content

Tower UI sweep: variables/audit redesign, config key edits, scope-key canonicalization - #99

Merged
mrtaikandi merged 25 commits into
mainfrom
develop
May 15, 2026
Merged

Tower UI sweep: variables/audit redesign, config key edits, scope-key canonicalization#99
mrtaikandi merged 25 commits into
mainfrom
develop

Conversation

@mrtaikandi

@mrtaikandi mrtaikandi commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • Redesign the Tower Variables pages (global and per-project) and the Audit page as list + detail layouts with toolbar-style filtering, hover-revealed actions, and details modals.
  • Move Audit under the Admin section of the sidebar, swap the inline filters for a popover (multi-select entity dropdown + date range defaulting to the last 7 days), make table columns resizable and fit the available width on first paint, and open changes in a JSON-diff dialog.
  • Replace the snapshot Active/Previous toggle with a searchable Compare picker.
  • Improve the Config tree/list view: enable column resizing, allow editing the entry key inline, and add group-create / template navigation actions.
  • Canonicalize scope keys on write in the API and render the canonical dimension in Tower so casing variants resolve consistently.
  • Fix the Clients tab skeleton overlapping progressively-loaded rows.
  • Tune the dark-mode JSON diff colors so additions and deletions read as vivid emerald and coral instead of muddy washes.

mrtaikandi added 25 commits May 15, 2026 09:13
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.
@mrtaikandi
mrtaikandi merged commit bebd86c into main May 15, 2026
3 checks passed
@mrtaikandi
mrtaikandi deleted the develop branch May 15, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant