diff --git a/Core.psm1 b/Core.psm1 index 456928c..79d15db 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -11,7 +11,7 @@ This module handles the WPF UI function Get-ModuleVersion { - '3.3.1' + '3.3.2' } function Start-CoreApp @@ -784,7 +784,7 @@ function Remove-Property function Get-GridCheckboxColumn { - param($bindingProperty = "IsSelected") + param($bindingProperty = "IsSelected", [scriptblock]$scriptBlock) $binding = [System.Windows.Data.Binding]::new($bindingProperty) $binding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged @@ -792,6 +792,11 @@ function Get-GridCheckboxColumn $fef = [System.Windows.FrameworkElementFactory]::new([System.Windows.Controls.CheckBox]) $binding.Mode = [System.Windows.Data.BindingMode]::TwoWay $fef.SetValue([System.Windows.Controls.CheckBox]::IsCheckedProperty,$binding) + if($null -ne $scriptBlock) + { + [System.Windows.RoutedEventHandler]$checkedEventHandler = $scriptBlock + $fef.AddHandler([System.Windows.Controls.CheckBox]::CheckedEvent, $checkedEventHandler) + } $dt = [System.Windows.DataTemplate]::new() $dt.VisualTree = $fef $column.CellTemplate = $dt @@ -799,6 +804,10 @@ function Get-GridCheckboxColumn $header.Margin = [System.Windows.Thickness]::new(-4,0,0,0) # Align header checkbox with the row checkboxes $header.ToolTip = "Select/deselect all items" $column.Header = $header + if($null -ne $scriptBlock) + { + #$header.add_click($scriptBlock) + } $column } @@ -1567,7 +1576,7 @@ function Add-DefaultSettings Type = "Boolean" DefaultValue = $false Description = "Enable featurs that are marked as Preview. This might require a restart and prompt for consent" - }) "General" + }) "General" } function Add-SettingsObject diff --git a/Documentation.md b/Documentation.md index 16577bb..44c6933 100644 --- a/Documentation.md +++ b/Documentation.md @@ -54,6 +54,8 @@ This is the first version of the documentation support. * Some Endpoint Security/Settings Catalog items is not translated based on Graph API in the portal e.g. *Antivirus - Windows 10 and Windows Server (ConfigMgr)* policies. These will be documented based on Graph API information which might be different compared to the portal +* Markdown is currently in experimental state. The script can document to an MD file created in the Documents folder but this can be to large in environments with many objects. The script will create HTML tables to support code blocks and column span. The MD View must support HTML tables to display the document. The *Markdown Viewer* extension in Chrome was used during testing. + Please create an [Issue](https://github.com/Micke-K/IntuneManagement/issues) if properties are documented incorrectly or missing. # Deep Dive diff --git a/Extensions/Compare.psm1 b/Extensions/Compare.psm1 index e4a53cb..308312a 100644 --- a/Extensions/Compare.psm1 +++ b/Extensions/Compare.psm1 @@ -11,7 +11,7 @@ Objects can be compared based on Properties or Documentatation info. function Get-ModuleVersion { - '1.0.8' + '1.0.9' } function Invoke-InitializeModule @@ -127,9 +127,6 @@ function Invoke-ShowMainWindow $button.Margin = "0,0,5,0" $button.IsEnabled = $false $button.ToolTip = "Compare object with exported file" - $global:dgObjects.add_selectionChanged({ - Set-XamlProperty $global:dgObjects.Parent "btnCompare" "IsEnabled" (?: ($global:dgObjects.SelectedItem -eq $null) $false $true) - }) $button.Add_Click({ Show-CompareForm $global:dgObjects.SelectedItem @@ -140,6 +137,12 @@ function Invoke-ShowMainWindow $global:spSubMenu.Children.Insert(0, $button) } +function Invoke-EMSelectedItemsChanged +{ + $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem) + Set-XamlProperty $global:dgObjects.Parent "btnCompare" "IsEnabled" $hasSelectedItems +} + function Invoke-ViewActivated { if($global:currentViewObject.ViewInfo.ID -ne "IntuneGraphAPI") { return } diff --git a/Extensions/Documentation.psm1 b/Extensions/Documentation.psm1 index 6ae6a82..f1d11bd 100644 --- a/Extensions/Documentation.psm1 +++ b/Extensions/Documentation.psm1 @@ -20,13 +20,38 @@ $global:documentationProviders = @() function Get-ModuleVersion { - '1.0.7' + '1.0.8' } function Invoke-InitializeModule { # Make sure we add the default Output types Add-OutputType + + $script:columnHeaders = @{ + Name="Inputs.displayNameLabel" + Value="TableHeaders.value" + Description="TableHeaders.description" + GroupMode="SettingDetails.modeTableHeader" #assignmentTypeSelectionLabel? + Group="TableHeaders.assignedGroups" + Groups="TableHeaders.groups" + useDeviceContext="SettingDetails.installContextLabel" + uninstallOnDeviceRemoval="SettingDetails.UninstallOnRemoval" + isRemovable="SettingDetails.installAsRemovable" + vpnConfigurationId="PolicyType.vpn" + Action="SettingDetails.actionColumnName" + Schedule="ScheduledAction.List.schedule" + MessageTemplate="ScheduledAction.Notification.messageTemplate" + EmailCC="ScheduledAction.Notification.additionalRecipients" + Filter="AssignmentFilters.assignmentFilterColumnHeader" + Rule="ApplicabilityRules.GridLabel.Rule" + ValueWithLabel="TableHeaders.value" + Status="TableHeaders.status" + CombinedValueWithLabel="TableHeaders.value" + CombinedValue="TableHeaders.value" + useDeviceLicensing="TableHeaders.licenseType" + #filterMode="Filter mode" # Not in any string file yet + } } function Invoke-ShowMainWindow @@ -38,12 +63,6 @@ function Invoke-ShowMainWindow $button.Margin = "0,0,5,0" $button.IsEnabled = $false $button.ToolTip = "Document selected objects" - $global:dgObjects.add_selectionChanged({ - ##Set-XamlProperty $global:dgObjects.Parent "btnDocument" "IsEnabled" (?: ($global:dgObjects.SelectedItem -eq $null) $false $true) - #$itemSelected = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true).Count -ge 0 -or $global:dgObjects.SelectedItem - - Set-XamlProperty $global:dgObjects.Parent "btnDocument" "IsEnabled" (?: ($global:dgObjects.SelectedItem -eq $null) $false $true) - }) $button.Add_Click({ @@ -57,6 +76,12 @@ function Invoke-ShowMainWindow $global:spSubMenu.Children.Insert(0, $button) } +function Invoke-EMSelectedItemsChanged +{ + $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem) + Set-XamlProperty $global:dgObjects.Parent "btnDocument" "IsEnabled" $hasSelectedItems +} + function Invoke-GraphObjectsChanged { $btnDocument = $global:spSubMenu.Children | Where-Object { $_.Name -eq "btnDocument" } @@ -87,6 +112,35 @@ function Invoke-ViewActivated } } +function Set-DocColumnHeaderLanguageId +{ + param($columnName, $lngId) + + if(-not $script:columnHeaders -or -not $lngId) { return } + + if($script:columnHeaders.ContainsKey($columnName)) + { + $script:columnHeaders[$columnName] = $lngId + } + else + { + $script:columnHeaders.Add($columnName, $lngId) + } +} + +function Invoke-DocTranslateColumnHeader +{ + param($columnName) + + $lngText = "" + if($script:columnHeaders.ContainsKey($columnName)) + { + $lngText = Get-LanguageString $script:columnHeaders[$columnName] + } + + (?? $lngText $columnName) +} + function Add-OutputType { param($outputInfo) @@ -588,27 +642,6 @@ function Add-ScopeTagStrings } } -function Get-AllEntityTypes -{ - param($entityType, $xml, $hashTable) - - if(-not $hashTable.ContainsKey($entityType)) - { - $hashTable.Add($entityType, $xml.SelectSingleNode("//*[name()='EntityType' and @Name='$entityType']")) - } - - $nodes = $xml.SelectNodes("//*[@BaseType='graph.$entityType']") - - foreach($node in $nodes) - { - if($node.Abstract -ne "true") - { - $hashTable.Add($node.Name, $node) - } - Get-AllEntityTypes $node.Name $xml $hashTable - } -} - function Get-ObjectPlatformFromType { param($obj) diff --git a/Extensions/DocumentationMD.psm1 b/Extensions/DocumentationMD.psm1 new file mode 100644 index 0000000..395eb14 --- /dev/null +++ b/Extensions/DocumentationMD.psm1 @@ -0,0 +1,482 @@ +function Get-ModuleVersion +{ + '1.0.0' +} + +function Invoke-InitializeModule +{ + Add-OutputType ([PSCustomObject]@{ + Name="Markdown (Experimental)" + Value="md" + #OutputOptions = (Add-MDOptionsControl) + #Activate = { Invoke-MDActivate @args } + PreProcess = { Invoke-MDPreProcessItems @args } + NewObjectGroup = { Invoke-MDNewObjectGroup @args } + Process = { Invoke-MDProcessItem @args } + PostProcess = { Invoke-MDPostProcessItems @args } + ProcessAllObjects = { Invoke-MDProcessAllObjects @args } + }) +} + + + +function Invoke-MDPreProcessItems +{ + $script:mdStrings = [System.Text.StringBuilder]::new() + $script:sectionAnchors = @() + $script:totAnchors = @() +} + +function Invoke-MDPostProcessItems +{ + $userName = $global:me.displayName + if($global:me.givenName -and $global:me.surname) + { + $userName = ($global:me.givenName + " " + $global:me.surname) + } + + $script:mdContent = [System.Text.StringBuilder]::new() + + $script:mdContent.AppendLine("# $((?? $global:txtMDTitleProperty.Text "Intune documentation"))") + $script:mdContent.AppendLine("") + $script:mdContent.AppendLine("") + + $mail = "" + if($global:me.mail) + { + $mail = " ($($global:me.mail))" + } + + $script:mdContent.AppendLine("*Organization:* $($global:Organization.displayName)`n") + $script:mdContent.AppendLine("*Generated by:* $userName$mail`n") + $script:mdContent.AppendLine("*Generated:* $((Get-Date).ToShortDateString()) $((Get-Date).ToLongTimeString())`n") + + if($script:sectionAnchors.Count -gt 0) + { + $script:mdContent.AppendLine("") + $script:mdContent.AppendLine("## Table of Contents") + } + + foreach($header in $script:sectionAnchors) + { + $script:mdContent.AppendLine("[$($header.Name)](#$($header.Anchor))`n") + } + + $script:mdContent.AppendLine("") + $mdText = $script:mdContent.ToString() + $mdText += $script:mdStrings.ToString() + + $fileName = Expand-FileName "%MyDocuments%\%Organization%-%Date%.md" + + try + { + $mdText | Out-File -FilePath $fileName -Force -Encoding utf8 -ErrorAction Stop + Write-Log "Markdown document $fileName saved successfully" + } + catch + { + Write-LogError "Failed to save Markdown file" $_.Exception + } +} + +function Invoke-MDNewObjectGroup +{ + param($obj, $documentedObj) + + $objectTypeString = Get-ObjectTypeString $obj.Object $obj.ObjectType + + Add-MDHeader "$((?? $objectTypeString $obj.ObjectType.Title))" -Level 1 -USEHtml + +} + +function Invoke-MDProcessItem +{ + param($obj, $objectType, $documentedObj) + + if(!$documentedObj -or !$obj -or !$objectType) { return } + + $objName = Get-GraphObjectName $obj $objectType + + Add-MDHeader $objName -Level 2 -USEHtml + + $script:mdStrings.AppendLine("") + + try + { + foreach($tableType in @("BasicInfo","FilteredSettings")) + { + if($tableType -eq "BasicInfo") + { + $properties = @("Name","Value") + } + elseif($global:cbMDDocumentationProperties.SelectedValue -eq 'extended' -and $documentedObj.DisplayProperties) + { + $properties = @("Name","Value","Description") + } + elseif($global:cbMDDocumentationProperties.SelectedValue -eq 'custom' -and $global:txtMDCustomProperties.Text) + { + $properties = @() + + foreach($prop in $global:txtMDCustomProperties.Text.Split(",")) + { + # This will add language support for custom columens (or replacing existing header) + $propInfo = $prop.Split('=') + if(($propInfo | measure).Count -gt 1) + { + $properties += $propInfo[0] + Set-DocColumnHeaderLanguageId $propInfo[0] $propInfo[1] + } + else + { + $properties += $prop + } + } + } + else + { + $properties = (?? $documentedObj.DefaultDocumentationProperties (@("Name","Value"))) + } + + $lngId = ?: ($tableType -eq "BasicInfo") "SettingDetails.basics" "TableHeaders.settings" -AddCategories + + Add-MDTableItems $obj $objectType ($documentedObj.$tableType) $properties $lngId -AddCategories -AddSubcategories + + #Add-MDTableItems $obj $objectType ($documentedObj.$tableType) $properties $lngId ` + # -AddCategories:($global:chkMDAddCategories.IsChecked -eq $true) ` + # -AddSubcategories:($global:chkMDAddSubCategories.IsChecked -eq $true) + } + + if(($documentedObj.ComplianceActions | measure).Count -gt 0) + { + $properties = @("Action","Schedule","MessageTemplate","EmailCC") + + Add-MDTableItems $obj $objectType $documentedObj.ComplianceActions $properties "Category.complianceActionsLabel" + } + + if(($documentedObj.ApplicabilityRules | measure).Count -gt 0) + { + $properties = @("Rule","Property","Value") + + Add-MDTableItems $obj $objectType $documentedObj.ApplicabilityRules $properties "SettingDetails.applicabilityRules" + } + + Add-MDObjectSettings $obj $objectType $documentedObj + + if(($documentedObj.Assignments | measure).Count -gt 0) + { + $params = @{} + if($documentedObj.Assignments[0].RawIntent) + { + $properties = @("GroupMode","Group","Filter","FilterMode") + + $settingsObj = $documentedObj.Assignments | Where { $_.Settings -ne $null } | Select -First 1 + + if($settingsObj) + { + foreach($objProp in $settingsObj.Settings.Keys) + { + if($objProp -in $properties) { continue } + if($objProp -in @("Category","RawIntent")) { continue } + $properties += ("Settings." + $objProp) + } + } + } + else + { + $isFilterAssignment = $false + foreach($assignment in $documentedObj.Assignments) + { + if(($assignment.target.PSObject.Properties | Where Name -eq "deviceAndAppManagementAssignmentFilterType")) + { + $isFilterAssignment = $true + break + } + } + $properties = @("Group") + if($isFilterAssignment) + { + $properties += @("Filter","FilterMode") + } + $params.Add("AddCategories", $true) + } + + Add-MDTableItems $obj $objectType $documentedObj.Assignments $properties "TableHeaders.assignments" @params + } + } + catch + { + Write-LogError "Failed to process object $objName" $_.Exception + } +} + +function Add-MDTableItems +{ + param($obj, $objectType, $items, $properties, $lngId, [switch]$AddCategories, [switch]$AddSubcategories, $captionOverride) + + if($captionOverride) + { + $caption = $captionOverride + } + elseif($lngId) + { + $caption = "$((Get-LanguageString $lngId)) - $((Get-GraphObjectName $obj $objectType))" + } + else + { + $caption = "$((Get-GraphObjectName $obj $objectType)) ($($objectType.Title))" + } + + $tableText = [System.Text.StringBuilder]::new() + $tableText.AppendLine("") + $tableText.AppendLine("") + $columnHeaders = "|" + $columnChars = "|" + $columnCount = 0 + foreach($prop in $properties) + { + $tableText.AppendLine("") + $columnCount++ + + $columnHeaders += ((Invoke-DocTranslateColumnHeader ($prop.Split(".")[-1])) + "|") + $columnChars += "----|" + } + $tableText.AppendLine("") + + #Add-MDText $columnHeaders + #Add-MDText $columnChars + + $curCategory = "" + $curSubCategory = "" + + $columnCategory = $null + $columnSubCategory = $null + + foreach($itemObj in $items) + { + if($itemObj.Category -and $curCategory -ne $itemObj.Category -and $AddCategories -eq $true) + { + $tableText.AppendLine("") + $tableText.AppendLine("") + $tableText.AppendLine("") + + #$columnCategory = "|**" + (Set-MDText $itemObj.Category) + "**|" + #Add-MDText $columnCategory + $curCategory = $itemObj.Category + $curSubCategory = "" + } + + if($itemObj.SubCategory -and $curSubCategory -ne $itemObj.SubCategory -and $AddSubcategories -eq $true) + { + $tableText.AppendLine("") + $tableText.AppendLine("") + $tableText.AppendLine("") + + #$columnSubCategory = "|***" + (Set-MDText $itemObj.SubCategory) + "***|" + #Add-MDText $columnSubCategory + $curSubCategory = $itemObj.SubCategory + } + + #$columnData = "|" + try + { + $tableText.AppendLine("") + + foreach($prop in $properties) + { + try + { + # This adds support for properties like Settings.PropName + $propArr = $prop.Split('.') + $tmpObj = $itemObj + $propName = $propArr[-1] + for($x = 0; $x -lt ($propArr.Count - 1);$x++) + { + $tmpObj = $tmpObj."$($propArr[$x])" + } + $tableText.AppendLine("") + #$columnData += "$((Set-MDText "$($tmpObj.$propName)"))|" + } + catch + { + #$columnData += "|" + Write-LogError "Failed to add property value for $prop" $_.Exception + } + } + + } + catch + { + Write-Log "Failed to process property" 2 + } + finally + { + $tableText.AppendLine("") + } + + #Add-MDText $columnData + } + + $tableText.AppendLine("
$($prop.Split(".")[-1])
`n`n**$((Set-MDText $itemObj.Category))**`n`n
`n`n***$((Set-MDText $itemObj.SubCategory))***`n`n
$((Set-MDText "$($tmpObj.$propName)" -CodeBlock))
") + Add-MDText $tableText.ToString() + + Add-MDHeader $caption -Level 6 -TOT -AddParagraph +} + +function Add-MDText +{ + param($text, [switch]$AddParagraph) + + $script:mdStrings.AppendLine($text) + + if($AddParagraph -eq $true) + { + # Add new paragraph by default + $script:mdStrings.AppendLine("") + } +} + +function Set-MDText +{ + param($text, [switch]$CodeBlock) + + if($CodeBlock -eq $true) + { + $trimText = $text.Trim() + if($trimText.StartsWith("<") -and $trimText.EndsWith(">")) + { + return ([Environment]::NewLine + [Environment]::NewLine + "``````xml" + [Environment]::NewLine + $text + [Environment]::NewLine + "``````" + [Environment]::NewLine + [Environment]::NewLine) + } + } + + $text = $text.Replace("|", '`|') + $text = $text.Replace("*", '`*') + $text = $text.Replace("$", '`$') + $text = $text.Replace("`r`n", "
") + $text = $text.Replace("`n", "
") + + $text +} + +function Add-MDHeader +{ + param($text, [int]$level = 1, [switch]$AddParagraph, [switch]$UseHTML, [switch]$ToT, [switch]$SkipTOC) + + $prefix = "" + if($ToT -eq $true) + { + $prefix = "Table $(($script:totAnchors.Count + 1)). " + } + + if($UseHTML -eq $true) + { + if($ToT -eq $true) + { + $sectionAnchor = "table-$(($script:totAnchors.Count + 1))" + } + else + { + $sectionAnchor = "section-$(($script:sectionAnchors.Count + 1))" + } + + $script:mdStrings.AppendLine("$text") + } + else + { + # Warnig: Not complete! Use HTML if not working... + $text = "$prefix$text" + $sectionAnchor = $text.ToLower().Replace(" ","-").Replace("[","").Replace("]","") + + $mdHeader = [String]::new('#',$level) + $script:mdStrings.AppendLine("$mdHeader $text") + } + + if($ToT -eq $true) + { + $script:totAnchors += [PSCustomObject]@{ + Name = $text + Anchor = $sectionAnchor + } + } + elseif($SkipTOC -ne $true) + { + $script:sectionAnchors += [PSCustomObject]@{ + Name = $text + Anchor = $sectionAnchor + } + } + + if($AddParagraph -eq $true) + { + # Add new paragraph by default + $script:mdStrings.AppendLine("`n") + } +} + +function Add-MDObjectSettings +{ + param($obj, $objectType, $documentedObj) + + if($obj."@OData.Type" -eq "#microsoft.graph.deviceManagementScript") + { + if($obj.ScriptContent) + { + $script:mdStrings.AppendLine("~~~powershell") + $script:mdStrings.AppendLine((Get-DocScriptContent $obj.ScriptContent)) + $script:mdStrings.AppendLine("~~~") + $caption = "{1} - {0}" -f $obj.fileName,(Get-LanguageString "WindowsManagement.powerShellScriptObjectName") + Add-MDHeader $caption -Level 6 -SkipTOC -AddParagraph + } + } + if($obj."@OData.Type" -eq "#microsoft.graph.deviceShellScript") + { + if($obj.ScriptContent) + { + $script:mdStrings.AppendLine("~~~shell") + $script:mdStrings.AppendLine((Get-DocScriptContent $obj.ScriptContent)) + $script:mdStrings.AppendLine("~~~") + + $caption = "{1} - {0}" -f $obj.fileName,(Get-LanguageString "WindowsManagement.shellScriptObjectName") + Add-MDHeader $caption -Level 6 -SkipTOC -AddParagraph + } + } + elseif($obj."@OData.Type" -eq "#microsoft.graph.deviceHealthScript") + { + if($obj.detectionScriptContent) + { + $script:mdStrings.AppendLine("~~~powershell") + $script:mdStrings.AppendLine((Get-DocScriptContent $obj.detectionScriptContent)) + $script:mdStrings.AppendLine("~~~") + $caption = Get-LanguageString "ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label" + Add-MDHeader $caption -Level 6 -SkipTOC -AddParagraph + } + + if($obj.remediationScriptContent) + { + $script:mdStrings.AppendLine("~~~powershell") + $script:mdStrings.AppendLine((Get-DocScriptContent $obj.remediationScriptContent)) + $script:mdStrings.AppendLine("~~~") + $caption = Get-LanguageString "ProactiveRemediations.Create.Settings.RemediationScriptMultiLineTextBox.label" + Add-MDHeader $caption -Level 6 -SkipTOC -AddParagraph + } + } + elseif($obj."@OData.Type" -eq "#microsoft.graph.win32LobApp") + { + foreach($rule in ($obj.requirementRules | Where { $_.'@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptRequirement" } )) + { + $script:mdStrings.AppendLine("~~~powershell") + $script:mdStrings.AppendLine((Get-DocScriptContent $obj.scriptContent)) + $script:mdStrings.AppendLine("~~~") + $caption = "{0} - {1}" -f @($obj.displayName, "Requirement script") + Add-MDHeader $caption -Level 6 -SkipTOC -AddParagraph + } + + foreach($rule in ($obj.detectionRules | Where { $_.'@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptDetection" } )) + { + $script:mdStrings.AppendLine("~~~powershell") + $script:mdStrings.AppendLine((Get-DocScriptContent $obj.scriptContent)) + $script:mdStrings.AppendLine("~~~") + $caption = "{0} - {1}" -f @($obj.displayName,(Get-LanguageString "ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label")) + Add-MDHeader $caption -Level 6 -SkipTOC -AddParagraph + } + } +} \ No newline at end of file diff --git a/Extensions/DocumentationWord.psm1 b/Extensions/DocumentationWord.psm1 index ba84676..ab520aa 100644 --- a/Extensions/DocumentationWord.psm1 +++ b/Extensions/DocumentationWord.psm1 @@ -3,7 +3,7 @@ #https://docs.microsoft.com/en-us/office/vba/api/overview/word function Get-ModuleVersion { - '1.0.6' + '1.0.7' } function Invoke-InitializeModule @@ -163,7 +163,7 @@ function Invoke-WordPreProcessItems } catch { - Write-LogError "Failed to create document based on tmeplate: $($global:txtWordDocumentTemplate.Text)" $_.Exception + Write-LogError "Failed to create document based on template: $($global:txtWordDocumentTemplate.Text)" $_.Exception } } else @@ -442,7 +442,7 @@ function Invoke-WordProcessItem foreach($prop in $global:txtWordCustomProperties.Text.Split(",")) { - # This will add language support for custom colument (or replacing existing header) + # This will add language support for custom columens (or replacing existing header) $propInfo = $prop.Split('=') if(($propInfo | measure).Count -gt 1) { diff --git a/Extensions/EndpointManager.psm1 b/Extensions/EndpointManager.psm1 index 0e8dd2e..3231a27 100644 --- a/Extensions/EndpointManager.psm1 +++ b/Extensions/EndpointManager.psm1 @@ -11,7 +11,7 @@ This module is for the Endpoint Manager/Intune View. It manages Export/Import/Co #> function Get-ModuleVersion { - '3.1.13' + '3.1.14' } function Invoke-InitializeModule @@ -225,6 +225,7 @@ function Invoke-InitializeModule Icon = "Branding" SkipRemoveProperties = @('Id') GroupId = "Azure" + SkipAddIDOnExport = $true }) Add-ViewItem (New-Object PSObject -Property @{ @@ -722,9 +723,7 @@ function Set-EMViewPanel Set-XamlProperty $panel "btnDelete" "Visibility" (?: ($allowDelete -eq $true) "Visible" "Collapsed") $global:dgObjects.add_selectionChanged({ - Set-XamlProperty $this.Parent "btnView" "IsEnabled" (?: ($null -eq $global:dgObjects.SelectedItem) $false $true) - Set-XamlProperty $this.Parent "btnCopy" "IsEnabled" (?: ($null -eq $global:dgObjects.SelectedItem) $false $true) - Set-XamlProperty $this.Parent "btnDelete" "IsEnabled" (?: ($null -eq $global:dgObjects.SelectedItem -and $global:curObjectType.AllowDelete -ne $false) $false $true) + Invoke-ModuleFunction "Invoke-EMSelectedItemsChanged" }) # ToDo: Move this to the view object @@ -760,12 +759,40 @@ function Set-EMViewPanel Show-GraphObjects Write-Status "" }) - } + } + + $global:btnLoadAllPages.add_click({ + Write-Status "Loading $($global:curObjectType.Title) objects" + $graphObjects = @(Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -AllPages) + $graphObjects | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } + $global:dgObjects.ItemsSource.CommitNew() + Set-GraphPagesButtonStatus + Invoke-FilterBoxChanged $global:txtFilter -ForceUpdate + Write-Status "" + }) + + $global:btnLoadNextPage.add_click({ + Write-Status "Loading $($global:curObjectType.Title) objects" + $graphObjects = @(Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -SinglePage) + $graphObjects | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } + $global:dgObjects.ItemsSource.CommitNew() + Set-GraphPagesButtonStatus + Invoke-FilterBoxChanged $global:txtFilter -ForceUpdate + Write-Status "" + }) +} + +function Invoke-EMSelectedItemsChanged +{ + $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem) + Set-XamlProperty $global:dgObjects.Parent "btnView" "IsEnabled" $hasSelectedItems #(?: ($null -eq ($global:dgObjects.SelectedItem)) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnCopy" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnDelete" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem -and $global:curObjectType.AllowDelete -ne $false) $false $true) } function Invoke-FilterBoxChanged { - param($txtBox) + param($txtBox,[switch]$ForceUpdate) $filter = $null @@ -774,11 +801,18 @@ function Invoke-FilterBoxChanged $txtBox.FontStyle = "Italic" $txtBox.Tag = 1 $txtBox.Text = "Filter" - $txtBox.Foreground="Lightgray" + $txtBox.Foreground="Lightgray" + } + elseif($ForceUpdate -eq $true) + { + $dgObjects.ItemsSource.Filter = $dgObjects.ItemsSource.Filter + } + elseif($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) + { + } else - { - if($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) { return } + { $txtBox.FontStyle = "Normal" $txtBox.Tag = $null $txtBox.Foreground="Black" @@ -800,11 +834,9 @@ function Invoke-FilterBoxChanged } } - if($dgObjects.ItemsSource -is [System.Windows.Data.ListCollectionView]) + if($dgObjects.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true) { - # This causes odd behaviour with focus e.g. and item has to be clicked twice to be selected $dgObjects.ItemsSource.Filter = $filter - $dgObjects.ItemsSource.Refresh() } $allObjectsCount = 0 @@ -869,9 +901,15 @@ function Start-PostExportEndpointSecurity { param($obj, $objectType, $path) + $fileName = (Get-GraphObjectName $obj $objectType) + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/settings" $settingsJson = "{ `"settings`": $((ConvertTo-Json $settings.value -Depth 20 ))`n}" - $fileName = "$path\$((Remove-InvalidFileNameChars (Get-GraphObjectName $obj $objectType)))_Settings.json" + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" $settingsJson | Out-File -LiteralPath $fileName -Force } @@ -1142,8 +1180,8 @@ function Start-PostGetIntuneBranding foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) { - Write-LogDebug "Get $imgType for $($obj.profileName)" - $imgJson = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)/$imgType" + Write-LogDebug "Get $imgType for $($obj.Object.profileName)" + $imgJson = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Object.Id)/$imgType" if($imgJson.Value) { $obj.Object.$imgType = $imgJson @@ -1793,9 +1831,23 @@ function Start-PostExportAdministrativeTemplate { param($obj, $objectType, $path) + $fileName = (Get-GraphObjectName $obj $objectType) + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + # Collect and save all the settings of the Administrative Templates profile - $settings = Get-GPOObjectSettings $obj - $fileName = "$path\$((Remove-InvalidFileNameChars (Get-GraphObjectName $obj $objectType)))_Settings.json" + if($obj.definitionValues) + { + $settings = $obj.definitionValues + } + else + { + $settings = Get-GPOObjectSettings $obj + } + + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" ConvertTo-Json $settings -Depth 20 | Out-File -LiteralPath $fileName -Force } @@ -1858,7 +1910,7 @@ function Start-PostGetAdministrativeTemplate $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force } <# - # Leave for now. This only loads the configured defenition values and not the values specified. + # Leave for now. This only loads the configured definition values and not the values specified. # That would require enumerating each definition value which takes time. $definitionValues = (Invoke-GraphRequest "deviceManagement/groupPolicyConfigurations('$($obj.Id)')/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,groupPolicyCategoryId)" -ODataMetadata "minimal").value @@ -2025,8 +2077,21 @@ function Start-PostExportRoleDefinitions { param($obj, $objectType, $path) - $fileName = "$path\$((Remove-InvalidFileNameChars (Get-GraphObjectName $obj $objectType))).json" - $tmpObj = Get-Content -LiteralPath $fileName | ConvertFrom-Json + $fileName = (Get-GraphObjectName $obj $objectType) + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $tmpObj = $null + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" + if([IO.File]::Exists($fileName)) + { + $tmpObj = Get-Content -LiteralPath $fileName | ConvertFrom-Json + } + else + { + Write-Log "File not found: $fileName. Could not get role assignments" 3 + } if(($tmpObj.RoleAssignments | measure).Count -gt 0) { @@ -2397,10 +2462,15 @@ function Add-EMAssignmentsToExportFile { param($obj, $objectType, $path, $Url = "") - $fileName = "$path\$((Remove-InvalidFileNameChars (Get-GraphObjectName $obj $objectType))).json" + $fileName = (Get-GraphObjectName $obj $objectType) + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" if([IO.File]::Exists($fileName) -eq $false) { - Write-Log "File not found: $fileName. Could not add assignments" 3 + Write-Log "File not found: $fileName. Could not add assignments to file" 3 return } $tmpObj = Get-Content -LiteralPath $fileName | ConvertFrom-Json diff --git a/Extensions/IntuneAppManagement.psm1 b/Extensions/IntuneAppManagement.psm1 index 33e1da3..346d350 100644 --- a/Extensions/IntuneAppManagement.psm1 +++ b/Extensions/IntuneAppManagement.psm1 @@ -10,7 +10,7 @@ This module manages Application objects in Intune e.g. uploading application fil #> function Get-ModuleVersion { - '3.1.1' + '3.1.2' } ######################################################################################### @@ -42,6 +42,12 @@ function Get-MSIFileInformation $values = @{} + if(-not $MSIFile) { return } + + $fi = [IO.FileInfo]$MSIFile + + if($fi.Extension -ne ".msi") { return } + try { $wiObj = New-Object -ComObject WindowsInstaller.Installer diff --git a/Extensions/IntuneAssignments.psm1 b/Extensions/IntuneAssignments.psm1 index 6424254..b12f39f 100644 --- a/Extensions/IntuneAssignments.psm1 +++ b/Extensions/IntuneAssignments.psm1 @@ -9,7 +9,7 @@ Module for listing Intune assignments #> function Get-ModuleVersion { - '1.0.2' + '1.0.3' } function Invoke-InitializeModule @@ -231,10 +231,13 @@ function Invoke-IntueAssignmentFilterBoxChanged $txtBox.Tag = 1 $txtBox.Text = "Filter" $txtBox.Foreground="Lightgray" + } + elseif($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) + { + } else { - if($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) { return } $txtBox.FontStyle = "Normal" $txtBox.Tag = $null $txtBox.Foreground="Black" @@ -245,15 +248,15 @@ function Invoke-IntueAssignmentFilterBoxChanged $filter = { param ($item) - return ( $item.Name -match [regex]::Escape($txtBox.Text) -or $item.IncludedString -match [regex]::Escape($txtBox.Text) -or $item.ExcludedString -match [regex]::Escape($txtBox.Text) ) + return ($item.Name -match [regex]::Escape($txtBox.Text) -or $item.IncludedString -match [regex]::Escape($txtBox.Text) -or $item.ExcludedString -match [regex]::Escape($txtBox.Text) ) } } } - if($dgObject.ItemsSource -is [System.Windows.Data.ListCollectionView]) + if($dgObject.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true) { # This causes odd behaviour with focus e.g. and item has to be clicked twice to be selected $dgObject.ItemsSource.Filter = $filter - $dgObject.ItemsSource.Refresh() + #$dgObject.ItemsSource.Refresh() } } diff --git a/Extensions/IntuneTools.psm1 b/Extensions/IntuneTools.psm1 index 2081c7e..d02498e 100644 --- a/Extensions/IntuneTools.psm1 +++ b/Extensions/IntuneTools.psm1 @@ -22,7 +22,7 @@ $global:EMToolsViewObject = $null function Get-ModuleVersion { - '1.0.2' + '1.0.3' } function Invoke-InitializeModule @@ -890,10 +890,13 @@ function Invoke-ADMXFilterPolicies $txtBox.Tag = 1 $txtBox.Text = "Filter" $txtBox.Foreground="Lightgray" + } + elseif($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) + { + } else { - if($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) { return } $txtBox.FontStyle = "Normal" $txtBox.Tag = $null $txtBox.Foreground="Black" @@ -908,11 +911,11 @@ function Invoke-ADMXFilterPolicies } } - if($global:dgADMXSettings.ItemsSource -is [System.Windows.Data.ListCollectionView]) + if($global:dgADMXSettings.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true) { # This causes odd behaviour with focus e.g. and item has to be clicked twice to be selected $global:dgADMXSettings.ItemsSource.Filter = $filter - $global:dgADMXSettings.ItemsSource.Refresh() + #$global:dgADMXSettings.ItemsSource.Refresh() } } diff --git a/Extensions/MSALAuthentication.psm1 b/Extensions/MSALAuthentication.psm1 index b542466..129d7e3 100644 --- a/Extensions/MSALAuthentication.psm1 +++ b/Extensions/MSALAuthentication.psm1 @@ -10,7 +10,7 @@ This module manages Authentication for the application with MSAL. It is also res #> function Get-ModuleVersion { - '3.3.1' + '3.3.2' } $global:msalAuthenticator = $null @@ -121,6 +121,14 @@ function Invoke-InitializeModule DefaultValue = "gcc" }) "MSAL" + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Sort Account List" + Key = "SortAccountList" + Type = "Boolean" + DefaultValue = $false + Description = "Sort the list of cached accounts based on user name. Updated at restart or account change" + }) "MSAL" + Add-MSALPrereq #$script:MSALDLLMissing = $true #!!!! @@ -1389,13 +1397,21 @@ function Get-MSALProfileEllipse [System.Windows.Controls.Canvas]::SetTop($obj,($point.Y + $obj.Tag.ActualHeight)) }) - $otherLogins = $global:grdProfileInfo.FindName("grdAccountsAndTenants") + $otherLogins = $global:grdProfileInfo.FindName("grdCachedAccounts") ######################################################################################################### ### Add cached users ######################################################################################################### + if((Get-SettingValue "SortAccountList") -eq $true) + { + $accounts = $global:MSALAccounts | Sort -Property Username + } + else + { + $accounts = $global:MSALAccounts + } - foreach($account in $global:MSALAccounts) + foreach($account in $accounts) { # Skip current logged on user if($global:MSALToken.Account.Username -eq $Account.Username) { continue } @@ -1518,9 +1534,13 @@ function Get-MSALProfileEllipse Show-GraphObjects } Write-Status "" - }) - + }) + + $otherLogins = $global:grdProfileInfo.FindName("grdLoginAccount") + Add-GridObject $otherLogins $lnkButton + + $otherLogins = $global:grdProfileInfo.FindName("grdTenantAccounts") if(($script:AccessableTenants | measure).Count -gt 1) { diff --git a/Extensions/MSGraph.psm1 b/Extensions/MSGraph.psm1 index 144348e..b7425cf 100644 --- a/Extensions/MSGraph.psm1 +++ b/Extensions/MSGraph.psm1 @@ -10,7 +10,7 @@ This module manages Microsoft Grap fuctions like calling APIs, managing graph ob #> function Get-ModuleVersion { - '3.1.9' + '3.1.10' } $global:MSGraphGlobalApps = @( @@ -158,7 +158,24 @@ function Invoke-InitializeModule Type = "Boolean" DefaultValue = $false Description = "This will add object ID to the export file to support objects with the same name e.g. ObjectName_ObjectId.json" - }) "ImportExport" + }) "ImportExport" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Use Batch API (Preview)" + Key = "UseBatchAPI" + Type = "Boolean" + DefaultValue = $false + Description = "This will use batch API to call up to extport 20 objects on each API call" + }) "ImportExport" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Refresh Objects after copy" + Key = "RefreshObjectsAfterCopy" + Type = "Boolean" + DefaultValue = $true + Description = "This will refresh all objects when after a copy. If this is disabled, the list must be refreshed manually to see the new objects. Default is true" + }) "ImportExport" + } function Get-GraphAppInfo @@ -240,6 +257,12 @@ function Invoke-GraphRequest [switch] $AllPages, + [int] + $PageSize = -1, + + [switch] + $Batch, + [switch] $NoError ) @@ -313,12 +336,8 @@ function Invoke-GraphRequest $Url = $Url -replace "%OrganizationId%", $global:Organization.Id } - <# - if($AllPages) - { - # Code to test paging - Force each page to size specified in top parameter below - # Kept for reference - + if($PageSize -gt 0 -and $url.IndexOf("`$top=") -eq -1) + { if(($url.IndexOf('?')) -eq -1) { $url = "$($url.Trim())?" @@ -327,9 +346,8 @@ function Invoke-GraphRequest { $url = "$($url.Trim())&" } - $url = "$($url.Trim())`$top=20" + $url = "$($url.Trim())`$top=$($PageSize)" } - #> $ret = $null try @@ -390,13 +408,15 @@ function Get-GraphObjects $exclude, $SortProperty = "displayName", $objectType, + [string] + $select, + [switch] + $SinglePage, + [switch] + $AllPages, [switch] $SingleObject) - - $objects = @() - - if($property -isnot [Object[]]) { $property = @('displayName', 'description', 'id')} - + $params = @{} if($objectType.ODataMetadata) { @@ -417,15 +437,35 @@ function Get-GraphObjects else { $url = "$($url.Trim())&$($objectType.QUERYLIST.Trim())" # Risky...does not check that the parameter is already in use - } + } } + + if(($url.IndexOf("`$select=")) -eq -1 -and $select) + { + $url += (?: (($url.IndexOf('?')) -eq -1) "?" "&") + $url += "`$select=$select" + } - if($SingleObject -ne $true) + if($SinglePage -eq $true) + { + #Use default page size or use below for a specific page size for testing + #$params.Add("pageSize",100) + } + elseif($SingleObject -ne $true -and $SinglePage -ne $true) { $params.Add('AllPages',$true) } + if($script:nextGraphPage -and ($SinglePage -eq $true -or $AllPages -eq $true)) + { + $url = $script:nextGraphPage + } + $graphObjects = Invoke-GraphRequest -Url $url @params + if($SinglePage -eq $true -or $AllPages -eq $true) + { + $script:nextGraphPage = $graphObjects.'@odata.nextLink' + } if($SingleObject -ne $true -and $objectType.PostListCommand) { @@ -441,6 +481,56 @@ function Get-GraphObjects $retObjects = $graphObjects } + return (Add-GraphObectProperties $retObjects $objectType $property $SortProperty) + + $objects = @() + + foreach($graphObject in $retObjects) + { + $params = @{} + if($property) { $params.Add("Property", $property) } + if($exclude) { $params.Add("ExcludeProperty", $exclude) } + foreach($objTmp in ($graphObject | Select-Object @params)) + { + $objTmp | Add-Member -NotePropertyName "IsSelected" -NotePropertyValue $false + $objTmp | Add-Member -NotePropertyName "Object" -NotePropertyValue $graphObject + $objTmp | Add-Member -NotePropertyName "ObjectType" -NotePropertyValue $objectType + $objects += $objTmp + } + } + $property = "IsSelected",$property + + if($objects.Count -gt 0 -and $SortProperty -and ($objects[0] | GM -MemberType NoteProperty -Name $SortProperty)) + { + $objects = $objects | sort -Property $SortProperty + } + + $objects +} + +function Add-GraphObectProperties +{ + param($graphObjects, + $objectType, + [Array] + $property = $null, + [Array] + $exclude, + $SortProperty = "displayName") + + if($property -isnot [Object[]]) { $property = @('displayName', 'description', 'id')} + + $objects = @() + + if($graphObjects -and ($graphObjects | GM -Name Value -MemberType NoteProperty)) + { + $retObjects = $graphObjects.Value + } + else + { + $retObjects = $graphObjects + } + foreach($graphObject in $retObjects) { $params = @{} @@ -460,6 +550,7 @@ function Get-GraphObjects { $objects = $objects | sort -Property $SortProperty } + $objects } @@ -505,13 +596,15 @@ function Show-GraphObjects $global:grdTitle.Visibility = "Visible" } - $graphObjects = @(Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType) + $script:nextGraphPage = $null + + $graphObjects = @(Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -SinglePage) $dgObjects.AutoGenerateColumns = $false $dgObjects.Columns.Clear() if(($graphObjects | measure).Count -gt 0) - { + { $tmpObj = $graphObjects | Select -First 1 $prop = $tmpObj.PSObject.Properties | Where Name -eq "IsSelected" @@ -526,6 +619,7 @@ function Show-GraphObjects $item.IsSelected = $this.IsChecked } $global:dgObjects.Items.Refresh() + Invoke-ModuleFunction "Invoke-EMSelectedItemsChanged" }) } @@ -585,6 +679,7 @@ function Show-GraphObjects $dgObjects.ItemsSource = $null } + # Show/Hide buttons based on object type foreach($ctrl in $spSubMenu.Children) { @@ -604,6 +699,16 @@ function Show-GraphObjects $ctrl.Visibility = "Collapsed" } } + + Set-GraphPagesButtonStatus +} + +function Set-GraphPagesButtonStatus +{ + $global:btnLoadAllPages.Visibility = (?: ($script:nextGraphPage) "Visible" "Collapsed") + $global:btnLoadNextPage.Visibility = (?: ($script:nextGraphPage) "Visible" "Collapsed") + $global:btnLoadAllPages.Tag = $script:nextGraphPage + $global:btnLoadNextPage.Tag = $script:nextGraphPage } function Clear-GraphObjects @@ -620,7 +725,7 @@ function Clear-GraphObjects function Get-GraphObject { - param($obj, $objectType, [switch]$SkipAssignments) + param($obj, $objectType, [switch]$SkipAssignments, [switch]$GetAPI) Write-Status "Loading $((Get-GraphObjectName $obj $objectType))" @@ -696,6 +801,16 @@ function Get-GraphObject $api = ($api + ($expand -join ",")) } + if($global:Organization.Id) + { + $api = $api -replace "%OrganizationId%", $global:Organization.Id + } + + if($GetAPI -eq $true) + { + return $api + } + $objInfo = Get-GraphObjects -Url $api -property $objectType.ViewProperties -objectType $objectType -SingleObject if($objInfo -and $objectType.PostGetCommand) @@ -959,18 +1074,38 @@ function Show-GraphBulkExportForm { $folder = Get-GraphObjectFolder $item.ObjectType (Get-XamlProperty $script:exportForm "txtExportPath" "Text") (Get-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked") (Get-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked") - $objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) - foreach($obj in $objects) + Write-Status "Get a list of all $($item.ObjectType.Title) objects" -SkipLog -Force + $objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) + + if((Get-SettingValue "UseBatchAPI") -eq $true) { - $objName = Get-GraphObjectName $obj.Object $obj.ObjectType - - if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + # Use batch to get details of each object + $batchObjects = Get-GraphBatchObjects $objects $txtNameFilter + $i = 1 + $total = ($batchObjects | measure).Count + foreach($batchResult in $batchObjects) { - continue - } + $objName = Get-GraphObjectName $batchResult.Object $batchResult.ObjectType + Write-Status "Export $($item.Title): $objName ($($i)/$($total))" -Force + Export-GraphObject $batchResult.Object $batchResult.ObjectType $folder -IsFullObject + $i++ + } + } + else + { + foreach($obj in $objects) + { + # Export objects one by one + $objName = Get-GraphObjectName $obj.Object $obj.ObjectType - Write-Status "Export $($item.Title): $objName" -Force - Export-GraphObject $obj.Object $item.ObjectType $folder + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + + Write-Status "Export $($item.Title): $objName" -Force + Export-GraphObject $obj.Object $item.ObjectType $folder + } } Save-Setting "" "LastUsedFullPath" $folder } @@ -2288,13 +2423,21 @@ function Export-GraphObject { param($objToExport, $objectType, - $exportFolder) + $exportFolder, + [switch]$IsFullObject) if(-not $exportFolder) { return } Write-Status "Export $((Get-GraphObjectName $objToExport $objectType))" - $obj = Get-GraphExportObject $objToExport $objectType + if($IsFullObject -eq $true) + { + $obj = $objToExport + } + else + { + $obj = Get-GraphExportObject $objToExport $objectType + } if(-not $obj) { @@ -2315,7 +2458,7 @@ function Export-GraphObject } $fileName = Get-GraphObjectName $obj $objectType - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id -and $objectType.SkipAddIDOnExport -ne $true) { $fileName = ($fileName + "_" + $obj.Id) } @@ -2335,6 +2478,83 @@ function Export-GraphObject } } +function Get-GraphBatchObjects +{ + param($objects, $txtNameFilter) + + $curBatch = 1 + $batchArr = @() + $batchResults = @() + $batchTotal = 0 + $objectType = $null + foreach($obj in $objects) + { + $objectType = $obj.ObjectType + $objName = Get-GraphObjectName $obj.Object $obj.ObjectType + + if($objName -and $txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + $batchTotal++ + } + else + { + $ometadata = ?? $obj.ObjectType.ODataMetadata "Full" + $batchArr += [PSCustomObject]@{ + id = ($batchArr.Count + 1) + method = "GET" + url = (Get-GraphObject $obj.Object $obj.ObjectType -GetAPI) + headers = @{"Accept"="application/json;odata.metadata=$ometadata"} + } + } + + if($batchArr.Count -eq 20 -or ($batchTotal + $batchArr.Count -eq $objects.Count)) + { + $batchObj = [PSCustomObject]@{ + requests = $batchArr + } + + Write-Status "Get batch $curBatch $($obj.ObjectType.Title)" -Force + $batchTotal += $batchArr.Count + $json = $batchObj | ConvertTo-Json -Depth 10 + $tmpResults = Invoke-GraphRequest -Url "`$batch" -Content $json -HttpMethod "POST" -Batch #-Url $api -property $obj.ObjectType.ViewProperties -objectType $obj.ObjectType - + $curResp = 1 + foreach($batchResult in ($tmpResults.responses | Sort -Property Id)) + { + if($batchResult.Status -ne "200" -or -not $batchResult.body) + { + $reqObj = $batchObj.requests | where id -eq $batchResult.Id + Write-Log "Batch result $($batchResult.Status) for URL $($reqObj.URL). Skipping..." 2 + continue + } + + $batchResults += $batchResult.body + $curResp++ + } + + $curBatch++ + $batchArr = @() + } + } + + if($objectType -and $batchResults.Count -gt 0) + { + $batchResultsTmp = $batchResults + $batchResults = Add-GraphObectProperties $batchResultsTmp $objectType -property $objectType.ViewProperties + + $curObj = 1 + foreach($obj in $batchResults) + { + if($obj.Object -and $obj.ObjectType.PostGetCommand) + { + Write-Status "Get full info - $((Get-GraphObjectName $obj.Object $obj.ObjectType)) ($($curObj)/$(@($batchResults).Count))" -Force + & $obj.ObjectType.PostGetCommand $obj $obj.ObjectType + } + $curObj++ + } + } + $batchResults +} + function Get-GraphExportObject { param($obj, $objectType) @@ -2527,7 +2747,10 @@ function Copy-GraphObject { if((& $global:curObjectType.PreCopyCommand $exportObj $global:curObjectType $ret)) { - Show-GraphObjects + if((Get-SettingValue "RefreshObjectsAfterCopy") -eq $true) + { + Show-GraphObjects + } Write-Status "" return } @@ -2547,7 +2770,10 @@ function Copy-GraphObject { & $global:curObjectType.PostCopyCommand $exportObj $newObj $global:curObjectType } - Show-GraphObjects + if((Get-SettingValue "RefreshObjectsAfterCopy") -eq $true) + { + Show-GraphObjects + } } else { @@ -2674,4 +2900,75 @@ function Add-GraphBulkMenu $menuItem.AddChild($subItem) | Out-Null $mnuMain.Items.Insert(1,$menuItem) | Out-Null -} \ No newline at end of file +} + + +function Get-GraphAllEntityTypes +{ + param($entityType, $xml, $hashTable) + + if(-not $hashTable.ContainsKey($entityType)) + { + $hashTable.Add($entityType, $xml.SelectSingleNode("//*[name()='EntityType' and @Name='$entityType']")) + } + + $nodes = $xml.SelectNodes("//*[@BaseType='graph.$entityType']") + + foreach($node in $nodes) + { + if($node.Abstract -ne "true") + { + $hashTable.Add($node.Name, $node) + } + Get-GraphAllEntityTypes $node.Name $xml $hashTable + } +} + +function Get-GraphEntityTypeObject +{ + param($entityType, $xml, $skipProperties = @()) + + $props = Get-GraphEntityTypeProperties $entityType $xml + + if(-not $props) { return } + + $obj = [PSCustomObject]@{ + + } + + foreach($prop in $props) + { + if($prop.Name -in $skipProperties) { continue } + $obj | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $null + } + $obj +} + +function Get-GraphEntityTypeProperties +{ + param($entityType, $xml) + + $tmpEntity = $xml.SelectSingleNode("//*[name()='EntityType' and @Name='$entityType']") + if(-not $tmpEntity) { return } + + $entities = @() + $entities += $tmpEntity + + while($tmpEntity.BaseType) + { + $baseType = $tmpEntity.BaseType.Split('.')[-1] + $tmpEntity = $xml.SelectSingleNode("//*[name()='EntityType' and @Name='$baseType']") + if($tmpEntity) + { + $entities += $tmpEntity + } + } + $properties = @() + [array]::Reverse($entities) + foreach($enitiy in $entities) + { + $properties += $enitiy.SelectNodes("*[name()='Property']") + } + + $properties +} diff --git a/README.md b/README.md index fbfdf76..574b9e8 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,8 @@ When multi tenant settings is Enabled/Disabled, the Profile Info is not updated The *List Applications* API might not list an imported app immediately after the import. Click *Refresh* to reload the application objects. -When using the filter box to search for items, the checkbox must be clicked twice to select an item. +~~When using the filter box to search for items, the checkbox must be clicked twice to select an item.~~
+Issue fixed in 3.3.2 Logout will only clear the token from cache and not from the browser e.g. if login is triggered after a logout, the user will still be listed in the 'Select user' dialog. diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 488e52b..9608a05 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,44 @@ # Release Notes +## 3.3.2 - 2021-12-14 + +**New features** + +- Markdown support for documentation (Experimental) + This will create a MD document in the Documents folder. + **Note:** This is not working 100% at the moment. The script will create a MD document but it might be too large if all objects in the environment are documented. + + Also note that HTML tables are used so that code can be documented as code blocks. This must be supported by the MD Viewer. The *Markdown Viewer* extension in Chrome was used during testing. + + Please report any suggestions to the issue.
+ This is based on [Issue 35](https://github.com/Micke-K/IntuneManagement/issues/35) + +- Added support for batched export + This will use batch API to request full info for up to 20 objects per batch to reduce export time + This can be enabled in setting + +- Added support for scrolling cached users and guest accounts in the profile info
+ This can be enabled in settings + +- Added support for sorting cached users
+ This can be enabled in settings + +**Fixes** + +- Paged return of objects
+ Only first page of objects will be loaded by default. + Additional pages can be loaded with **Load More** or all available objects can be loaded with **Load All**.
+ This is based on [Issue 28](https://github.com/Micke-K/IntuneManagement/issues/28) + +- Fixed an issue where a checkbox had to be clicked twice to be checked when the list was filtered
+ This is based on a known issue + +- Fixed an issue where buttons were not enabled when **Select All** was checked
+ This is based on [Issue 36](https://github.com/Micke-K/IntuneManagement/issues/36) + +- Fixed an issue when adding object ID to the file name during export + The separate settings file was not exported with the ID in the name which could cause issues during import + ## 3.3.1 (Beta) - 2021-10-28 This is a **BETA** release. It contains core changes for Authentication and Settings management. Please report any issues [here](https://github.com/Micke-K/IntuneManagement/issues). diff --git a/Xaml/EndpointManagerPanel.xaml b/Xaml/EndpointManagerPanel.xaml index dc6e2c2..fcc7a0a 100644 --- a/Xaml/EndpointManagerPanel.xaml +++ b/Xaml/EndpointManagerPanel.xaml @@ -14,7 +14,13 @@ SelectionUnit="FullRow" CanUserAddRows="False" Grid.Column="1" - Grid.Row="1" /> + Grid.Row="1"> + + + + @@ -22,13 +28,17 @@ + + - - + +