feat(tower): UX improvements across pages and modals - #95
Merged
Conversation
- Switch default button radius from rounded-full (pill) to rounded-lg (6px) for the Button component and four custom-styled call sites - Update radius token descriptions: lg is now the canonical button radius; pill is reserved for chips, badges, dots, avatars, and segmented controls - Convert the Projects page Filter button to an icon-only circle with a corner count badge - Convert the single project Edit button to an icon-only circle - Add a Plus icon to the New project button
- Pair del/add hunks into rows with placeholders so left/right line up index-for-index instead of relying on independent filter passes - Wrap long lines (whitespace-pre-wrap break-all) and let the parent scroll container handle vertical overflow, so the bounce effect applies to both columns at once instead of only the active side - Drop the inner panel borders and per-column padding in favor of a single divide-x rule between sides - Move overflow-y-auto out of JsonDiff onto callers that actually want internal scrolling; PublishModal opts in via its className. On the snapshot page sticky now walks up to the page scroll container so the Before/After headers stay pinned while reviewing the diff
- FilterButton: icon-only circular trigger with optional active-count badge - SearchFilterPopover: generic search-by-text filter popover that uses FilterButton as the trigger - Refactor ProjectsFilterPopover to delegate to SearchFilterPopover so the existing tests and call sites keep working without changes
- Add optional Scopes property to UpdateClientRequest; when provided, it replaces the existing scope context - New UpdateClientValidator validates dimensions and values against the configured scopes (mirrors CreateClient validation) - UpdateClientHandler applies scope changes by mutating the existing dictionary in place and emits one audit FieldChange per dimension that was added, removed, or had its value changed - Add tests for the happy path and the unknown-dimension rejection - Regenerate OpenApi.json and Tower TypeScript types
- Move projectId from useCreateClient hook param into the mutation body so the same hook serves both per-project and global Create flows - Add useUpdateClient mutation hook (with conflict handling) - NewClientModal accepts an optional projectId; on the global Clients page where it is omitted, the dialog renders a project picker and validates the selection - New EditClientModal: edits the client name and scope context; the owning project is shown read-only since clients cannot move between projects - Clients route header now shows the SearchFilterPopover next to a New client button, matching the Projects page layout - Replace the Manage column action with Edit and Open-project icon buttons; clicking the client name also opens the edit dialog - Open-project links to the project's Clients tab and uses a native title tooltip to describe the destination
…ine delete - Header actions: SearchFilterPopover next to the New scope button (Plus icon, matching the Projects page pattern); search filters by dimension or description - Card layout flattened to a stacked design: dimension name (mono) and Edit button on the top row, description below, allowed-value pills underneath - Allowed values render as bordered monospace tags both on the cards and inside the edit modal so the visual language stays consistent; modal pills are clickable buttons with an inline X to remove - Delete moved into the edit modal as a destructive action with a type-to-confirm AlertDialog (mirrors the EditClientModal pattern); removed the standalone DeleteScopeDialog now that the flow lives inside the edit dialog
…pages - Replace per-item bordered cards with a single bordered container of divided rows, mirroring the Projects page layout - Each row is a role=button with click and Enter/Space handlers that open the edit dialog directly; per-row Edit buttons are gone - Clients row: name in <project> [active] on top, scope tags below, Created/Updated/Expires (when set) in a small caption row; right column shows relative Last used time and the navigate-to-project icon button (with stopPropagation so clicking it doesn't trigger the row's edit handler) - Scopes row: dimension on top, allowed-value tags directly below, description last; tags now use the same bg-bg-selected / text-fg-on-selected colours as the client scope tags for visual consistency, and the editable remove-pills inside the edit modal follow the same scheme
Adopts the projects-style divided list pattern on the Variables page and introduces a shared RevealButton extracted from EntryValue. Each row now shows the variable name, sensitive chip, owner pill (green 'GLOBAL' for ungrouped/unscoped vars), description, and a config-style value panel that supports masking, on-demand decryption, and copy. The edit modal gains a reveal button on the value input and decrypts automatically when unchecking 'Sensitive value' so the user never sees the literal '***'.
Consolidates variable structure, ownership tiers, two-tier resolution, group/system-wide visibility, sensitivity, uniqueness rules, sharp edges, and worked examples into a single guide. Cross-links from the concepts page.
Splits the Templates route into a list page and per-template detail page
following the same divided-list pattern used by Clients/Scopes/Variables,
and reuses the project Configuration tab's flat/tree/JSON views to manage
template entries.
Generalises ConfigFlatView, ConfigTreeView and ConfigJsonView to accept a
ConfigOwner ({ kind: 'project' | 'template'; id }) instead of a projectId.
useOwnedEntries dispatches to useEffectiveEntries for projects (with
inheritance/overrides) and to a flat ConfigEntry list for templates with
the new 'template-self' source kind. JSON view drives off snapshot preview
for projects and raw entries for templates; entriesToResolvedDocument
accepts the broader ResolvableEntry shape so the same builder works for
both. useConfigEntries now disables the underlying query for empty owner
ids so the wrapper can call both hooks unconditionally.
The EntryModal renames its title to 'New/Edit Configuration', drops the
boxed background on the sensitive-value checkbox to match other modals,
and is keyed by entry id at the consumer so RHF/useFieldArray re-mount
with fresh defaultValues — fixes scope dimension/value selects rendering
empty when reopening an entry that has scoped overrides.
ConfigTreeView aligns the bordered tree container's height with the
detail panel via items-stretch + self-start, but only grows the left side
to match the right (not the other way around). EntryValue's empty-state
adds min-h-7 so the panel keeps the same height as the populated state.
Restrict /variables to globals (system-wide and group-owned) and move project-tier variables to a new Variables tab on each project. Project variables can no longer be created from the global page; the project tab also surfaces inherited globals and shadow indicators. Add ScopedValuesField, a shared scoped-overrides editor consumed by both EntryModal and the new VariableEditorModal, so saving a variable no longer overwrites scoped variants with a single unscoped value. Extend Badge with an icon prop, mono tone, and selected variant; the mono tone uses leading-none to align uppercase text with the icon.
Sweep page titles, modal/alert dialog titles, sidebar nav, section headings, table column headers, and the Variable flow button labels so every visible title follows the design language's Title Case rule (e.g. "Edit Configuration", not "Edit configuration"). Body text, form field labels, and dynamic identifiers are unchanged.
The scoped-value dimension and value selects passed `{stringValue}` as
children to `<SelectValue>`. When the field's value transitioned from
empty to a real selection on the first pick, the manually-rendered
children stayed stale until the next render, so the trigger kept
showing the placeholder until a second selection. Letting Radix render
the matching item automatically via the controlled `value` prop fixes
the race.
Also remove the read-only Tier field from the Variable editor modal;
the modal description already conveys which tier is being edited.
…enhance scope key handling
…pedValuesField improvements
…riggerKey logic and improving SelectValue rendering
…tons in design tokens
The Scopes property is documented as replacing the existing scope context when provided. The handler was clearing scopes unconditionally before the null check, so any caller that omitted the field would silently lose all scope bindings. Move the Clear() call inside the null check so null means "no change" and an empty dictionary clears. Add tests pinning both behaviours.
The Edit Client modal was filling every configured scope dimension with its first allowed value when the modal opened, then sending the whole dictionary on save. Editing only the name on a client that was not bound to a dimension (or on a client created before the dimension existed) would silently add bindings the user never picked. Render an explicit "Unset" segment for each dimension and drop unset entries from the request body before calling UpdateClient.
…luesField generic The valuesAreMasked memo depended on the scopedValues array returned by form.watch, which is a fresh reference each render, so the memo never actually cached. Inline it as a plain expression. The decryptValues.isSuccess short-circuit was redundant because a successful reveal also resets the form fields away from '***'. Constrain ScopedValuesField's generic to forms that actually contain a scopedValues field array, and replace the per-call `as never` casts with typed Path/ArrayPath casts so misuse fails at compile time instead of relying on runtime hand-waving.
Add a test that opens the edit modal for a sensitive entry whose value comes back masked, asserts the form is locked and the reveal banner is visible, then clicks Reveal Sensitive Values and asserts the form unlocks with the decrypted value populated.
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
Iterates on Tower UX across most surfaces. Highlights:
VariableEditorModalandEntryModalnow mask***values, gate every editable field, and require an explicit "Reveal Sensitive Values" round-trip before save. While masked, the form (including type and description on entries) is fully read-only.FilterButton,SearchFilterPopover,RevealButton, and the multi-rowScopedValuesFieldso config entries and variables share one editor surface.UpdateClientnow accepts and validates a scope-context update so the Edit Client dialog can mutate scopes; added handler tests.docs/guide/variables.mdreference page and a Concepts cross-link.