Private/Show-AACStoragePlanView.ps1

function Show-AACStoragePlanView {
    <#
    .SYNOPSIS
        Renders Deploy-AACStorageAccount's plan and gates as a
        Spectre.Console view.
    .DESCRIPTION
        The account and scope; Terraform's kind of summary line; every change
        with its properties (+ create, ~ update, - delete, ! can't change in
        place, ? couldn't be read), and drift; the policy assignments that
        apply to storage; the gates by outcome (Blocks, Breaks, Changes,
        Audit, Warn, Pass), each block with its fix; how to fix the failing
        PSRule rules - a configuration snippet with every setting that fixes
        one, and what to do for the others - or the fixes -UseSuggestedFix
        applied; and the decision: blocked, nothing to do, or ready.
    #>

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

        [int] $MaxProperty = 30
    )

    $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) }
    $glyph = Get-AACGlyph
    $changes = @($Deployment.Changes)
    $short = { param([string] $Text) $flat = $Text -replace '\s*\r?\n\s*', ' '; if ($flat.Length -gt 110) { $flat.Substring(0, 110) + '...' } else { $flat } }
    $newTable = {
        param([string] $Title, [string] $Color, [string[]] $Columns)
        $table = [Spectre.Console.Table]::new()
        $table.Border = [Spectre.Console.TableBorder]::Rounded
        $table.BorderStyle = [Spectre.Console.Style]::Parse($Color)
        $table.Expand = $true
        $table.Title = [Spectre.Console.TableTitle]::new($Title)
        foreach ($column in $Columns) { $table.AddColumn([Spectre.Console.TableColumn]::new("[grey62]$column[/]")) | Out-Null }
        $table
    }
    $addRow = { param($Table, [string[]] $Cells) [Spectre.Console.TableExtensions]::AddRow($Table, [Spectre.Console.Rendering.IRenderable[]]@($Cells | ForEach-Object { [Spectre.Console.Markup]::new($_) })) | Out-Null }
    $write = { param($Table) [Spectre.Console.AnsiConsole]::Write($Table); [Spectre.Console.AnsiConsole]::WriteLine() }
    $count = { param([string] $Action) @($changes | Where-Object Action -EQ $Action).Count }
    $style = @{ Create = @('+', 'green3'); Update = @('~', 'orange1'); Delete = @('-', 'red1'); Replace = @('!', 'red1'); Unknown = @('?', 'grey58'); Drift = @('*', 'yellow') }

    Write-AACMarkup "[grey58][white]$(& $escape $Deployment.Name)[/] $($glyph.Dot) $(& $escape $Deployment.ResourceGroupName) $($glyph.Dot) subscription $(& $escape $Deployment.SubscriptionId) $($glyph.Dot) $(& $escape $Deployment.Location)[/]"
    [Spectre.Console.AnsiConsole]::WriteLine()
    $line = "[bold]Plan:[/] [green3]$(& $count 'Create') to create[/], [orange1]$(& $count 'Update') to update[/], [red1]$(& $count 'Delete') to delete[/], [grey58]$(& $count 'NoChange') unchanged[/]"
    if (& $count 'Replace') { $line += ", [red1 bold]$(& $count 'Replace') can't change in place[/]" }
    if (& $count 'Drift') { $line += ", [yellow]$(& $count 'Drift') not in the configuration[/]" }
    Write-AACMarkup "$line."
    [Spectre.Console.AnsiConsole]::WriteLine()

    # --- Changes ------------------------------------------------------------------------------------------------------
    $shown = @($changes | Where-Object Action -NE 'NoChange')
    if ($shown.Count) {
        $table = & $newTable "[bold]$($glyph.Bullet) Changes[/] [grey58]$($glyph.Dot) in apply order[/]" 'grey42' @('', 'Resource', 'What changes')
        foreach ($item in $shown) {
            $symbol = $style[$item.Action]
            $lines = @(foreach ($difference in @($item.Differences) | Select-AACFirst $MaxProperty) {
                    $name = & $escape $difference.Property
                    if ($item.Action -eq 'Create') { "[green3]+[/] $name = [white]$(& $escape (& $short $difference.Desired))[/]" }
                    elseif ($item.Action -eq 'Delete') { "[red1]-[/] $name" }
                    else { "$(if ($difference.Immutable) { '[red1]![/]' } else { '[orange1]~[/]' }) $name`: [white]$(& $escape (& $short $difference.Current))[/] [grey58]$($glyph.Arrow)[/] [white]$(& $escape (& $short $difference.Desired))[/]$(if ($difference.Immutable) { ' [red1 bold]can''t change in place[/]' })" }
                })
            if (@($item.Differences).Count -gt $MaxProperty) { $lines += "[grey50]... and $(@($item.Differences).Count - $MaxProperty) more[/]" }
            if ($item.Reason) { $lines += "[grey58]$(& $escape $item.Reason)[/]" }
            & $addRow $table @("[bold $($symbol[1])]$(& $escape $symbol[0])[/]", "[bold]$(& $escape $item.Resource)[/]`n[grey50]$(& $escape $item.Action)[/]", ($lines -join "`n"))
        }
        & $write $table
    }

    # --- Policy assignments ------------------------------------------------------------------------------------------
    $policies = @($Deployment.Policies)
    if ($policies.Count) {
        $table = & $newTable "[bold mediumpurple2]$($glyph.Bullet) Azure Policy assigned here for storage[/] [grey58]$($glyph.Dot) $($policies.Count)[/]" 'mediumpurple2' @('Assignment', 'Effect', 'Assigned at', 'Applies to')
        foreach ($policy in $policies) {
            $effect = [string]$policy.Effect
            $color = if ($effect -match 'Deny') { 'red1' } elseif ($effect -match 'Modify|Append|DeployIfNotExists') { 'deepskyblue1' } elseif ($effect -match 'Audit') { 'orange1' } else { 'grey70' }
            & $addRow $table @("[bold]$(& $escape $policy.Assignment)[/]$(if ($policy.Enforcement -eq 'DoNotEnforce') { "`n[grey50](not enforced)[/]" })", "[$color]$(& $escape $effect)[/]", "[grey70]$(& $escape $policy.Scope)[/]", "[grey70]$(& $escape (& $short $policy.ResourceType))[/]")
        }
        & $write $table
    }

    # --- Gates ----------------------------------------------------------------------------------------------------------
    $outcomes = [ordered]@{ Blocks = 'red1'; Breaks = 'orange1'; Changes = 'deepskyblue1'; Audit = 'yellow'; Warn = 'orange1'; Info = 'grey70'; Pass = 'green3' }
    $gates = @($Deployment.Gates | Sort-Object { @($outcomes.Keys).IndexOf($_.Outcome) }, Gate, Resource)
    if ($gates.Count) {
        $table = & $newTable "[bold]$($glyph.Bullet) Gates[/] [grey58]$($glyph.Dot) name, immutable properties, Azure Policy, PSRule[/]" 'grey42' @('Outcome', 'Gate', 'Resource', 'Detail')
        foreach ($item in $gates) {
            $what = "$(if ($item.Item) { "[bold]$(& $escape $item.Item)[/]`n" })[grey70]$(& $escape $item.Detail)[/]"
            if ($item.PSObject.Properties['Fix'] -and $item.Fix) { $what += "`n[springgreen3]Fix:[/] [white]$(& $escape $item.Fix)[/]" }
            & $addRow $table @("[bold $($outcomes[$item.Outcome])]$(& $escape $item.Outcome)[/]$(if ($item.Severity) { "`n[grey50]$(& $escape $item.Severity)[/]" })", (& $escape $item.Gate), (& $escape $item.Resource), $what)
        }
        & $write $table
    }

    # --- How to fix the failing PSRule rules ------------------------------------------------------------------------------
    # The settings as a .psd1 snippet, in the configuration's own names.
    $psd1 = {
        param($Value, [int] $Depth = 0)
        $pad = ' ' * ($Depth + 1)
        if ($Value -is [System.Collections.IDictionary]) { return "@{`n$(@(foreach ($key in $Value.Keys) { "$pad$key = $(& $psd1 $Value[$key] ($Depth + 1))" }) -join "`n")`n$(' ' * $Depth)}" }
        if ($Value -is [System.Collections.IList] -and $Value -isnot [string]) { return "@($(@(foreach ($item in $Value) { & $psd1 $item ($Depth + 1) }) -join ', '))" }
        if ($Value -is [bool]) { return $(if ($Value) { '$true' } else { '$false' }) }
        if ($Value -is [int] -or $Value -is [long] -or $Value -is [double]) { return [string]$Value }
        "'$(([string]$Value) -replace "'", "''")'"
    }
    $applied = @($Deployment.FixesApplied)
    $automatic = @($Deployment.Fixes | Where-Object Kind -EQ 'Auto')
    $manual = @($Deployment.Fixes | Where-Object Kind -EQ 'Manual')
    if ($applied.Count) {
        $snippet = & $psd1 (Set-AACStorageConfigurationFix -Configuration ([ordered]@{}) -Fix $applied)
        Show-AACPanel -Header 'Suggested fixes applied to this run' -Content "[grey70]The plan above uses these settings (-UseSuggestedFix) and was checked again. Put them in your configuration so the next run has them:[/]`n[white]$(& $escape $snippet)[/]" -BorderColor 'springgreen3' -AllowMarkup
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    elseif ($automatic.Count -or $manual.Count) {
        $lines = [System.Collections.Generic.List[string]]::new()
        if ($automatic.Count) {
            $lines.Add("[bold]Add to the configuration[/] [grey58](or run again with -UseSuggestedFix):[/]")
            $lines.Add("[white]$(& $escape (& $psd1 (Set-AACStorageConfigurationFix -Configuration ([ordered]@{}) -Fix $automatic)))[/]")
        }
        foreach ($fix in $manual) { $lines.Add("[orange1]$($glyph.Bullet)[/] [bold]$(& $escape $fix.Rule)[/]: [grey70]$(& $escape $fix.Advice)[/]") }
        $lines.Add('[grey50]A rule that does not apply to this account can be excluded on purpose: -ExcludeRule <rule>.[/]')
        Show-AACPanel -Header 'How to fix the failing PSRule rules' -Content ($lines -join "`n") -BorderColor 'orange1' -AllowMarkup
        [Spectre.Console.AnsiConsole]::WriteLine()
    }

    # --- The decision ---------------------------------------------------------------------------------------------------
    $blocks = @($gates | Where-Object Outcome -In 'Blocks', 'Breaks').Count
    $breaks = @($gates | Where-Object Outcome -EQ 'Breaks').Count
    $byPolicy = @($gates | Where-Object Outcome -EQ 'Changes').Count
    $summary = "Blocks $(@($gates | Where-Object Outcome -EQ 'Blocks').Count) $($glyph.Dot) breaks standards $breaks $($glyph.Dot) changed by policy $byPolicy $($glyph.Dot) $($Deployment.Writes) change(s) to apply"
    if ($blocks) { Show-AACPanel -Content "[bold red1]Blocked[/] [grey70]- nothing will be written$(if ($breaks) { ': fix the failing PSRule rules (above), or exclude one that does not apply' }).[/]`n[grey58]$(& $escape $summary)[/]" -BorderColor 'red1' -AllowMarkup }
    elseif (-not $Deployment.Writes) { Show-AACPanel -Content "[bold green3]No changes.[/] [grey70]The account matches the configuration.[/]`n[grey58]$(& $escape $summary)[/]" -BorderColor 'green3' -AllowMarkup }
    else { Show-AACPanel -Content "[bold]Ready to apply.[/]`n[grey58]$(& $escape $summary)[/]" -BorderColor 'springgreen3' -AllowMarkup }
    [Spectre.Console.AnsiConsole]::WriteLine()
}