wks_htmlReporting.psm1
function format-htmlTreeLevel() { [cmdletbinding()] param( $dataTree, $idField, $parentidField, $nameField, $parentIdValue, $method, $progressId, $progressCountVariable, $progressTotal ) if ($progressCountVariable) { $progress = get-variable -name $progressCountVariable -valueonly -scope global write-progress -id $progressId -activity "Building TreeView" -PercentComplete ($progress*100/$progressTotal) $progress++ set-variable -name $progressCountVariable -value $progress -force -scope global } $item = $dataTree |? { $_.$idField -eq $parentIdValue} $iconColorStr = "" if ($null -ne $item.icon_color) { $iconColorStr = "style='color:$($item.icon_color)'" } $childItems = @($dataTree |? { $_.$parentidField -eq $parentIdValue }) if ($childItems.count -gt 0) { #write-verbose "Nested Node $($item.$nameField)" "<li class='treeview-animated-items'> <a id='$($item.$idField)' onClick='$method(this.id)' class='closed'> <i class='fas fa-angle-right'></i> <span><i class='far ic-w mx-1 $($item.icon)' $iconColorStr></i>$($item.$nameField)</span> </a> <ul class='nested'>" foreach ($childitem in $childItems) { if ($progressCountVariable) { format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField ` -nameField $nameField -parentidValue $childitem.$idField -method $method ` -progressId $progressId -progressCountVariable $progressCountVariable -progressTotal $progressTotal } else { format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField ` -nameField $nameField -parentidValue $childitem.$idField -method $method } } "</ul> </li>" } else { #write-verbose "End nesting $($item.$nameField)" "<li> <div id='$($item.$idField)' onClick='$method(this.id)' class='treeview-animated-element'><i class='far ic-w mr-1 $($item.icon)' $iconColorStr></i>$($item.$nameField) </li>" } } function get-randomId { param( $length = 8 ) # Characters $chars = (48..57) + (65..90) + (97..122) [string]$Result = $null # First char cannot be numeric (causes issues with Javascript for DOM node id reference) (65..90) + (97..122) | Get-Random |% { $Result += [char]$_ } $chars | Get-Random -Count ($length -1) | ForEach-Object{ $Result += [char]$_ } $Result } function get-translatedNames { param( $collection, [string[]]$ids, $idKey='id', $valueKey='displayName', [switch]$html ) $result = @() foreach ($id in $ids) { $obj = $collection |? {$_.$idKey -eq $id} if ($null -ne $obj) { $result += ("$($obj.$valueKey)" -replace ' ',' ') } else { $result += $id} } if ($html) { $result -join "<br/>" } else { $result -join ', ' } } function get-htmlGroupedProperties { [cmdletbinding()] param ( [string[]]$attributes, [pscustomobject]$object, [switch]$showTitle, [hashtable]$attributeMappingTable, [hashtable]$collapsableAttributes, [hashtable]$collapsableAttributesMaxValue ) if ($object) { foreach ($attrib in $attributes) { if ($null -ne $object.$attrib) { # Apply key name mapping $key = $attrib if ($attributeMappingTable) { if ($null -ne $attributeMappingTable[$attrib]) { $key = $attributeMappingTable[$attrib] } } # by default : concatenate values $val = $(($object.$attrib -replace ' ',' ') -join '<br/>') if ($attrib -like "*user*") { $val = get-translatedNames -collection $users -ids $object.$attrib -html } if ($attrib -like "*group*") { $val = get-translatedNames -collection $groups -ids $object.$attrib -html } if ($attrib -like "*application*") { $val = get-translatedNames -collection $applications -ids $object.$attrib -idKey 'appId' -html } if ($attrib -like "signInFrequency") { $val = "$($object.$attrib.value) $($object.$attrib.type) ($(if ($object.$attrib.isEnabled) {'Enabled'} else {'Disabled'}))" } if ($attrib -like "persistentBrowser") { $val = "$($object.$attrib.mode) ($(if ($object.$attrib.isEnabled) {'Enabled'} else {'Disabled'}))" } if ($attrib -like "cloudAppSecurity") { $val = "$($object.$attrib.cloudAppSecurityType) ($(if ($object.$attrib.isEnabled) {'Enabled'} else {'Disabled'}))" } if ($attrib -like "*Roles*") { $val = get-translatedNames -collection $roles -ids $object.$attrib -html } if ($attrib -like "termsOfUse") { $val = @($object.$attrib |% { $reportConfig.aadTermsOfUseMapping[$_] }) -join ', ' } # builtin controls if ($attrib -like "builtInControls") { $val = $($object.$attrib -join " $($object.operator) ") } if ($attrib -like "operator") { $val = '' } Write-Verbose "Processing attribute $attrib" if ($val -ne '') { if ($showTitle) { $itemCount = @($object.$attrib).count Write-verbose "- itemCount = $itemCount" $htmlBadge = '' $collapsing = '' $linkId = '' Write-Verbose "- collapsableAttributes : $($collapsableAttributes.keys -join ',')" $collapsableTreshold = $null if ($collapsableAttributes.keys -contains $attrib) { Write-Verbose "- collapsableAttributes contains attrib '$attrib'" $collapsableTreshold = $collapsableAttributes[$attrib] } if ($collapsableAttributes.keys -contains '*') { Write-Verbose "- collapsableAttributes contains attrib '*'" $collapsableTreshold = $collapsableAttributes['*'] } Write-verbose "- collapsableTreshold = $collapsableTreshold" # Add a badge to count items (combined with item collapse) if ($null -ne $collapsableTreshold) { if ($itemCount -gt $collapsableTreshold) { $allItems = '' # if all roles are listed add the (All) information into html badge foreach ($attribMax in $collapsableAttributesMaxValue.keys) { if (($attrib -like $attribMax) -and ($itemCount -eq $collapsableAttributesMaxValue[$attribMax])) { $allItems = '(All)'} } # # Adds a badge with the item count (for lisibility when collapsing) $htmlBadge = "<span class='badge badge-pill badge-primary'> $itemCount $allItems </span>" $collapsing = "collapse" $linkId = get-randomId } } $val = " <div class='card'> <div class='card-header mb-1'> <a $(if ($linkId -ne '') {"href='#$linkId'"} )data-bs-toggle='collapse' class='card-link'>$key $htmlBadge</a> </div> <div id='$linkId' class='$collapsing mb-1'> <div class='card-body mb-1'>$val</div> </div> </div>" } else { $val = "<p class='mb-1'>$val</p>" } $val } } } } } <# # # HTML Item # #> function new-htmlItemTable { [cmdletbinding(DefaultParameterSetName = 'textTable')] param( [Parameter(Mandatory=$true, ParameterSetName='textTable')] [Parameter(Mandatory=$true, ParameterSetName='htmlTable')] $dataset, [Parameter(Mandatory=$false, ParameterSetName='textTable')] [Parameter(Mandatory=$false, ParameterSetName='htmlTable')] [string[]]$filterKeys, [Parameter(Mandatory=$false, ParameterSetName='textTable')] [Parameter(Mandatory=$false, ParameterSetName='htmlTable')] [string[]]$hiddenKeys=@(), [Parameter(Mandatory=$false, ParameterSetName='htmlTable')] $notificationLevelQuery, [Parameter(Mandatory=$false, ParameterSetName='htmlTable')] [switch]$dataContainsHtml, [Parameter(Mandatory=$false, ParameterSetName='textTable')] [Parameter(Mandatory=$false, ParameterSetName='htmlTable')] [switch]$noPaging, [Parameter(Mandatory=$false, ParameterSetName='textTable')] [Parameter(Mandatory=$false, ParameterSetName='htmlTable')] [switch]$noSearching ) $htmlItem = new-object -typename htmlItem $keys = $dataset |select-object -excludeproperty $hiddenKeys |Get-Member -MemberType noteproperty |select -ExpandProperty name -unique if ($null -eq $keys) { $keys = $dataset.keys |select -unique |? {$hiddenKeys -notcontains $_} } # filter keys to required fields (also force fileds order at display) if ($null -ne $filterKeys) { $keys = $filterKeys |? {$keys -contains $_} } # Having HTML in TD causes issues with search feature when data is imported from JSON # In this situation, we code the data as full HTML (slower and inefficient for large amount of data) if ($dataContainsHtml) { $htmlItem.htmlContent += " <div class='col w-20 mx-4 my-4 w-auto'> <table class='table table-hover table-striped' id='$($htmlItem.id)' style='width: 100%'> <thead class='table-primary'> <tr> $($keys |foreach-object { if ($hiddenKeys -contains $_) { "<th class='visually-hidden'>$_</th>" } else { "<th>$_</th>" } }) </tr> </thead> <tbody>" foreach ($item in $dataset) { $myClass = '' foreach ($query in $notificationLevelQuery.keys) { Write-Verbose "Item state: $($item.state)" Write-Verbose "- Query: $query" Write-Verbose "- Query result: $($item |% {iex $query})" if ($item |% {iex $query}) { $myClass = "alert " if ($notificationLevelQuery[$query] -eq 'Warning') { $myClass += "alert-warning" } if ($notificationLevelQuery[$query] -eq 'Critical') { $myClass += "alert-danger" } Write-Verbose "- myClass: $myClass" } } $htmlItem.htmlContent += "<tr class='$myClass'>$($keys |foreach-object { $strClass = '' if ($hiddenKeys -contains $_) { $strClass = "class='visually-hidden'" } if ($_ -eq (@($keys)|select -first 1)) { "<th $strClass>$($item.$_)</th>" } else { "<td $strClass>$($item.$_)</td>" } })</tr>" } $htmlItem.htmlContent += '</tbody> </table></div>' # Run table processing to display paging / sorting etc. $htmlItem.javascriptOnLoad += "`$('#$($htmlItem.id)').DataTable({ searchHighlight: true, paging: $(if ($noPaging) {"false"} else {"true"}), searching: $(if ($noSearching) {"false"} else {"true"}), columnDefs: [ { targets: '_all', 'render': function (data, type, row, meta ) { if(type === 'display'){ let aStr = ('' + data).split(',') if (aStr.length > 1) { var str = ''; aStr.forEach((item)=>{ str += item + '<br/>'; }); return str; } else { return data } }else{ return data; } } } ] });" } else { $tableid = "table$(get-randomId)" $htmlItem.htmlContent += " <div class='col w-20 mx-4 my-4 w-auto'> <table class='table table-hover table-striped' id='$($htmlItem.id)' style='width: 100%'> <thead class='table-primary'> <tr> $($keys |foreach-object { "<th>$_</th>" }) </tr> </thead> <tbody></tbody> </table> </div>" $htmlItem.javascriptOnLoad += "var jsonData = $($dataset |ConvertTo-Json); `$('#$($htmlItem.id)').DataTable({ data: jsonData, columns: [ $(($keys |% { "{ data: '$_' }"}) -join ',') ], columnDefs: [ { targets: '_all', 'render': function (data, type, row, meta ) { if(type === 'display'){ let aStr = ('' + data).split(',') if (aStr.length > 1) { var str = ''; aStr.forEach((item)=>{ str += item + '<br/>'; }); return str; } else { return data } }else{ return data; } } }, $(if ($hiddenKeys) { ($hiddenKeys |% { if ($(@($keys).indexof($_) -gt -1)) { "{ targets: [ $(@($keys).indexof($_) ) ], visible: false, searchable: false } "}}) -join ',' }) ], searchHighlight: true, paging: $(if ($noPaging) {"false"} else {"true"}), searching: $(if ($noSearching) {"false"} else {"true"}) });" } return $htmlItem } function new-htmlItemNotifications { param( [Parameter(Mandatory)] [hashtable]$notificationTable ) $htmlItem = new-object -typename htmlItem $htmlItem.htmlContent += " <div class='card col'> <div class='card-body row'>" foreach ($notif in $notificationTable.keys) { if (@("Information", "Success", "Warning", "Critical") -notcontains $notificationTable[$notif]) { throw "The value associated to the key '$notif' is not part of the accepted values ('Information', 'Success', 'Warning', 'Critical')" } $strLevel = "alert " if ($notificationTable[$notif] -eq 'Success') { $strLevel += "alert-success" } if ($notificationTable[$notif] -eq 'Warning') { $strLevel += "alert-warning" } if ($notificationTable[$notif] -eq 'Critical') { $strLevel += "alert-danger" } $htmlItem.htmlContent += "<div class='$strLevel'>$notif</div>" } $htmlItem.htmlContent += " </div> </div>" return $htmlItem } function new-htmlItemTree { [cmdletbinding()] param( $dataTree, $idField, $parentidField, $nameField, $detailField ) $htmlItem = new-object -typename htmlItem $htmlItem.htmlContent += "<div class='container card-body row align-items-start'> <div class='col-1 treeview-animated w-20 border mx-4 my-4'> <ul class='treeview-animated-list mb-3'>" $itemsToProcess = @() # Start with root items $itemsToProcess += $dataTree |? { ($null -eq $_.$parentidField) -or ('' -eq $_.$parentidField.trim()) ` -or ($dataTree.$idField -notcontains $_.$parentidField) } write-verbose "Found $($itemsToProcess.count) root items" foreach ($item in ($itemsToProcess |sort-object -property $nameField)) { Write-Verbose "Processing Node $($item.$nameField)" $htmlItem.htmlContent += format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField ` -nameField $nameField -parentidValue $item.$idField -method 'showTreeDetails' -verbose } $htmlItem.htmlContent += " </ul> </div> <div id='$($htmlItem.id)' class='col-4 w-20 mx-4 my-4'> <!--Details--> </div> <script> function showTreeDetails(elementId) { var tree_$($htmlItem.id) = $($dataTree |convertto-json); jQuery.map(tree_$($htmlItem.id), function(obj) { if (obj.$($idField) === elementId) { `$('#$($htmlItem.id)').text(obj.$detailField); } }); } </script> </div>" $htmlItem.javascriptOnLoad += "`$('.treeview-animated').mdbTreeview();" return $htmlItem } function new-htmlItemTreeWithDetailedTable { [cmdletbinding(DefaultParameterSetName = 'simple')] param( [Parameter(Mandatory=$true, ParameterSetName='simple')] [Parameter(Mandatory=$true, ParameterSetName='customId')] $dataTree, [Parameter(Mandatory=$true, ParameterSetName='simple')] [Parameter(Mandatory=$true, ParameterSetName='customId')] $idField, [Parameter(Mandatory=$true, ParameterSetName='simple')] $parentidField, [Parameter(Mandatory=$true, ParameterSetName='simple')] [Parameter(Mandatory=$true, ParameterSetName='customId')] $nameField, [Parameter(Mandatory=$true, ParameterSetName='simple')] [Parameter(Mandatory=$true, ParameterSetName='customId')] $detailDatas, [Parameter(Mandatory=$true, ParameterSetName='simple')] $detailsId, [Parameter(Mandatory=$false, ParameterSetName='simple')] [Parameter(Mandatory=$false, ParameterSetName='customId')] [String[]]$hiddenFields, [Parameter(Mandatory=$true, ParameterSetName='customId')] [string]$idMappingQuery, [Parameter(Mandatory=$true, ParameterSetName='customId')] [string]$parentMappingQuery, [Parameter(Mandatory=$false, ParameterSetName='simple')] [Parameter(Mandatory=$false, ParameterSetName='customId')] [String]$itemAlertQuery, [Parameter(Mandatory=$false, ParameterSetName='simple')] [Parameter(Mandatory=$false, ParameterSetName='customId')] [switch]$showAlertInHierarchy, [Parameter(Mandatory=$false, ParameterSetName='simple')] [Parameter(Mandatory=$false, ParameterSetName='customId')] [switch]$showOnlyAlertItems ) $originalIdName = $idField # Pre-Processing : # - transform id & parentid fields # - use custom query to match item and parent # - generate unique id to avoid rendering issues (ie. when the id is an URL) # Create HashTable of computed id query -> ID (to compute hierarchy = detect parent) $htComputedId = @{} # Create HashTable of nativeId -> ID (to assign ID for mapping between tables) $htOriginalId = @{} if ($PSCmdlet.ParameterSetName -eq 'customId') { Write-Verbose "new-htmlItemTreeWithDetailedTable -> Create ID mapping HashTable" foreach ($nativeId in $dataTree |select -ExpandProperty $idField) { $id = "url$(get-randomId)" $htOriginalId[$nativeId] = $id # "'{0}' -replace '/sites/', '/' -replace '/lists/', '/'" $computedId = Invoke-Expression ($idMappingQuery.toString() -f ($nativeId -replace "'", "''")) $htComputedId[$computedId] = $id } Write-Verbose "new-htmlItemTreeWithDetailedTable -> Compute parent hierarchy" $index = 0 foreach ($s in $dataTree) { write-progress -activity "Processing Hierarchy" -status "$($s.$idField)" -PercentComplete ($index*100/($dataTree.count)) # "(({0}.url -split '/') |select -SkipLast 1|? {@('sites','lists') -notcontains $_}) -join '/'" $parentId = $null if ($parentMappingQuery) { #Write-Verbose "Parent Mapping: $($parentMappingQuery.toString() -f $s.$idField)" $parentId = Invoke-Expression ($parentMappingQuery.toString() -f ($s.$idField -replace "'", "''")) } else { $parentId = $s.$idField } #$parentId = $synthetic |? {$_.url -like $parentUri} #$s |Add-Member -MemberType NoteProperty -Name 'parent' -Value "$($parentId.id)" $s |Add-Member -MemberType NoteProperty -Name 'parent' -Value $htComputedId[$parentId] $s |Add-Member -MemberType NoteProperty -Name 'id' -Value $htOriginalId[$s.$idField] $index++ } $idField = 'id' $parentIdField = 'parent' write-progress -activity "Processing Hierarchy" -completed } # Create HashTable of ID -> Object (to boost icon assignment on parent item) $htId = @{} if ($PSCmdlet.ParameterSetName -eq 'customId') { foreach ($s in $dataTree) { $htId[$htOriginalId[$s.$originalIdName]] = $s } } else { foreach ($s in $dataTree) { $htId[$s.$originalIdName] = $s } } # Process Alerting on treeview if ($itemAlertQuery) { Write-Verbose "new-htmlItemTreeWithDetailedTable -> Compute tree icons" $index = 0 foreach ($item in $dataTree) { write-progress -activity "Processing icons" -status "$($item.$nameField)" -PercentComplete ($index*100/($dataTree.count)) # "'({0}.PermissionsUniques' -eq 'Oui'" if (Invoke-Expression ($itemAlertQuery.toString() -f (($item|convertto-json) -replace "'", "''"))) { $item |Add-Member -MemberType NoteProperty -Name 'icon' -Value 'fas fa-lock-open' -force $item |Add-Member -MemberType NoteProperty -Name 'icon_color' -Value 'red' -force if ($showAlertInHierarchy) { while ($null -ne $item) { try { $item = $htId[$item.$parentIdField] } catch { $item = $null } $item |% { $_ |Add-Member -MemberType NoteProperty -Name 'icon' -Value 'fas fa-exclamation' -ea Silentlycontinue } } } } $index++ } write-progress -activity "Processing icon" -completed } # Rendering filtering if ($showOnlyAlertItems) { $dataTree = $dataTree |? {$null -ne $_.icon} Write-Verbose "new-htmlItemTreeWithDetailedTable -> Filtered tree data to impacted elements and hierarchy : $($dataTree.count)" $dataFilteredList = $dataTree |select -ExpandProperty $originalIdName $detailDatas = $detailDatas |? {$dataFilteredList -contains $_.$originalIdName} Write-Verbose "new-htmlItemTreeWithDetailedTable -> Filtered details data to impacted elements : $($detailDatas.count)" } # Associate New ID to detailed data if ($PSCmdlet.ParameterSetName -eq 'customId') { $total = @($detailDatas).count Write-Verbose "new-htmlItemTreeWithDetailedTable -> Associate IDs" $index = 0 # Add correponding IDs foreach ($d in $detailDatas) { write-progress -activity "Associating ID on Detailed data" -status "$($d.$originalIdName)" -PercentComplete ($index*100/$total) #$s = $synthetic |? {$_.url -like $d.url} $d |Add-Member -MemberType NoteProperty -Name 'id' -Value $htOriginalId[$d.$originalIdName] $index++ } $detailsId = 'id' write-progress -activity "Processing ID" -completed } # Update hidden fields on detail table to hide id and originalId @($idField, $originalIdName) |% { if ($hiddenFields -notcontains $_) { $hiddenFields += $_ } } # Process Rendering Write-Verbose "new-htmlItemTreeWithDetailedTable -> Rendering HTML TreeView" $htmlItem = new-object -typename htmlItem $htmlItem.htmlContent += "<div class='container card-body row align-items-start'> <div class='col-1 treeview-animated w-20 border mx-4 my-4'> <ul class='treeview-animated-list mb-3'>" $itemsToProcess = @() # Start with root items $itemsToProcess += $dataTree |? { ($null -eq $_.$parentidField) -or ('' -eq $_.$parentidField.trim()) ` -or ($dataTree.$idField -notcontains $_.$parentidField) } $progressId = Get-Random $progresscount = new-variable -name "progress_treeview_$progressId" -value 0 -scope global $totalItems = @($dataTree).count foreach ($item in $itemsToProcess) { $progress = get-variable -name "progress_treeview_$progressId" -valueonly write-progress -id $progressId -activity "Building TreeView" -PercentComplete ($progresst*100/$totalItems) $progress++ set-variable -name "progress_treeview_$progressId" -value $progress -force -scope global $htmlItem.htmlContent += format-htmlTreeLevel -dataTree $dataTree -idField $idField -parentIdField $parentidField ` -nameField $nameField -parentidValue $item.$idField -method 'showTreeDetailed' ` -progressId $progressId -progressCountVariable "progress_treeview_$progressId" -progressTotal $totalItems } write-progress -id $progressId -activity "Building TreeView" -completed $htmlItem.javascriptOnLoad += "`$('.treeview-animated').mdbTreeview();" $keys = $detailDatas |select-object -excludeproperty icon, icon_color |Get-Member -MemberType noteproperty |select -ExpandProperty name -unique if ($null -eq $keys) { $keys = $detailDatas.keys |select -unique } $htmlItem.htmlContent += " </ul> </div> <div class='col w-20 mx-4 my-4 w-auto'> <table class='table table-hover table-striped' id='$($htmlItem.id)' style='width: 100%'> <thead class='table-primary'> <tr> $($keys |foreach-object { "<th>$_</th>" }) </tr> </thead> <tbody></tbody> </table></div> <script> function showTreeDetailed(elementId) { var dataTables = `$('#$($htmlItem.id)').DataTable() dataTables.columns($(@($keys|%{$_.tolower()}).indexof($detailsId.tolower()) )).search(elementId).draw(); } </script> </div>" $htmlItem.javascriptOnLoad += "var jsonData = $($detailDatas |ConvertTo-Json); `$('#$($htmlItem.id)').DataTable({ data: jsonData, columns: [ $(($keys |% { "{ data: '$_' }"}) -join ',') ], columnDefs: [ { targets: '_all', 'render': function (data, type, row, meta ) { if(type === 'display'){ let aStr = ('' + data).split(',') if (aStr.length > 1) { var str = ''; aStr.forEach((item)=>{ str += item + '<br/>'; }); return str; } else { return data } }else{ return data; } } }, $(if ($hiddenFields) { $keys = $keys |% {$_.toLower()} ($hiddenFields |% { if ($(@($keys).indexof($_.toLower()) -gt -1)) { "{ targets: [ $(@($keys).indexof($_.toLower()) ) ], visible: false } "}}) -join ',' }) ], searchHighlight: true, });" #$htmlItem.javascriptOnLoad += "`$('#$($htmlItem.id) tr').toggle();" return $htmlItem } function add-htmlItemNewRowAfter { param( [htmlItem]$item ) $item.newlineAfter = $true } function new-htmlItemChart { param( [string]$title, [Parameter(Mandatory)] $data, [String]$linkedTableId, [String]$linkedFieldName, [Parameter(Mandatory)] [ValidateSet("pie","bar","doughnut")] [String]$chartType, [ValidateSet("left","right","top","bottom")] [String]$legendPosition = 'right' ) $htmlItem = new-object -typename htmlItem $config = @{ type= $chartType data= @{ labels= $data.keys #@('January', 'February', 'March', 'April', 'May', 'June', 'July') datasets= @( @{ data= $data.values #@(1, 65, 45, 65, 35, 65, 30) backgroundColor= @("#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#4D5360", "#E57373", "#F06292", "#BA68C8", "#9575CD", "#7986CB", "#64B5F6", "#4DD0E1", "#4DB6AC", "#81C784", "#AED581", "#DCE775", "#FFD54F", "#FFB74D", "#A1887F", "#90A4AE") |select -first ($data.count) hoverBackgroundColor= @("#FF5A5E", "#5AD3D1", "#FFC870", "#A8B3C5", "#616774", "#EF9A9A", "#F48FB1", "#CE93D8", "#B39DDB", "#9FA8DA", "#90CAF9", "#80DEEA", "#80CBC4", "#A5D6A7", "#C5E1A5", "#E6EE9C", "#FFE082", "#FFCC80", "#BCAAA4", "#B0BEC5") |select -first ($data.count) borderWidth= @(1, 1, 1, 1, 1) } ) } options= @{ responsive= $true title= @{ display= $true text= $title } tooltips= @{ mode= 'index' intersect= $false } hover= @{ mode= 'nearest' intersect= $true } legend= if ($chartType -eq 'bar') { @{display = $false} } else { @{position= $legendPosition} } interaction= @{ mode= 'dataset' } } } # HTML Content to return $htmlItem.htmlContent += "<div class='col-md-4 py-1'> <div class='card'> <div class='card-body'> <canvas id='canvas_$($htmlItem.id)'></canvas> </div> </div> </div> <script> var config_$($htmlItem.id) = $($config |ConvertTo-Json -Depth 10) var ctx_$($htmlItem.id) = document.getElementById('canvas_$($htmlItem.id)').getContext('2d'); window.myLine_$($htmlItem.id) = new Chart(ctx_$($htmlItem.id), config_$($htmlItem.id)); $( if ($linkedTableId) { "document.getElementById('canvas_$($htmlItem.id)').onclick = function(evt){ var firstPoint = window.myLine_$($htmlItem.id).getElementAtEvent(evt)[0]; if (firstPoint) { var label = window.myLine_$($htmlItem.id).data.labels[firstPoint._index]; var linkedDataTable = `$('#$linkedTableId').DataTable() linkedDataTable.columns().eq(0).each( function (colIdx) { var title = linkedDataTable.columns(colIdx).header(); if (`$(title).html() == '$linkedFieldName') { linkedDataTable.columns(colIdx).search(label).draw(); } }); } else { var linkedDataTable = `$('#$linkedTableId').DataTable() linkedDataTable.columns().eq(0).each( function (colIdx) { var title = linkedDataTable.columns(colIdx).header(); if (`$(title).html() == 'Name') { linkedDataTable.columns(colIdx).search('').draw(); } }); } };" } ) </script>" return $htmlItem } class htmlItem { [string]$id = 'item' + (get-randomId -length 12) [String]$htmlContent [string]$javascriptToLoad [string]$javascriptOnLoad [bool]$newlineAfter = $false } <# # # HTML Section # #> function new-htmlSection { param( [String]$title, [switch]$foldable, [switch]$folded, [int]$notifInfoCount = 0, [int]$notifSuccessCount = 0, [int]$notifWarningCount = 0, [int]$notifAlertCount = 0 ) $htmlSection = new-object -typename htmlSection if ($title) {$htmlSection.title = $title} if ($foldable) {$htmlSection.foldable = $true} if ($folded) {$htmlSection.folded = $true} $htmlSection.notif_info = $notifInfoCount $htmlSection.notif_success = $notifSuccessCount $htmlSection.notif_warning = $notifWarningCount $htmlSection.notif_alert = $notifAlertCount return $htmlSection } function add-htmlSectionItem { param( [Parameter(ValueFromPipeline=$true)] [htmlSection]$section, [htmlItem[]]$itemList ) foreach ($item in $itemList) { $section.addHtmlItem($item) } } class htmlSection { [string]$id = 'section' + (get-randomId -length 12) [string]$title [string]$data [string]$htmlCharts [bool]$hasTree=$false [string[]]$javascriptToLoad [string[]]$javascriptOnLoad [htmlItem[]]$itemList [bool]$foldable = $false [bool]$folded = $false [int]$notif_info = 0 [int]$notif_success = 0 [int]$notif_warning = 0 [int]$notif_alert = 0 [void]addHtmlContent($htmlContent) { $this.data += $htmlContent } [void]addHtmlItem([htmlItem]$item) { $this.itemList += $item } [string]toHtml() { $collapsing = "" $defaultShow = "" $sectionId = "section$(get-randomId)" if ($this.foldable) { $collapsing = "collapse" if ($this.folded -eq $false) { $defaultShow = "show" } } return " <div class='card'> <h2 class='card-header'><a href='#$sectionId' data-bs-toggle='$collapsing'>$($this.title)</a> $(if ($this.notif_info -gt 0) { "<span class='badge-pill badge-info'>$($this.notif_info)</span>"}) $(if ($this.notif_success -gt 0) { "<span class='badge-pill badge-success'>$($this.notif_success)</span>"}) $(if ($this.notif_warning -gt 0) { "<span class='badge-pill badge-warning'>$($this.notif_warning)</span>"}) $(if ($this.notif_alert -gt 0) { "<span class='badge-pill badge-danger'>$($this.notif_alert)</span>"}) </h2> <div id='$sectionId' class='card-body row align-items-start gap-2 $collapsing $defaultShow'> $(foreach ($item in $this.itemList) { " $($item.htmlContent)" if ($item.newlineAfter) { " </div> <div id='$sectionId' class='card-body row align-items-start gap-2 $collapsing $defaultShow'> "} $this.javascriptOnLoad += $item.javascriptOnLoad $this.javascriptToLoad += $item.javascriptToLoad }) </div> </div>" } } <# # # HTML Report # #> function new-htmlReport { param( [string]$title, [string]$subtitle, [string]$logoFile ) $htmlreport = new-object -typename htmlReport if ($title) {$htmlreport.title = $title} if ($subtitle) {$htmlreport.subtitle = $subtitle} if ($logoFile) {$htmlReport.addLogo($logoFile)} return $htmlreport } function add-htmlReportSection { param( [Parameter(ValueFromPipeline=$true)] [htmlReport]$report, [htmlSection[]]$sectionList ) foreach ($section in $sectionList) { $report.sections += $section } } class htmlReport { [string]$title [string]$subtitle [htmlSection[]]$sections [string]$logoBase64 [string]toHtml() { $result = "<!DOCTYPE html> <html lang='en'> <head> <title>$($this.title)</title> <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css' integrity='sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC' crossorigin='anonymous'> <link rel='stylesheet' href='https://cdn.datatables.net/1.10.25/css/dataTables.bootstrap5.min.css'> <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.9.0/css/mdb.css'> <link rel='stylesheet' href='https://cdn.datatables.net/plug-ins/1.10.25/features/searchHighlight/dataTables.searchHighlight.css'> <script src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'></script> <script src='https://kit.fontawesome.com/3bf1dda615.js' crossorigin='anonymous'></script> <style type='text/css'> .card-body,.card-header { padding: 0.5em ; word-break: keep-all; } .jumbotron { padding-top: 1em; padding-bottom: 1em; } .card { margin-bottom: 0.5em; } .visually-hidden { display: none; } canvas{ -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; } .treeview-animated { max-height: 500px; overflow-y: scroll; } table { width: 100%; } </style> </head> <body> <div class='navbar navbar-light'> <img class='navbar-brand' src='$($this.logoBase64)' style='max-width:100%;'/> <div class='col'> <h1 class='display-4'>$($this.title)</h1>" if ($this.subtitle) { $result += "<p class='lead'>$($this.subtitle)</p>" } $result += "<p class='display-8'>Generated on $(Get-Date)</p> </div> </div> " $result += $this.sections | ForEach-Object { $_.toHtml() } $result += ' <script src=''https://code.jquery.com/jquery-3.6.0.min.js'' integrity=''sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4='' crossorigin=''anonymous''></script> <script src=''https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js'' integrity=''sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF'' crossorigin=''anonymous''></script> <script src=''https://cdnjs.cloudflare.com/ajax/libs/mdbootstrap/4.9.0/js/mdb.min.js''></script> <script src=''https://cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js''></script> <script src=''https://cdn.datatables.net/1.10.25/js/dataTables.bootstrap4.min.js''></script> <script src=''https://bartaz.github.io/sandbox.js/jquery.highlight.js''></script> <script src=''https://cdn.datatables.net/plug-ins/1.10.25/features/searchHighlight/dataTables.searchHighlight.min.js''></script> ' # Add Custom script resources $_.javascriptToLoad |% { $result += $_ } $result += '<script>$(document).ready(function() {' if ($this.sections.hasTree -contains $true) { $result += "`$('.treeview-animated').mdbTreeview();" } # Add onLoad calls $result += $this.sections | ForEach-Object { $_.javascriptOnLoad } $result += ' });</script>' $result += '</body> </html>' return $result } [void]addLogo([string]$filename) { if ((test-path $filename) -eq $false) { write-warning "Failed opening logo image: file not found." } else { $file = get-item $filename $this.logoBase64 = "data:image/" $extension = $file.name -split '\.' |select -last 1 $this.logoBase64 += $extension $this.logoBase64 += ";base64," $this.logoBase64 += [convert]::ToBase64String((get-content $filename -encoding byte)) } } [void]toFile([string]$filename) { $this.toHtml() |out-file -FilePath $filename -Encoding utf8 } } |