Public/Reports/Invoke-NMMReport.ps1
|
function Invoke-NMMReport { <# .SYNOPSIS Generate pre-built HTML reports for NMM accounts. .DESCRIPTION Invoke-NMMReport provides ready-to-use report templates that automatically fetch data from multiple NMM API endpoints and generate comprehensive HTML reports. When called without -ReportType, displays an interactive menu for report selection. When called with -ReportType, generates the report directly (suitable for automation). .PARAMETER ReportType The type of report to generate. Available types: - AccountOverview: Host pools, session hosts, images, and users - DeviceInventory: Intune devices with compliance, hardware, and apps - SecurityCompliance: Device compliance, backup status, and users - Infrastructure: Complete AVD infrastructure configuration If not specified, displays an interactive selection menu. .PARAMETER AccountId The NMM account ID to generate the report for. .PARAMETER OutputPath Path for the output HTML file. Defaults to ./NMM-{ReportType}_{timestamp}.html .PARAMETER OpenInBrowser Automatically open the generated report in the default browser. .PARAMETER Theme Report theme: 'light' (default) or 'dark'. .EXAMPLE Invoke-NMMReport -AccountId 67 Shows interactive menu to select a report type, then generates the selected report. .EXAMPLE Invoke-NMMReport -ReportType AccountOverview -AccountId 67 -OpenInBrowser Generates an Account Overview report and opens it in the browser. .EXAMPLE Invoke-NMMReport -ReportType SecurityCompliance -AccountId 67 -Theme dark -OutputPath "./reports/security.html" Generates a Security Compliance report with dark theme to a custom path. .NOTES Pre-built reports are defined in Private/Data/PrebuiltReports.json #> [CmdletBinding()] param( [Parameter()] [ValidateSet('AccountOverview', 'DeviceInventory', 'SecurityCompliance', 'Infrastructure')] [string]$ReportType, [Parameter(Mandatory = $true)] [int]$AccountId, [Parameter()] [string]$OutputPath, [Parameter()] [switch]$OpenInBrowser, [Parameter()] [ValidateSet('light', 'dark')] [string]$Theme = 'light' ) begin { # Load report definitions $configPath = Join-Path $PSScriptRoot "../../Private/Data/PrebuiltReports.json" if (-not (Test-Path $configPath)) { throw "PrebuiltReports.json not found at: $configPath" } $reportDefinitions = Get-Content $configPath -Raw | ConvertFrom-Json $reportTypes = @($reportDefinitions.PSObject.Properties.Name) } process { # Interactive menu if no ReportType specified if (-not $ReportType) { Write-Host "`n" -NoNewline Write-Host " NMM Pre-built Reports" -ForegroundColor Cyan Write-Host " " + ("=" * 40) -ForegroundColor DarkGray Write-Host "" for ($i = 0; $i -lt $reportTypes.Count; $i++) { $type = $reportTypes[$i] $def = $reportDefinitions.$type Write-Host " [$($i + 1)] " -ForegroundColor Yellow -NoNewline Write-Host "$type" -ForegroundColor White Write-Host " $($def.subtitle)" -ForegroundColor DarkGray } Write-Host "" Write-Host " [0] Cancel" -ForegroundColor DarkGray Write-Host "" $selection = Read-Host " Select report type (1-$($reportTypes.Count))" if ($selection -eq '0' -or [string]::IsNullOrWhiteSpace($selection)) { Write-Host " Cancelled." -ForegroundColor Yellow return } $index = [int]$selection - 1 if ($index -lt 0 -or $index -ge $reportTypes.Count) { Write-Error "Invalid selection. Please enter a number between 1 and $($reportTypes.Count)." return } $ReportType = $reportTypes[$index] Write-Host "" Write-Host " Selected: $ReportType" -ForegroundColor Green Write-Host "" } # Get report definition $reportDef = $reportDefinitions.$ReportType if (-not $reportDef) { throw "Report type '$ReportType' not found in configuration." } # Set default output path if (-not $OutputPath) { $timestamp = Get-Date -Format 'yyyy-MM-dd_HHmm' $OutputPath = "./NMM-${ReportType}_${timestamp}.html" } # Initialize report builder Write-Host "Generating $($reportDef.title)..." -ForegroundColor Cyan Write-Host "" $reportParams = @{ Title = $reportDef.title Subtitle = $reportDef.subtitle Theme = $Theme } $report = New-NMMReport @reportParams # Process each section $sectionCount = $reportDef.sections.Count $currentSection = 0 foreach ($section in $reportDef.sections) { $currentSection++ $percentComplete = [math]::Round(($currentSection / $sectionCount) * 100) Write-Progress -Activity "Generating Report" -Status "Fetching: $($section.title)" -PercentComplete $percentComplete Write-Host " [$currentSection/$sectionCount] Fetching $($section.title)..." -ForegroundColor Gray try { # Fetch data using the specified function $functionName = $section.function $data = $null # Handle functions that need special context switch ($section.functionRequiresContext) { 'Device' { # Get all devices first, then fetch detail for each $devices = Get-NMMDevice -AccountId $AccountId if ($devices) { $allData = [System.Collections.Generic.List[object]]::new() foreach ($device in $devices) { try { $deviceData = & $functionName -AccountId $AccountId -DeviceId $device.id -ErrorAction SilentlyContinue if ($deviceData) { # Add device name for context (suppress output) foreach ($item in @($deviceData)) { $item | Add-Member -NotePropertyName 'DeviceName' -NotePropertyValue $device.deviceName -Force $allData.Add($item) } } } catch { # Some devices may not have all data, continue silently } } $data = $allData.ToArray() } } 'HostPool' { # Get hosts from all host pools $pools = Get-NMMHostPool -AccountId $AccountId if ($pools) { $allData = @() foreach ($poolWrapper in $pools) { # Unwrap pool data (Get-NMMHostPool wraps in {HostPool: {...}, Details: {...}}) $pool = if ($poolWrapper.HostPool) { $poolWrapper.HostPool } else { $poolWrapper } try { $hostParams = @{ AccountId = $AccountId SubscriptionId = $pool.subscription ResourceGroup = $pool.resourceGroup PoolName = $pool.hostPoolName } $hostData = & $functionName @hostParams if ($hostData) { $allData += $hostData } } catch { # Some pools may have no hosts } } $data = $allData } } default { # Standard function call - check for custom parameter name $paramName = if ($section.parameterName) { $section.parameterName } else { 'AccountId' } $params = @{ $paramName = $AccountId } $data = & $functionName @params } } # Assign PSTypeName based on function for template matching $typeMapping = @{ 'Get-NMMProtectedItem' = 'NMM.Backup' 'Get-NMMBackup' = 'NMM.Backup' 'Get-NMMDevice' = 'NMM.Device' 'Get-NMMUsers' = 'NMM.User' 'Get-NMMUser' = 'NMM.User' 'Get-NMMHostPool' = 'NMM.HostPool' 'Get-NMMHost' = 'NMM.Host' 'Get-NMMAccount' = 'NMM.Account' 'Get-NMMDesktopImage' = 'NMM.DesktopImage' 'Get-NMMImageTemplate' = 'NMM.DesktopImage' } if ($data -and $typeMapping.ContainsKey($functionName)) { $typeName = $typeMapping[$functionName] foreach ($item in @($data)) { if ($item.PSObject.TypeNames[0] -ne $typeName) { $item.PSObject.TypeNames.Insert(0, $typeName) } } } # Flatten wrapped data (e.g., Get-NMMHostPool returns {HostPool: {...}, Details: {...}}) if ($data -and @($data).Count -gt 0) { $firstItem = @($data)[0] # Check if data is wrapped (has HostPool property with nested object) if ($firstItem.PSObject.Properties.Name -contains 'HostPool' -and $firstItem.HostPool -is [PSCustomObject]) { $data = $data | ForEach-Object { $_.HostPool } } # Flatten nested array properties to summaries (for table display) $data = $data | ForEach-Object { $item = $_ foreach ($prop in $item.PSObject.Properties) { if ($prop.Value -is [System.Array] -and $prop.Value.Count -gt 0) { # Check if array of objects if ($prop.Value[0] -is [PSCustomObject] -or $prop.Value[0] -is [hashtable]) { # Summarize: count items, or extract key field if available if ($prop.Value[0].PSObject.Properties.Name -contains 'state') { # Compliance states - group by state $grouped = $prop.Value | Group-Object state $summary = ($grouped | ForEach-Object { "$($_.Count) $($_.Name)" }) -join ', ' $item | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $summary -Force } elseif ($prop.Value[0].PSObject.Properties.Name -contains 'displayName') { # Apps - show count $item | Add-Member -NotePropertyName $prop.Name -NotePropertyValue "$($prop.Value.Count) items" -Force } else { # Generic: show count $item | Add-Member -NotePropertyName $prop.Name -NotePropertyValue "$($prop.Value.Count) items" -Force } } else { # Array of primitives - join as string $item | Add-Member -NotePropertyName $prop.Name -NotePropertyValue ($prop.Value -join ', ') -Force } } } $item } } # Add section to report $sectionParams = @{ Title = $section.title Description = $section.description Data = $data PassThru = $true } if ($section.showChart -and $data -and @($data).Count -gt 0) { $sectionParams['ShowChart'] = $true $sectionParams['ChartType'] = $section.chartType if ($section.chartConfig) { $sectionParams['ChartConfig'] = @{ groupField = $section.chartConfig.groupField } } } $report = $report | Add-NMMReportSection @sectionParams $itemCount = if ($data) { @($data).Count } else { 0 } Write-Host " Retrieved $itemCount items" -ForegroundColor DarkGray } catch { Write-Warning "Failed to fetch $($section.title): $_" # Add empty section with error note $report = $report | Add-NMMReportSection -Title $section.title -Description "Error: $_" -Data @() -PassThru } } Write-Progress -Activity "Generating Report" -Completed # Export report Write-Host "" Write-Host "Exporting report..." -ForegroundColor Cyan $exportParams = @{ OutputPath = $OutputPath } if ($OpenInBrowser) { $exportParams['OpenInBrowser'] = $true } $result = $report | Export-NMMReport @exportParams Write-Host "" Write-Host "Report generated successfully!" -ForegroundColor Green Write-Host " Path: $($result.Path)" -ForegroundColor White Write-Host " Sections: $($result.SectionCount)" -ForegroundColor DarkGray Write-Host " Generated: $($result.GeneratedAt)" -ForegroundColor DarkGray Write-Host "" return $result } } |