Skip to content

Group the Azure tab by scope, and let one click take a subtree - #193

Merged
FrodeHus merged 1 commit into
mainfrom
azure-scope-hierarchy
Sep 19, 2026
Merged

FrodeHus merged 1 commit into
mainfrom
azure-scope-hierarchy

Conversation

@FrodeHus

Copy link
Copy Markdown
Owner

Closes #186.

Azure resource eligibility does not scale in a flat list. Someone eligible for Contributor on sixty subscriptions reads sixty sibling rows with similar names and differing paths, then clicks sixty times before the single activation Elevate promised them. The ARM scope string already carries the hierarchy, so nothing extra has to be fetched to show it.

Scoped to the .NET surfaces. The macOS Azure tab still lists flat; its tree follows in a separate PR — I have no Swift toolchain on this machine and would rather not ship a SwiftUI tree I could not compile.

What changed

New in Elevate.Core/Support

  • ArmScope reads an ARM path as the steps it is, and answers IsAtOrUnder, glob matching and left-truncating Tail from them.
  • ScopeTree builds the management group / subscription / resource group / resource tree from a tenant's eligibilities and flattens it back into rows.

Two shapes keep the tree from costing more rows than it saves — without them the motivating case gets worse, since sixty subscriptions with one eligibility each would become a hundred and twenty rows:

  • a scope that only passes through (no eligibility of its own, one way down) is folded into the node below it and named there, Alpha / prod;
  • a scope leading to a single role gets no header at all.

Windows Azure pivot draws that tree in the panel's existing single list, as ScopeRow items with an indent, so nothing became a TreeView. Each header opens and closes and says how many roles sit under it, and in select mode carries a three-state checkbox that takes the whole subtree at once. Search narrows the tree rather than sitting beside it, now reaches the whole ARM path, and keeps matches in place even under a header that was closed.

CLI gains --under <scope> and a glob form of --scope (* crosses slashes, as ArmActions already reads action strings).

Three judgement calls worth a look

Search was already there. PanelFilter and the filter box exist on both platforms — the issue predates them. I extended it to match the full ARM path (it only saw the caption) and left the rest alone.

--all is not in the issue, but the CLI ask does not work without it. --under and a glob --scope only narrow candidates; a name that still matches twelve roles remains an error, which is exactly the twelve-subscription case the issue wants scripted. elevate activate --all takes every match instead, and with no name at all takes every eligible role the filters leave. Existing single-match semantics are untouched, and it is on activate only.

Management groups sit beside the subscriptions, not above them. The issue expected the whole tree to fall out of the scope string; that holds for subscription → resource group → resource, but ARM writes a management group scope as its own flat path and never repeats it in a subscription's. Nesting them would need extra ARM calls. This is called out in the option help, both READMEs and the changelog, and pinned by a test.

Testing

Core 555, App 180, CLI 224 — all passing; the WinUI app builds clean with warnings as errors. New coverage: ArmScopeTests, ScopeTreeTests, AppModelScopeTreeTests, ScopeFilterTests.

SubtreeKeys turned up a real inconsistency while I was writing those: CanActivate only rules out view-only Entra roles, so a subtree checkbox would have selected rows whose own checkbox the view had disabled. The rule that lived in PanelListBuilder.Fill is now AppModel.CanSelect, and both use it.

Note for review

This leaves PanelFilter diverged until the macOS PR lands: the C# copy searches the ARM path and the Swift one does not. The follow-up should port ArmScope, ScopeTree and that one-line filter change together.

🤖 Generated with Claude Code

A flat list does not scale: someone eligible for Contributor on sixty
subscriptions reads sixty sibling rows and clicks sixty times before the
single activation Elevate promised them. The scope string already carries
the hierarchy, so nothing extra has to be fetched to show it.

ArmScope reads an ARM path as the steps it is, and ScopeTree builds the
management group / subscription / resource group / resource tree from a
tenant's eligibilities and flattens it back into rows. Two shapes keep the
tree from costing more rows than it saves: a scope that only passes through
is folded into the node below it and named there ("Alpha / prod"), and a
scope leading to a single role gets no header at all.

The Windows Azure pivot draws that tree, each header opening and closing and
saying how many roles sit under it, with a three-state checkbox in select
mode that takes the whole subtree at once. Search narrows the tree rather
than sitting beside it, reaches the whole ARM path, and keeps matches in
place even under a header that was closed.

For scripts, --under takes everything at or below a scope, compared step by
step so /subscriptions/abc never swallows /subscriptions/abcdef, and --scope
becomes a glob when it carries a *. Neither is enough on its own, because a
name matching twelve roles is still an error, so activate --all takes every
match instead of insisting on one.

Management groups sit beside the subscriptions rather than above them, in
the panel and for --under: ARM writes a management group scope as its own
flat path and never repeats it in a subscription's, so the eligibilities
alone cannot say which subscriptions belong to which management group.

The macOS Azure tab still lists flat; its tree follows. That leaves
PanelFilter diverged for now — the C# copy searches the ARM path and the
Swift one does not.

Refs #186

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@FrodeHus
FrodeHus merged commit 5a19ddd into main Sep 19, 2026
17 checks passed
@FrodeHus
FrodeHus deleted the azure-scope-hierarchy branch September 19, 2026 00:17
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.

Azure tab: scope hierarchy, search and subtree selection

1 participant