Private/Show-AACAssignedPolicyView.ps1

function Show-AACAssignedPolicyView {
    <#
    .SYNOPSIS
        Renders Get-AACAssignedPolicy's inventory (ConvertTo-AACAssignedPolicy)
        as a Spectre.Console view.
    .DESCRIPTION
        The scope; tiles (assignments, initiatives, policies, parameters and
        how many are assigned, assignments not enforced, inherited, custom);
        a table of the assignments by scope - definition, enforcement,
        parameters assigned and the resource types; a bar chart of the
        resource types with the most assignments; and each assignment's
        parameters as a tree: assigned values in green, defaults in grey,
        parameters with no value in orange. An assignment with more than
        -ShowDefault parameters lists the assigned ones and counts the rest,
        which keep their defaults (-NoDisplay or -HtmlPath has every one).
        Output goes straight to the Spectre console; wrap the call in
        Invoke-AACPagedOutput to page it.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable] $Inventory,

        [System.Collections.IDictionary] $Scope,

        # Up to this many parameters, defaults are listed too.
        [int] $ShowDefault = 12
    )

    $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) }
    $glyph = Get-AACGlyph
    $unicode = [Spectre.Console.AnsiConsole]::Profile.Capabilities.Unicode
    $stats = $Inventory.Stats
    $assignments = @($Inventory.Assignments)
    $shorten = { param([string] $Text, [int] $Max) if ($Text.Length -le $Max) { $Text } else { $Text.Substring(0, $Max - 3) + '...' } }
    $types = {
        param([string] $Text, [int] $Show = 2)
        if (-not $Text) { return '[grey50]-[/]' }
        if ($Text -eq 'All' -or $Text.StartsWith('All except')) { return "[grey62]$(& $escape (& $shorten $Text 60))[/]" }
        $list = @($Text -split ', ')
        $shown = ($list | Select-Object -First $Show | ForEach-Object { "[turquoise2]$(& $escape ($_ -replace '^Microsoft\.', ''))[/]" }) -join "`n"
        if ($list.Count -gt $Show) { $shown += "`n[grey50]+$($list.Count - $Show) more[/]" }
        $shown
    }
    $scopeColor = @{ 'Management group' = 'mediumpurple2'; Subscription = 'deepskyblue1'; 'Resource group' = 'springgreen2'; Resource = 'gold1'; Other = 'grey62' }

    $facts = [System.Collections.Generic.List[string]]::new()
    if ($script:AACSession) { $facts.Add("[white]$(& $escape $script:AACSession.Account)[/]") }
    if ($Scope) { foreach ($key in $Scope.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Scope[$key])") } }
    $facts.Add((Get-Date).ToString('d MMM yyyy HH:mm'))
    Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]"
    [Spectre.Console.AnsiConsole]::WriteLine()

    if (-not $assignments.Count) {
        Show-AACPanel -Content '[bold]No policy assignments[/] [grey58]were found in this scope.[/]' -BorderColor 'grey50' -AllowMarkup
        return
    }

    # --- Tiles ------------------------------------------------------------------------------------------
    Show-AACTileRow -Tile @(
        @{ Value = '{0:N0}' -f $stats.Assignments; Caption = 'assignments'; Color = 'deepskyblue1' }
        @{ Value = '{0:N0}' -f $stats.Initiatives; Caption = 'initiatives'; Color = 'mediumpurple2' }
        @{ Value = '{0:N0}' -f $stats.Policies; Caption = 'single policies'; Color = 'turquoise2' }
        @{ Value = "$('{0:N0}' -f $stats.Assigned) / $('{0:N0}' -f $stats.Parameters)"; Caption = 'parameters assigned'; Color = 'springgreen2' }
        @{ Value = '{0:N0}' -f $stats.DoNotEnforce; Caption = 'not enforced'; Color = $(if ($stats.DoNotEnforce) { 'orange1' } else { 'green3' }) }
        if ($stats.Inherited) { @{ Value = '{0:N0}' -f $stats.Inherited; Caption = 'inherited'; Color = 'gold1' } }
        @{ Value = '{0:N0}' -f $stats.Custom; Caption = 'custom definitions'; Color = 'grey70' }
    )
    [Spectre.Console.AnsiConsole]::WriteLine()

    # --- The assignments ----------------------------------------------------------------------------------
    $table = [Spectre.Console.Table]::new()
    $table.Border = [Spectre.Console.TableBorder]::Rounded
    $table.BorderStyle = [Spectre.Console.Style]::Parse('deepskyblue3_1')
    $table.Expand = $true
    $table.Title = [Spectre.Console.TableTitle]::new("[bold]$($glyph.Bullet) Policy assignments[/] [grey58]$($glyph.Dot) by scope[/]")
    foreach ($header in 'Assignment', 'Assigned at', 'Definition', 'Enforced', 'Params', 'Resource types') {
        $column = [Spectre.Console.TableColumn]::new("[grey62]$header[/]")
        if ($header -eq 'Params') { $column.Alignment = [Spectre.Console.Justify]::Right }
        if ($header -in 'Enforced', 'Params') { $column.NoWrap = $true }
        $table.AddColumn($column) | Out-Null
    }
    foreach ($a in $assignments) {
        $kind = if ($a.DefinitionType -eq 'PolicySet') { "[mediumpurple2]Initiative[/][grey50] $($glyph.Dot) $($a.Members) policies[/]" } else { '[turquoise2]Policy[/]' }
        $cells = @(
            "[bold]$(& $escape $a.AssignmentDisplayName)[/]$(if ($a.AssignmentDisplayName -ne $a.AssignmentName) { "`n[grey50]$(& $escape $a.AssignmentName)[/]" })"
            "[$($scopeColor[$a.ScopeType])]$(& $escape $a.ScopeType)[/]`n[grey85]$(& $escape $a.ScopeName)[/]$(if ($a.Inherited) { "`n[gold1]inherited[/]" })"
            "$(& $escape (& $shorten $a.DefinitionDisplayName 60))`n$kind$(if ($a.PolicyType) { "[grey50] $($glyph.Dot) $(& $escape $a.PolicyType)[/]" })$(if ($a.Category) { "[grey50] $($glyph.Dot) $(& $escape $a.Category)[/]" })"
            $(if ($a.EnforcementMode -eq 'DoNotEnforce') { '[orange1]Off[/]' } else { '[green3]On[/]' })
            $(if ($a.Parameters) { "[springgreen2]$($a.Assigned)[/][grey50] / $($a.Parameters)[/]" } else { '[grey50]-[/]' })
            (& $types $a.ResourceType)
        )
        [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Rendering.IRenderable[]]@($cells | ForEach-Object { [Spectre.Console.Markup]::new($_) })) | Out-Null
    }
    [Spectre.Console.AnsiConsole]::Write($table)
    [Spectre.Console.AnsiConsole]::WriteLine()

    # --- Resource types ------------------------------------------------------------------------------------
    $bars = @($stats.ResourceTypes | Select-Object -First 12 | ForEach-Object { @{ Label = ($_.Label -replace '^Microsoft\.', ''); Value = $_.Value } })
    if ($bars.Count) {
        Show-AACBarChart -Item $bars -Title 'Resource types with the most assignments'
        [Spectre.Console.AnsiConsole]::WriteLine()
    }

    # --- Each assignment's parameters ------------------------------------------------------------------------
    $byAssignment = @{}
    foreach ($row in $Inventory.Rows) {
        if (-not $row.ParameterName) { continue }
        if (-not $byAssignment.ContainsKey($row.AssignmentId)) { $byAssignment[$row.AssignmentId] = [System.Collections.Generic.List[object]]::new() }
        $byAssignment[$row.AssignmentId].Add($row)
    }
    $tree = [Spectre.Console.Tree]::new([Spectre.Console.Markup]::new("[bold deepskyblue1]$($glyph.Bullet) Parameters[/] [grey58]$($glyph.Dot) [/][springgreen2]assigned[/][grey58], [/][grey62]default[/][grey58], [/][orange1]not set[/]"))
    $tree.Style = [Spectre.Console.Style]::Parse('grey42')
    if (-not $unicode) { $tree.Guide = [Spectre.Console.TreeGuide]::Ascii }
    foreach ($a in $assignments) {
        $rows = @(if ($byAssignment.ContainsKey($a.AssignmentId)) { $byAssignment[$a.AssignmentId] })
        $node = [Spectre.Console.TreeNode]::new([Spectre.Console.Markup]::new("[bold]$(& $escape $a.AssignmentDisplayName)[/] [grey50]$(& $escape $a.ScopeName)$(if (-not $rows.Count) { " $($glyph.Dot) no parameters" })[/]"))
        $tree.Nodes.Add($node)
        $shown = if ($rows.Count -le $ShowDefault) { $rows } else { @($rows | Where-Object ValueSource -NE 'Default') }
        $width = [Math]::Min(40, [Math]::Max(10, (@($shown | ForEach-Object { $_.ParameterName.Length }) + 0 | Measure-Object -Maximum).Maximum))
        foreach ($row in $shown) {
            $color = switch ($row.ValueSource) { 'Assigned' { 'springgreen2' } 'Default' { 'grey62' } default { 'orange1' } }
            $value = if ($row.ValueSource -eq 'Not set') { '(not set)' } elseif ($row.EffectiveValue -eq '') { '(empty)' } elseif ($row.EffectiveValue -match '^/subscriptions/[^,]+/providers/[^,]+/([^/,]+)$') { ".../$($Matches[1])" } else { & $shorten $row.EffectiveValue 140 }
            $default = if ($row.ValueSource -eq 'Assigned' -and $row.DefaultValue -ne '' -and $row.DefaultValue -ne $row.EffectiveValue) { " [grey42](default $(& $escape (& $shorten $row.DefaultValue 40)))[/]" } else { '' }
            $node.Nodes.Add([Spectre.Console.TreeNode]::new([Spectre.Console.Markup]::new("[white]$(& $escape $row.ParameterName.PadRight($width))[/] [$color]$(& $escape $value)[/]$default"))) | Out-Null
        }
        $hidden = $rows.Count - @($shown).Count
        if ($hidden -gt 0) {
            $node.Nodes.Add([Spectre.Console.TreeNode]::new([Spectre.Console.Markup]::new("[grey50]$($glyph.Chevron) $hidden more at their default value[/]"))) | Out-Null
        }
    }
    [Spectre.Console.AnsiConsole]::Write($tree)
    [Spectre.Console.AnsiConsole]::WriteLine()

    foreach ($notice in @($Inventory.Notice)) { Write-AACMarkup "[orange1]$(& $escape $notice)[/]" }
    Write-AACMarkup "[grey42]Add -PassThru (or pipe the command) for one row per assignment and parameter; -CsvPath and -HtmlPath write every one.[/]"
}