Private/Write-AACSkuAvailabilityHtml.ps1

function Write-AACSkuAvailabilityHtml {
    <#
    .SYNOPSIS
        Writes Get-AACSkuAvailability's result as an interactive HTML report:
        tiles and charts that filter tables of the VM sizes, the vCPU quota,
        the zones and the AKS cluster's node pools.
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param(
        [Parameter(Mandatory)]
        [hashtable] $Availability,

        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter(Mandatory)]
        [string] $Title,

        [System.Collections.IDictionary] $Detail
    )

    $stats = $Availability.Stats
    $statusTones = @{ Available = 'good'; Partial = 'warn'; NoQuota = 'warn'; ZoneUnavailable = 'bad'; NotSupported = 'bad'; Restricted = 'bad'; NotChecked = 'neutral' }
    $skus = @($Availability.Skus)
    $usable = @($skus | Where-Object { $_.Status -eq 'Available' -or $_.Status -eq 'Partial' })

    $tiles = @(
        @{ Value = '{0:N0}' -f $stats.Sizes; Label = 'sizes checked'; Tone = 'info'; Table = 'sizes' }
        @{ Value = '{0:N0}' -f $stats.Available; Label = 'available'; Tone = $(if ($stats.Available) { 'good' } else { 'neutral' }); Table = 'sizes'; Filters = @{ Status = 'Available' } }
        @{ Value = '{0:N0}' -f $stats.Partial; Label = 'in some zones only'; Tone = $(if ($stats.Partial) { 'warn' } else { 'neutral' }); Table = 'sizes'; Filters = @{ Status = 'Partial' } }
        @{ Value = '{0:N0}' -f $stats.NoQuota; Label = 'short of quota'; Tone = $(if ($stats.NoQuota) { 'warn' } else { 'neutral' }); Table = 'sizes'; Filters = @{ Status = 'NoQuota' } }
        @{ Value = '{0:N0}' -f $stats.ZoneUnavailable; Label = 'not in the zones'; Tone = $(if ($stats.ZoneUnavailable) { 'bad' } else { 'neutral' }); Table = 'sizes'; Filters = @{ Status = 'ZoneUnavailable' } }
        @{ Value = '{0:N0}' -f $stats.NotSupported; Label = 'not for AKS'; Tone = $(if ($stats.NotSupported) { 'bad' } else { 'neutral' }); Table = 'sizes'; Filters = @{ Status = 'NotSupported' } }
        @{ Value = '{0:N0}' -f $stats.Restricted; Label = 'restricted for the subscription'; Tone = $(if ($stats.Restricted) { 'bad' } else { 'neutral' }); Table = 'sizes'; Filters = @{ Status = 'Restricted' } }
        @{ Value = '{0:N0}' -f $stats.QuotaNearLimit; Label = 'quotas 80%+ used'; Tone = $(if ($stats.QuotaNearLimit) { 'warn' } else { 'good' }); Table = 'quota' }
    )
    $charts = @(
        @{ Title = 'VM sizes by status'; Kind = 'donut'; CenterLabel = 'sizes'; Table = 'sizes'; Column = 'Status'; Items = @($stats.ByStatus | ForEach-Object { @{ Label = $_.Label; Value = $_.Value; Filter = $_.Label; Tone = $statusTones[$_.Label] } }) }
        @{ Title = 'Usable sizes by series'; Items = @($usable | Where-Object Series | Group-Object Series | Sort-Object -Property @{ Expression = 'Count'; Descending = $true }, Name | Select-Object -First 15 | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name } }); Table = 'sizes'; Column = 'Series'; Tone = 'good' }
        @{ Title = 'vCPU quota used (%)'; Items = @($Availability.Quotas | Where-Object { $null -ne $_.UsedPercent -and $_.Used -gt 0 } | Sort-Object UsedPercent -Descending | Select-Object -First 12 | ForEach-Object { @{ Label = $_.Quota; Value = $_.UsedPercent; Display = "$($_.UsedPercent)% ($($_.Used) of $($_.Limit))"; Tone = $(if ($_.UsedPercent -ge 90) { 'bad' } elseif ($_.UsedPercent -ge 70) { 'warn' } else { 'good' }); Filter = $_.Quota } }); Table = 'quota'; Column = 'Quota'; Wide = $true }
    )
    $tables = @(
        @{
            Id = 'sizes'; Title = 'VM sizes'; Note = 'Usable first. Zones: where the size can run for this subscription (offered, less zone restrictions). Family free: vCPUs left in the family''s quota.'
            Noun = 'sizes'; File = 'vm-sizes'; Rows = $skus; GroupBy = @('Status', 'Series', 'Location', 'SubscriptionName')
            Columns = @(
                @{ Key = 'Sku'; Label = 'Size'; Type = 'mono' }
                @{ Key = 'Status'; Label = 'Status'; Type = 'badge'; Tones = $statusTones; Facet = $true }
                @{ Key = 'Reason'; Label = 'Why'; Type = 'wide' }
                @{ Key = 'Series'; Label = 'Series'; Facet = $true }
                @{ Key = 'vCPUs'; Label = 'vCPUs'; Type = 'number'; Format = 'N0'; Facet = $true }
                @{ Key = 'MemoryGB'; Label = 'Memory GB'; Type = 'number'; Format = 'N2' }
                @{ Key = 'Zones'; Label = 'Zones'; Facet = $true }
                @{ Key = 'ZonesMissing'; Label = 'Zones missing'; Facet = $true }
                @{ Key = 'FamilyVCpuFree'; Label = 'Family vCPUs free'; Type = 'number'; Format = 'N0' }
                @{ Key = 'Architecture'; Label = 'Architecture'; Facet = $true }
                @{ Key = 'GPUs'; Label = 'GPUs'; Type = 'number'; Format = 'N0' }
                @{ Key = 'EphemeralOSDisk'; Label = 'Ephemeral OS'; Facet = $true }
                @{ Key = 'AcceleratedNetworking'; Label = 'Accelerated networking'; Facet = $true }
                @{ Key = 'PremiumStorage'; Label = 'Premium storage'; Facet = $true }
                @{ Key = 'Note'; Label = 'Note'; Type = 'wide' }
                @{ Key = 'InUse'; Label = 'Node pools'; Facet = $true }
                @{ Key = 'Location'; Label = 'Region'; Facet = $true }
                @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
                @{ Key = 'Family'; Label = 'Family'; Facet = $true; Hidden = $true }
                @{ Key = 'Version'; Label = 'Version'; Facet = $true; Hidden = $true }
                @{ Key = 'Spot'; Label = 'Spot'; Facet = $true; Hidden = $true }
                @{ Key = 'MaxDataDisks'; Label = 'Data disks'; Type = 'number'; Format = 'N0'; Hidden = $true }
                @{ Key = 'HyperVGenerations'; Label = 'Hyper-V generations'; Facet = $true; Hidden = $true }
                @{ Key = 'RestrictedZones'; Label = 'Restricted zones'; Hidden = $true }
                @{ Key = 'RegionalVCpuFree'; Label = 'Regional vCPUs free'; Type = 'number'; Format = 'N0'; Hidden = $true }
            )
        }
        @{
            Id = 'quota'; Title = 'vCPU quota'; Note = 'The region''s total and each family of the sizes checked.'; Noun = 'quotas'; File = 'vcpu-quota'; Rows = @($Availability.Quotas)
            Columns = @(
                @{ Key = 'Quota'; Label = 'Quota' }
                @{ Key = 'UsedPercent'; Label = 'Used %'; Type = 'number'; Format = 'N0' }
                @{ Key = 'Used'; Label = 'vCPUs used'; Type = 'number'; Format = 'N0' }
                @{ Key = 'Limit'; Label = 'Limit'; Type = 'number'; Format = 'N0' }
                @{ Key = 'Free'; Label = 'Free'; Type = 'number'; Format = 'N0' }
                @{ Key = 'Location'; Label = 'Region'; Facet = $true }
                @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
            )
        }
        if (@($Availability.Zones).Count) {
            @{
                Id = 'zones'; Title = 'Availability zones'; Note = 'Each subscription''s logical zones map to physical zones of its own.'; Noun = 'zones'; File = 'zones'; Rows = @($Availability.Zones)
                Columns = @(
                    @{ Key = 'LogicalZone'; Label = 'Logical zone' }
                    @{ Key = 'PhysicalZone'; Label = 'Physical zone'; Type = 'mono' }
                    @{ Key = 'Location'; Label = 'Region'; Facet = $true }
                    @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
                )
            }
        }
        if (@($Availability.Pools).Count) {
            @{
                Id = 'pools'; Title = 'AKS node pools'; Note = 'The cluster''s pools, with their size''s status in its region now.'; Noun = 'node pools'; File = 'aks-node-pools'; Rows = @($Availability.Pools)
                Columns = @(
                    @{ Key = 'Pool'; Label = 'Pool' }
                    @{ Key = 'Mode'; Label = 'Mode'; Facet = $true }
                    @{ Key = 'VmSize'; Label = 'Size'; Type = 'mono' }
                    @{ Key = 'Nodes'; Label = 'Nodes'; Type = 'number'; Format = 'N0'; Sum = $true }
                    @{ Key = 'Zones'; Label = 'Zones' }
                    @{ Key = 'SizeStatus'; Label = 'Size status'; Type = 'badge'; Tones = $statusTones; Facet = $true }
                    @{ Key = 'FamilyVCpuFree'; Label = 'Family vCPUs free'; Type = 'number'; Format = 'N0' }
                    @{ Key = 'Reason'; Label = 'Why'; Type = 'wide' }
                )
            }
        }
    )
    $notices = @(@($Availability.Notice | Where-Object { $_ }) | ForEach-Object { @{ Tone = 'info'; Text = $_ } })
    $notices += @{ Tone = 'info'; Text = 'Read-only, from Microsoft.Compute SKUs (offered zones and this subscription''s restrictions), vCPU usage and the subscription''s zone mapping. Capacity at deployment time can''t be checked without deploying.' }

    Write-AACHtmlReport -Path $Path -Title $Title -Subtitle 'Which VM sizes can be used, in which zones, within quota - and why not' -Fact $Detail -Tile $tiles -Chart $charts -Table $tables -Notice $notices
}