HtmlReport.psm1
$PSModuleRoot = $PSScriptRoot enum Emphasis { default primary success info warning danger } enum ChartType { LineChart PieChart ColumnChart BarChart AreaChart ScatterChart GeoChart Timeline } class DataWrapper { DataWrapper([string]$Title, [PSObject]$Data) { $this.Title = $Title $this.Data = $Data } DataWrapper([string]$Title, [PSObject]$Data, [string]$Description) { $this.Title = $Title $this.Description = $Description $this.Data = $Data } DataWrapper([string]$Title, [PSObject]$Data, [string]$Description, [Emphasis]$Emphasis) { $this.Title = $Title $this.Description = $Description $this.Emphasis = $Emphasis $this.Data = $Data } [string]$Title = "" [string]$Description = "" [Emphasis]$Emphasis = "default" [PSObject]$Data = $null } class Table : DataWrapper { Table([string]$Title, [PSObject]$Data) : base($Title, $Data) {} Table([string]$Title, [PSObject]$Data, [string]$Description) : base($Title, $Data, $Description) {} Table([string]$Title, [PSObject]$Data, [string]$Description, [Emphasis]$Emphasis) : base($Title, $Data, $Description, $Emphasis) {} } class Chart : DataWrapper { Chart([string]$Title, [PSObject]$Data) : base($Title, $Data) {} Chart([string]$Title, [PSObject]$Data, [string]$Description) : base($Title, $Data, $Description) {} Chart([string]$Title, [PSObject]$Data, [string]$Description, [Emphasis]$Emphasis) : base($Title, $Data, $Description, $Emphasis) {} [ChartType]$ChartType = "Line" }function New-Chart { #.Synopsis # Creates a new ChartData for New-Report #.Description # Collects ChartData for New-Report. # ChartData should be in specific shapes in order to work properly, but it depends somewhat on the chart type you're trying to create (LineChart, PieChart, ColumnChart, BarChart, AreaChart, ScatterChart, GeoChart, Timeline). There should be examples for each in the help below... #.Notes # TODO: Write examples... [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param( # Chart Type [ChartType]$ChartType, # A title that goes on the top of the table [Parameter(Mandatory)] [string]$Title, # Description to go above the table [Parameter()] [string]$Description, # Data for the table (can be piped in) [Parameter(Mandatory,ValueFromPipeline)] [PSObject]$InputObject, # Emphasis value: default (unadorned), primary (highlighted), success (green), info (blue), warning (yellow), danger (red) [Parameter()] [Emphasis]$Emphasis = "default" ) begin { $ChartData = @() } process { $ChartData += $InputObject } end { $Chart = [Chart]::new($Title, [PSObject]$ChartData, $Description, $Emphasis) $Chart.ChartType = $ChartType $Chart } } $TemplatePath = Join-Path $PSModuleRoot Templates $ChartTemplate = @' <div class="card panel ${Emphasis}"> <div class="panel-header"><h4 class="panel-title">${Title}</h4></div> <div class="panel-body" id="chart${id}"></div> <div class="panel-body">${Description}</div> </div> <script> $(function() { new Chartkick.${ChartType}("chart${id}", ${data}); }) </script> '@ $TableTemplate = @' <div class="row"> <div class="col-xs-16"> <div class="panel ${emphasis}"> <div class="panel-heading"><h4 class="panel-title">${Title}</h4></div> <div class="panel-body">${Description}</div> <table class="table table-striped"> ${data} </table> </div> </div> </div> '@ function New-Report { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param( # The template to use for the report (must exist in templates folder) [Parameter()] [string] $Template = "template.html", # The title of the report [Parameter(ValueFromPipelineByPropertyName)] [string] $Title, # A sentence or two describing the report [Parameter(ValueFromPipelineByPropertyName)] [string] $Description, # The author of the report [Parameter(ValueFromPipelineByPropertyName)] [string] $Author=${Env:UserName}, [Parameter(ValueFromPipeline)] $InputObject ) begin { Write-Debug "Beginning $($PSBoundParameters | Out-String)" if($Template -notmatch "\.html$") { $Template += ".html" } if(!(Test-Path $Template)) { $Template = Join-Path $TemplatePath $Template if(!(Test-Path $Template)) { Write-Error "Template file not found in Templates: $Template" } } $TemplateContent = Get-Content $Template -Raw $FinalTable = @() $FinalChart = @() $Index = 0 $Finished = $false if($InputObject -is [ScriptBlock]) { $null = $PSBoundParameters.Remove("InputObject") & $InputObject | New-Report @PSBoundParameters $Finished = $true return } } process { if($Finished) { return } Write-Debug "Processing $($_ | Out-String)" if($Title) { $FinalTitle = [System.Security.SecurityElement]::Escape($Title) } if($Description) { $FinalDescription = [System.Security.SecurityElement]::Escape($Description) } if($Author) { $FinalAuthor = [System.Security.SecurityElement]::Escape($Author) } if($InputObject -is [ScriptBlock]) { $Data = & $InputObject } elseif($InputObject -is [DataWrapper]) { if($InputObject.Data -is [ScriptBlock]) { $Data = & $InputObject.Data } else { $Data = $InputObject.Data } } else { $Data = $InputObject } if($InputObject -is [Table]) { $Data = $Data | Microsoft.PowerShell.Utility\ConvertTo-Html -As Table -Fragment # Make sure each row is on a line, and the headers are called out properly Write-Verbose "Table with $($Data.Count) rows of data." $Data = "<thead>`n{0}`n</thead>`n<tbody>`n{1}`n</tbody>" -f $Data[2], ($Data[3..($Data.Count - 2)] -join "`n") $Table = $TableTemplate -replace '\${Title}', $InputObject.Title ` -replace '\${Description}', $InputObject.Description ` -replace '\${Emphasis}', $("panel-" + $InputObject.Emphasis) ` -replace '\${Data}', $Data $FinalTable += $Table } if($InputObject -is [Chart]) { Write-Verbose "$ChartType Chart with $($Data.Count) data.points" if($Data -isnot [string]) { # Microsoft's ConvertTo-Json doesn't handle PSObject unwrapping properly # https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/15123162-convertto-json-doesn-t-serialize-simple-objects-pr # To bypass this bug, we must round-trip through the CliXml serializer $TP = [IO.Path]::GetTempFileName() Export-CliXml -InputObject $Data -LiteralPath $TP $Data =Import-CliXml -LiteralPath $TP | ConvertTo-json Remove-Item $TP # $Data = Microsoft.PowerShell.Utility\ConvertTo-Json -InputObject $Data } $Chart = $ChartTemplate -replace '\${Title}', $InputObject.Title ` -replace '\${Description}', $InputObject.Description ` -replace '\${Emphasis}', $("panel-" + $InputObject.Emphasis) ` -replace '\${ChartType}', $InputObject.ChartType ` -replace '\${Data}', $Data ` -replace '\${id}', ($Index++) $FinalChart += $Chart } } end { if($Finished) { return } Write-Debug "Ending $($PSBoundParameters | Out-String)" $Output = $TemplateContent -replace '\${Title}', $FinalTitle ` -replace '\${Description}', $FinalDescription ` -replace '\${Author}', $FinalAuthor ` -replace '\${Tables}', ($FinalTable -join "`n`n") ` -replace '\${Charts}', ($FinalChart -join "`n") Write-Output $Output } } function New-Table { #.Synopsis # Creates a new TableData object for rendering in New-Report #.Example # Get-ChildItem C:\Users -Directory | # Select LastWriteTime, @{Name="Length"; Expression={ # (Get-ChildItem $_.FullName -Recurse -File -Force | Measure Length -Sum).Sum # } }, Name | # New-Table -Title $Pwd -Description "Full file listing from $($Pwd.Name)" # # Collect the list of user directories and measure the size of each [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param( # A title that goes on the top of the table [Parameter(Mandatory)] [string]$Title, # Description to go above the table [Parameter()] [string]$Description, # Data for the table (can be piped in) [Parameter(Mandatory,ValueFromPipeline)] [PSObject]$InputObject, # Emphasis value: default (unadorned), primary (highlighted), success (green), info (blue), warning (yellow), danger (red) [Parameter()] [Emphasis]$Emphasis = "primary" ) begin { $TableData = @() } process { $TableData += $InputObject } end { [Table]::new($Title, [PSObject]$TableData, $Description, $Emphasis) } } |