Skip to content

macOS: draw the Azure tab's scope tree #194

Description

@FrodeHus

Follow-up to #186, which landed on Windows and the CLI in #193. The macOS Azure tab still lists flat.

The logic is already ported and pushed to azure-scope-hierarchy-macos (branched off azure-scope-hierarchy, the #193 branch). What is left is the SwiftUI layer.

⚠️ The Swift on that branch has never been compiled. It was written on a Windows machine with no Swift toolchain. swift test is the first thing to run — the tests mirror the C# suite case for case, so they should find anything the transliteration got wrong. Treat the logic as a head start, not as working code.

What is on the branch

  • macos/Sources/ElevateCore/Support/ArmScope.swift — reads an ARM path as its steps; isAtOrUnder, glob matches (delegates to the existing ArmActions.covers), displayName(detail:), label, tail.
  • macos/Sources/ElevateCore/Support/ScopeTree.swift — ScopeNode, ScopeTreeEntry, build(_:) and flatten(_:isCollapsed:).
  • PanelFilter.matches(query:role:tenantName:upn:) now also searches the ARM path.
  • Tests for all three.

What is left

1. AppModel state and accessors — mirror windows/src/Elevate.App.Model/ViewModels/AppModel.Panel.cs:

  • collapsedScopes: Set<String> on AppModel, alongside collapsedTenants. In-memory, not persisted, and scopes start open — same as tenants, so nothing is hidden on first sight.
  • azureTree(for:) → ScopeTree.build(roles(for: tenantKey, tab: .azure)). Building from the already-filtered rows is what makes search narrow the tree instead of sitting beside it.
  • scopeNodeKey(_:_:) — "\(identityId)|\(tenantId)|\(node.scope)". Per tenant, because two accounts can reach the same scope.
  • isScopeCollapsed(_:_:) — returns false while isFiltering, so a match is never hidden behind a node closed earlier.
  • toggleScope(_:_:).
  • subtreeKeys(_:), subtreeState(_:) → .none / .some / .all, toggleSubtree(_:). A partly chosen subtree fills up rather than emptying — the checkbox is offering the rest.
  • canSelect(_:) — worth porting deliberately. canActivate only rules out view-only Entra roles; the "is this row's checkbox enabled" rule lived in the view. Without it the subtree checkbox selects rows whose own checkbox is disabled. It is: canActivate(key) && (assignment == nil || assignment is .failed).

2. The view — TenantRoles in macos/Sources/ElevateApp/Views/TenantSection.swift currently does ForEach(roles) { RoleRow(role: $0) }. On .azure it should walk ScopeTree.flatten instead, emitting a scope header or a RoleRow per entry, with a leading inset from entry.depth.

A new ScopeRow view needs: a disclosure chevron, the dimmed ancestors prefix ahead of title, ArmScope.label(kind), a roleCount on the right, and — in select mode — a three-state checkbox bound to subtreeState. Tooltip/.help should carry the full scope.

windows/src/Elevate.App/Views/PanelItems.cs (ScopeRow, ScopeRowFor, AddScopeTreeRows) and the ScopeRowTemplate in PanelView.xaml are the reference. Windows caps the indent at 3 steps of 12 pt; the panel is narrow and a management group path is long, so the same restraint applies.

3. Docs — docs/activating-roles.md is the macOS guide and says nothing about this yet; the Azure section and probably the Searching section want a paragraph. Windows got its version in windows/README.md. The CHANGELOG entry for #186 currently ends "The macOS Azure tab still lists flat; its tree follows" — drop that sentence and widen the entry to macOS.

Two things worth knowing before starting

Management groups sit beside subscriptions, not above them. #186 assumed the whole tree falls out of the scope string. It does 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, so the eligibilities alone cannot say which subscriptions belong to which management group. Nesting them needs extra ARM calls. Pinned by buildPutsManagementGroupsBesideSubscriptionsRatherThanAboveThem.

Two foldings keep the tree from costing more rows than it saves. Without them the motivating case gets worse — sixty subscriptions with one eligibility each would become a hundred and twenty rows. flatten already does both; the view just has to respect them:

  • a scope that only passes through (no eligibility of its own, one way down) is folded into the node below and named via ancestors, so it reads "Alpha / prod";
  • a scope leading to a single role gets no header at all, and its role row stands where the header would have.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions