finos / finos/architecture-as-code

calm-hub-ui: represent the namespace hierarchy in the Explore rail (collapsible dot-prefix tree)

Open
#2,787 0 comments 0 reactions 1 assignee Claimed by @YoofiTT96 View on GitHub
calm-hub-ui
Dominant language
TypeScript
Stars
399
Forks
138
Avg merge
2d 14h
Merged PRs (30d)
37

Description

## Feature Proposal

### Target Project:
`calm-hub-ui/` — the CALM Hub **Explore rail** (`src/hub/components/explore-rail/ExploreRail.tsx`, `RailItem.tsx`, `RailSectionLabel.tsx`, `CountBadge.tsx`).

### Description of Feature:
Represent the **namespace hierarchy** in the Explore rail as a collapsible dot-prefix tree, so nested namespaces (`finos.calm`, `finos.traderx`) sit *beneath* their parent (`finos`) rather than as flat siblings.

This addresses feedback from **@markscott-ms** on the redesign epic #2754:

> "how do the hierarchy of namespaces get represented? Still nested? … [they] are no longer nested … `finos.calm` and `finos.traderx` are not 'beneath' `finos`, which may become unwieldy in a large enterprise."

The redesign (#2761–#2766) replaced the old Explore tree with a flat one-level rail. That reads cleanly at demo scale but loses the "these belong together" grouping the tree afforded, and does not scale to an enterprise with hundreds of namespaces several dot-levels deep.

A design exploration (produced in Claude Design — *CALM Namespace Hierarchy Browser*) compared three directions and recommends **Direction A: a collapsible dot-prefix tree, with the filter flattening to matches (the hybrid)**. The current flat state is captured in `current-state-flat-rail.png` on the `design/namespace-hierarchy` branch of the rocketstack-matt fork.

**Recommended design — the live rail** (`finos` expanded with `calm` / `traderx` nested beneath it, active row, indent guides, own-count pills):

![Recommended nested rail — finos expanded with calm and traderx nested beneath, indent guides and count pills](https://raw.githubusercontent.com/rocketstack-matt/architecture-as-code/design/namespace-hierarchy/namespace-tree-recommended.jpg)

**Every state** — a collapsed branch showing the ghost `+8` nested-count chip; filtering `tra` (full names shown so `traderx` vs `finos.traderx` stay distinct, `finos` auto-expanded as ancestor, match highlighted); and the collapsed rail (roots as initials):

![States: collapsed branch with +N chip, filtering with highlighted matches and auto-expanded ancestor, and the collapsed rail as initials](https://raw.githubusercontent.com/rocketstack-matt/architecture-as-code/design/namespace-hierarchy/namespace-tree-states.jpg)

### User Stories:
- As a **platform architect in a large org**, I want namespaces grouped by their dotted prefix so I can scan and collapse whole areas of the catalogue instead of scrolling a flat list of hundreds.
- As a **CALM Hub user**, I want to open a parent namespace's *own* page **and** expand its children independently, because a prefix like `finos` both owns artefacts and parents others.
- As a **user searching**, I want to type a fragment and see every matching namespace at any depth surface immediately, with its ancestors expanded, without losing which full namespace each match is.

### Current Limitations:
`ExploreRail` renders a flat list — `filteredNamespaces.map(nc => )` — with `label` set to the full dotted name. There is no tree construction, no expand/collapse, and no parent/child relationship; hierarchy information latent in the dotted names is discarded.

### Proposed Implementation:

**Recommended design — Direction A (collapsible dot-prefix tree + search-flatten hybrid).** All required design elements:

**Tree derived purely from the dotted name.** Split each namespace on `.`; `a.b.c` nests under `a.b` under `a`. No parent field exists or is needed. Sort children alphabetically (`localeCompare`) at each level.

**Two hit targets per row — the key to parent-is-also-a-leaf:**
- **Chevron** — toggles children only; a separate target that never navigates.
- **Label** — navigates to `/namespace/:ns` (the prefix's *own* page). Stays a router ``, so the active row remains URL-derived and **one row = one namespace** (highlight can't drift).

**Counts — two visually distinct pills, never ambiguous:**
- **Own count** — *solid* pill (accent when active, grey otherwise); identical meaning to today (artefacts in *this* namespace); shown on every real row.
- **Nested count** — *ghost* (outlined, muted) `+N` pill; **collapsed-only**; sums artefacts hidden in descendants (e.g. collapsed `finos` shows `+8` = 5 + 3). Disappears on expand, since they're then visible.

**Legibility at the narrow rail width (~236px):**
- **Prefix elision** — collapsed/normal view shows a child's last segment only (`calm` under `finos`); full dotted name on `title` hover.
- **Indent guides** — a hairline vertical rule per depth level.

**Filtering** — typing in "Filter namespaces": matches surface at any depth, **ancestors auto-expanded**, **full names shown** (so `traderx` vs `finos.traderx` stay distinct), and the matched substring is highlighted.

**Synthetic parent (no own page)** — if `platform.payments.ledger` exists but `platform` does not, derive `platform`/`payments` as group-only rows: muted italic, **no own pill, label not clickable — only the chevron works**.

**Collapsed rail** — roots become single-letter initials; the active namespace keeps its accent bar; hover opens a fly-out of that subtree.

**Mobile (rail → drawer)** — one code path, breakpoint switch. Two enlarged tap zones both ≥44px: the chevron becomes a ~40px column with a hairline divider, the label fills the rest. **Tap chevron → expand/collapse in place, drawer stays open** (browsing structure); **tap label → navigate to `/namespace/:ns` and dismiss the drawer** (chosen a destination). Expansion state persists across open/close.

**Tunable props (A/B without a rebuild):** `accent` colour, `elidePrefixes` (bool), `showNestedChips` (bool).

**Recommended rail — expanded (`finos` open, active):**
```
NAMESPACES
▾ finos [10]
│ calm 5
│ traderx 3
workshop 13
traderx 6
ai-governance-v2 2
timeline-demo 6
```
**Collapsed branch (`finos` closed) — own count stays, ghost chip flags the hidden subtree:**
```
▸ finos (+8) [10]
workshop 13

```
**Filtering "tra" — matches at any depth, full names, ancestor auto-expanded:**
```
▾ finos [10]
│ finos.·tra·derx 3
·tra·derx 6
```
**Row anatomy (collapsed parent):** `▸` `finos` `(+8 ghost)` `[10 solid]` — chevron · label(→ /namespace/finos) · nested-count · own-count.

- **Technical design considerations:** build the tree in `ExploreRail` (or a small `useNamespaceTree(namespaceCounts)` hook) from `namespaceCounts`; flatten to a render list honouring expansion + filter; keep expansion in local state (persisted across the mobile drawer open/close). No new data model — hierarchy is a pure function of the dotted names already returned by `/api/calm/namespaces/counts`.
- **API changes:** none. Nested counts are derived client-side by summing descendants.
- **Data model changes:** none.
- **Dependencies:** `CONTROL DOMAINS` section stays flat and unchanged.

### Alternatives Considered:
- **B — Indented grouping, no collapse** (always-visible children with elided prefix). Zero clicks, but doesn't scale (hundreds of rows still scroll) and a header that is *also* a namespace muddies "which row is selected." *Rejected as the primary; A degrades toward it.*
- **C — Drill-down + breadcrumb** (roots only; click a prefix to descend). Scales best for very wide/deep trees but adds navigation steps and hides cross-group context; overkill at today's single-dot-level scale. *Kept as a "focus" affordance A can borrow for pathologically deep subtrees.*
- **Do nothing (keep flat).** Rejected — the concern @markscott-ms raised (enterprise scale) only grows.

### Testing Strategy:
- **Unit (vitest):** tree construction from dotted names (incl. synthetic parents, `traderx` vs `finos.traderx` disambiguation); expand/collapse toggles only the chevron's subtree; label navigates to `/namespace/:ns` and is the single active row; own vs nested count pills (nested only when collapsed, sums descendants, hidden when expanded); prefix elision + full-name `title`; filter surfaces matches at any depth with ancestors expanded and full names; synthetic-parent row is not clickable.
- **Interaction/a11y:** chevron and label are distinct, keyboard-reachable targets; mobile tap zones ≥44px; drawer expansion persistence.
- Regression: `CONTROL DOMAINS` section unchanged.

### Documentation Requirements:
Update any Explore-rail screenshots in the CALM Hub docs to show the nested tree.

### Implementation Checklist:
- [ ] Design reviewed and approved
- [ ] Implementation completed
- [ ] Tests written and passing
- [ ] Documentation updated
- [ ] Relevant workflows updated (if needed)
- [ ] Performance impact assessed

### Additional Context:
- Follow-up to the redesign epic #2754; addresses @markscott-ms's nested-namespace comment there.
- Design exploration (three directions, recommendation, live rail across all states, mobile flow, count/parent-as-leaf anatomy) produced in Claude Design — *CALM Namespace Hierarchy Browser*.
- Current flat-rail screenshot + brief on the `design/namespace-hierarchy` branch (rocketstack-matt fork): `current-state-flat-rail.png`, `namespace-hierarchy-brief.md`.
- Out of scope: hiding zero-count artefact-type tabs on the namespace page (a separate point in the same #2754 thread) — the current "auto-select first non-empty type" behaviour is intentional and kept.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.