Bulk Compare, Bulk Copy + bug fixes
This commit is contained in:
Mikael Karlsson
2021-07-05 18:25:33 +10:00
parent 1556c447c5
commit e66814055d
40 changed files with 1477 additions and 103 deletions

1
.gitignore vendored
View File

@@ -4,4 +4,5 @@
/CloudAPIPowerShellManagement.Log
/CloudAPIPowerShellManagement.Lo_
/Documentation/Get-LanguageStrings.ps1
/*.csv

111
Core.psm1
View File

@@ -12,7 +12,7 @@ This module handles the WPF UI
function Get-ModuleVersion
{
'3.0.1'
'3.1.2'
}
function Start-CoreApp
@@ -363,6 +363,24 @@ function Get-XamlObject
}
}
function Invoke-RegisterName
{
param($parent, $name, $registerTo)
try
{
$control = $parent.FindName($name)
if($control)
{
$registerTo.RegisterName($name, $control)
}
}
catch
{
Write-LogError "Failed to register $name" $_.Exception
}
}
#endregion
#region Dialogs
@@ -429,6 +447,65 @@ function Show-AboutDialog
Show-ModalForm "About" $script:dlgAbout
}
function Show-UpdatesDialog
{
$script:dlgUpdates = Get-XamlObject ($global:AppRootFolder + "\Xaml\UpdatesDialog.xaml")
if(-not $script:dlgUpdates) { return }
Write-Status "Getting Release Notes Information"
Add-XamlEvent $script:dlgUpdates "btnClose" "add_click" {
$script:dlgUpdates = $null
Show-ModalObject
}
#Get-Module | Where Name -eq "Core"
$fileContent = Get-Content -Raw -Path ($global:AppRootFolder + "\ReleaseNotes.md")
try
{
$tmp = $fileContent.Replace("`r`n","`n")
$mystring = ("blob $($tmp.Length)`0" + $tmp)
$mystream = [IO.MemoryStream]::new([byte[]][char[]]$mystring)
$curHash = Get-FileHash -InputStream $mystream -Algorithm SHA1
}
finally
{
if($mystream) { $mystream.Dispose() }
}
<#
$latest = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases/latest"
if($latest)
{
}
#>
$content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md"
if($content)
{
$txt = [System.Text.Encoding]::ASCII.GetString(([System.Convert]::FromBase64String($content.content)))
Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $txt
if($content.sha -ne $curHash.Hash)
{
# ReleaseNotes.md not matching
Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Visible"
Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $fileContent
Set-XamlProperty $script:dlgUpdates "txtReleaseNotesMatch" "Visibility" "Collapsed"
}
else
{
Set-XamlProperty $script:dlgUpdates "txtReleaseNotesNoMatch" "Visibility" "Collapsed"
Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Collapsed"
}
}
Write-Status ""
Show-ModalForm "Release Notes" $script:dlgUpdates -HideButtons
}
function Show-InputDialog
{
param(
@@ -664,6 +741,9 @@ function Get-Folder
function Remove-Property
{
param($obj, $prop)
if(-not $prop) { return }
if(($obj | GM -MemberType NoteProperty -Name $prop))
{
Write-LogDebug "Remove property $prop"
@@ -692,6 +772,28 @@ function Get-GridCheckboxColumn
$column
}
function Expand-FileName
{
param($fileName)
[Environment]::SetEnvironmentVariable("Date",(Get-Date).ToString("yyyy-MM-dd"),[System.EnvironmentVariableTarget]::Process)
[Environment]::SetEnvironmentVariable("DateTime",(Get-Date).ToString("yyyyMMdd-HHmm"),[System.EnvironmentVariableTarget]::Process)
[Environment]::SetEnvironmentVariable("Organization",$global:Organization.displayName,[System.EnvironmentVariableTarget]::Process)
$fileName = [Environment]::ExpandEnvironmentVariables($fileName)
foreach($tmpFolder in ([System.Enum]::GetNames([System.Environment+SpecialFolder])))
{
$fileName = $fileName -replace "%$($tmpFolder)%",([Environment]::GetFolderPath($tmpFolder))
}
[Environment]::SetEnvironmentVariable("Date",$null,[System.EnvironmentVariableTarget]::Process)
[Environment]::SetEnvironmentVariable("DateTime",$null,[System.EnvironmentVariableTarget]::Process)
[Environment]::SetEnvironmentVariable("Organization",$null,[System.EnvironmentVariableTarget]::Process)
$fileName
}
#endregion
#region Reg functions
@@ -708,7 +810,7 @@ function Save-Setting
$regPath = Get-RegPath $SubPath
if((Test-Path $regPath) -eq $false)
{
New-Item (Get-RegPath $SubPath) -ErrorAction SilentlyContinue
New-Item (Get-RegPath $SubPath) -Force -ErrorAction SilentlyContinue | Out-Null
}
New-ItemProperty -Path $regPath -Name $Key -Value $Value -Type $Type -Force | Out-Null
}
@@ -1201,6 +1303,8 @@ function Show-View
& $global:currentViewObject.ViewInfo.Deactivating
}
$global:currentViewObject = $viewObject
$viewItems = ?: ($viewObject.ViewInfo.Sort -ne $false) ($viewObject.ViewItems | Sort-Object -Property Title) ($viewObject.ViewItems)
$lblMenuTitle.Content = $viewObject.ViewInfo.Title
@@ -1227,8 +1331,6 @@ function Show-View
$grdViewPanel.Children.Add($viewObject.ViewInfo.ViewPanel) | Out-Null
}
$global:currentViewObject = $viewObject
Set-MainTitle
Show-AuthenticationInfo
@@ -1286,6 +1388,7 @@ function Get-MainWindow
# ToDo: Convert to a list for data binding
Add-XamlEvent $window "mnuSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm })
Add-XamlEvent $window "mnuUpdates" "Add_Click" -scriptBlock ([scriptblock]{ Show-UpdatesDialog })
Add-XamlEvent $window "mnuAbout" "Add_Click" -scriptBlock ([scriptblock]{ Show-AboutDialog })
Add-XamlEvent $window "mnuExit" "Add_Click" -scriptBlock ([scriptblock]{
if([System.Windows.MessageBox]::Show("Are you sure you want to exit?", "Exit?", "YesNo", "Question") -eq "Yes")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -11,13 +11,102 @@ Objects can be compared based on Properties or Documentatation info.
function Get-ModuleVersion
{
'1.0.1'
'1.0.2'
}
function Invoke-InitializeModule
{
# Make sure we add the default Output types
Add-OutputType
$global:comparisonTypes = $null
$global:compareProviders = $null
$script:CompareProviderOptionsCache = $null
$script:defaultCompareProps = [Collections.Generic.List[String]]@('ObjectName', 'Id', 'Type', 'Category', 'SubCategory', 'Property', 'Value1', 'Value2', 'Match')
# Make sure we add the default providers
Add-CompareProvider
Add-ComparisonTypes
$script:saveType = @(
[PSCustomObject]@{
Name="One file for each object type"
Value="objectType"
},
[PSCustomObject]@{
Name="One file for all objects"
Value="all"
}
)
}
function Add-CompareProvider
{
param($compareProvider)
if(-not $global:compareProviders)
{
$global:compareProviders = @()
}
if($global:compareProviders.Count -eq 0)
{
$global:compareProviders += [PSCustomObject]@{
Name = "Exported File"
Value = "export"
ObjectCompare = { Compare-ObjectsBasedonProperty @args }
BulkCompare = { Start-BulkCompareExportObjects @args }
ProviderOptions = "CompareExportOptions"
Activate = { Invoke-ActivateCompareExportObjects @args }
}
$global:compareProviders += [PSCustomObject]@{
Name = "Named Objects"
Value = "name"
BulkCompare = { Start-BulkCompareNamedObjects @args }
ProviderOptions = "CompareNamedOptions"
Activate = { Invoke-ActivateCompareNamesObjects @args }
RemoveProperties = @("Id")
}
$global:compareProviders += [PSCustomObject]@{
Name = "Existing objects"
Value = "existing"
Compare = { Compare-ObjectsBasedonDocumentation @args }
}
}
if(!$compareProvider) { return }
$global:compareProviders += $compareProvider
}
function Add-ComparisonTypes
{
param($comparisonType)
if(-not $global:comparisonTypes)
{
$global:comparisonTypes = @()
}
if($global:comparisonTypes.Count -eq 0)
{
$global:comparisonTypes += [PSCustomObject]@{
Name = "Property"
Value = "property"
Compare = { Compare-ObjectsBasedonProperty @args }
RemoveProperties = @('Category','SubCategory')
}
$global:comparisonTypes += [PSCustomObject]@{
Name = "Documentation"
Value = "doc"
Compare = { Compare-ObjectsBasedonDocumentation @args }
}
}
if(!$comparisonType) { return }
$global:comparisonTypes += $comparisonType
}
function Invoke-ShowMainWindow
@@ -42,6 +131,510 @@ function Invoke-ShowMainWindow
$global:spSubMenu.Children.Insert(0, $button)
}
function Invoke-ViewActivated
{
if($global:currentViewObject.ViewInfo.ID -ne "IntuneGraphAPI") { return }
$tmp = $mnuMain.Items | Where Name -eq "EMBulk"
if($tmp)
{
$tmp.AddChild(([System.Windows.Controls.Separator]::new())) | Out-Null
$subItem = [System.Windows.Controls.MenuItem]::new()
$subItem.Header = "_Compare"
$subItem.Add_Click({Show-CompareBulkForm})
$tmp.AddChild($subItem)
}
}
function Show-CompareBulkForm
{
$script:form = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkCompare.xaml") -AddVariables
if(-not $script:form) { return }
$global:cbCompareProvider.ItemsSource = @(($global:compareProviders | Where BulkCompare -ne $null))
$global:cbCompareProvider.SelectedValue = (Get-Setting "Compare" "Provider" "export")
$global:cbCompareSave.ItemsSource = @($script:saveType)
$global:cbCompareSave.SelectedValue = (Get-Setting "Compare" "SaveType" "objectType")
$global:cbCompareType.ItemsSource = $global:comparisonTypes | Where ShowOnBulk -ne $false
$global:cbCompareType.SelectedValue = (Get-Setting "Compare" "Type" "property")
$script:compareObjects = @()
foreach($objType in $global:lstMenuItems.ItemsSource)
{
if(-not $objType.Title) { continue }
$script:compareObjects += New-Object PSObject -Property @{
Title = $objType.Title
Selected = $true
ObjectType = $objType
}
}
$column = Get-GridCheckboxColumn "Selected"
$global:dgObjectsToCompare.Columns.Add($column)
$column.Header.IsChecked = $true # All items are checked by default
$column.Header.add_Click({
foreach($item in $global:dgObjectsToCompare.ItemsSource)
{
$item.Selected = $this.IsChecked
}
$global:dgObjectsToCompare.Items.Refresh()
}
)
# Add Object type column
$binding = [System.Windows.Data.Binding]::new("Title")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object type"
$column.IsReadOnly = $true
$column.Binding = $binding
$global:dgObjectsToCompare.Columns.Add($column)
$global:dgObjectsToCompare.ItemsSource = $script:compareObjects
Add-XamlEvent $script:form "btnClose" "add_click" {
$script:form = $null
Show-ModalObject
}
Add-XamlEvent $script:form "btnStartCompare" "add_click" {
Write-Status "Compare objects"
Save-Setting "Compare" "Provider" $global:cbCompareProvider.SelectedValue
Save-Setting "Compare" "Type" $global:cbCompareType.SelectedValue
if($global:cbCompareProvider.SelectedItem.BulkCompare)
{
& $global:cbCompareProvider.SelectedItem.BulkCompare
}
Write-Status ""
}
$global:cbCompareProvider.Add_SelectionChanged({
Set-CompareProviderOptions $this
})
Set-CompareProviderOptions $global:cbCompareProvider
Show-ModalForm "Bulk Compare Objects" $script:form -HideButtons
}
function Set-CompareProviderOptions
{
param($control)
$providerOptions = $null
$firstTime = $false
if($control.SelectedItem.ProviderOptions)
{
if($script:CompareProviderOptionsCache -isnot [Hashtable]) { $script:CompareProviderOptionsCache = @{} }
if($script:CompareProviderOptionsCache.Keys -contains $control.SelectedValue)
{
$providerOptions = $script:CompareProviderOptionsCache[$control.SelectedValue]
}
else
{
$providerOptions = Get-XamlObject ($global:AppRootFolder + "\Xaml\$($control.SelectedItem.ProviderOptions).xaml") -AddVariables
if($providerOptions)
{
$firstTime = $true
$script:CompareProviderOptionsCache.Add($control.SelectedValue, $providerOptions)
}
else
{
Write-Log "Failed to create options for $($control.SelectedItem.Name)" 3
}
}
$global:ccContentProviderOptions.Content = $providerOptions
}
else
{
$global:ccContentProviderOptions.Content = $null
}
$global:ccContentProviderOptions.Visibility = (?: ($global:ccContentProviderOptions.Content -eq $null) "Collapsed" "Visible")
if($control.SelectedItem.Activate)
{
if($firstTime)
{
Write-Log "Initialize $($global:cbCompareProvider.SelectedItem.Name) provider options"
}
& $control.SelectedItem.Activate $providerOptions $firstTime
}
}
function Invoke-ActivateCompareExportObjects
{
param($providerOptions, $firstTime)
if($firstTime)
{
$path = Get-Setting "" "LastUsedFullPath"
if($path)
{
$path = [IO.Directory]::GetParent($path).FullName
}
Set-XamlProperty $providerOptions "txtExportPath" "Text" (?? $path (Get-SettingValue "RootFolder"))
Add-XamlEvent $providerOptions "browseExportPath" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $this.Parent "txtExportPath" "Text") "Select root folder for compare"
if($folder)
{
Set-XamlProperty $this.Parent "txtExportPath" "Text" $folder
}
})
}
}
function Invoke-ActivateCompareNamesObjects
{
param($providerOptions, $firstTime)
if($providerOptions -and $firstTime)
{
Set-XamlProperty $providerOptions "txtCompareSource" "Text" (Get-Setting "Compare" "CompareSource" "")
Set-XamlProperty $providerOptions "txtCompareWith" "Text" (Get-Setting "Compare" "CompareWith" "")
Set-XamlProperty $providerOptions "txtSavePath" "Text" (Get-Setting "Compare" "SavePath" "")
Add-XamlEvent $providerOptions "browseSavePath" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $this.Parent "txtSavePath" "Text") "Select folder"
if($folder)
{
Set-XamlProperty $this.Parent "txtSavePath" "Text" $folder
}
})
}
}
function Start-BulkCompareNamedObjects
{
Write-Log "****************************************************************"
Write-Log "Start bulk Named Objects compare"
Write-Log "****************************************************************"
$compareObjectsResult = @()
$compareSource = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareSource" "Text")
$compareWith = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareWith" "Text")
if(-not $compareSource -or -not $compareWith)
{
[System.Windows.MessageBox]::Show("Both source and compare name patterns must be specified", "Error", "OK", "Error")
return
}
Save-Setting "Compare" "CompareSource" $compareSource
Save-Setting "Compare" "CompareWith" $compareWith
Invoke-BulkCompareNamedObjects $compareSource $compareWith
Write-Log "****************************************************************"
Write-Log "Bulk compare Named Objects finished"
Write-Log "****************************************************************"
Write-Status ""
}
function Invoke-BulkCompareNamedObjects
{
param($sourcePattern, $comparePattern)
$outputType = $global:cbCompareSave.SelectedValue
Save-Setting "Compare" "SaveType" $outputType
$compResultValues = @()
$compareObjectsResult = @()
$outputFolder = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtSavePath" "Text")
if(-not $outputFolder)
{
$outputFolder = Expand-FileName "%MyDocuments%"
}
$compareProps = $script:defaultCompareProps
foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties)
{
$compareProps.Remove($removeProp) | Out-Null
}
foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties)
{
$compareProps.Remove($removeProp) | Out-Null
}
foreach($item in ($global:dgObjectsToCompare.ItemsSource | where Selected -eq $true))
{
Write-Status "Compare $($item.ObjectType.Title) objects" -Force -SkipLog
Write-Log "----------------------------------------------------------------"
Write-Log "Compare $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------"
$url = $item.ObjectType.API
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
$nameProp = ?? $item.ObjectType.NameProperty "displayName"
foreach($graphObj in ($graphObjects | Where { $_.Object."$($nameProp)" -imatch [regex]::Escape($sourcePattern) }))
{
$sourceName = $graphObj.Object."$($nameProp)"
$compareName = $sourceName -ireplace [regex]::Escape($sourcePattern),$comparePattern
$compareObj = $graphObjects | Where { $_.Object."$($nameProp)" -eq $compareName -and $_.Object.'@OData.Type' -eq $graphObj.Object.'@OData.Type' }
if(($compareObj | measure).Count -gt 1)
{
Write-Log "Multiple objects found with name $compareName. Compare will not be performed" 2
continue
}
elseif($compareObj)
{
$sourceObj = Get-GraphObject $graphObj.Object $graphObj.ObjectType
$compareObj = Get-GraphObject $compareObj.Object $compareObj.ObjectType
$compareProperties = Compare-Objects $sourceObj.Object $compareObj.Object $sourceObj.ObjectType
}
else
{
# Add objects that are exported but deleted
Write-Log "Object '$((Get-GraphObjectName $graphObj.Object $graphObj.ObjectType))' with id $($graphObj.Object.Id) has no matching object with the compate pattern" 2
$compareProperties = @([PSCustomObject]@{
Object1Value = (Get-GraphObjectName $graphObj.Object $graphObj.ObjectType)
Object2Value = $null
Match = $false
})
}
$compareObjectsResult += [PSCustomObject]@{
Object1 = $sourceObj.Object
Object2 = $compareObj.Object
ObjectType = $item.ObjectType
Id = $sourceObj.Object.Id
Result = $compareProperties
}
}
if($outputType -eq "objectType")
{
$compResultValues = @()
}
foreach($compObj in @($compareObjectsResult | Where { $_.ObjectType.Id -eq $item.ObjectType.Id }))
{
$objName = Get-GraphObjectName (?? $compObj.Object1 $compObj.Object2) $item.ObjectType
foreach($compValue in $compObj.Result)
{
$compResultValues += [PSCustomObject]@{
ObjectName = $objName
Id = $compObj.Id
Type = $compObj.ObjectType.Title
ODataType = $compObj.Object1.'@OData.Type'
Property = $compValue.PropertyName
Value1 = $compValue.Object1Value
Value2 = $compValue.Object2Value
Category = $compValue.Category
SubCategory = $compValue.SubCategory
Match = $compValue.Match
}
}
}
if($outputType -eq "objectType")
{
$fileName = Remove-InvalidFileNameChars (Expand-FileName "Compare-$($graphObj.ObjectType.Id)-$sourcePattern-$comparePattern-%DateTime%.csv")
Save-BulkCompareResults $compResultValues (Join-Path $outputFolder $fileName) $compareProps
}
}
#$fileName = Expand-FileName $fileName
if($compareObjectsResult.Count -eq 0)
{
[System.Windows.MessageBox]::Show("No objects were comparced. Verify name patterns", "Error", "OK", "Error")
}
elseif($outputType -eq "all")
{
$fileName = Remove-InvalidFileNameChars (Expand-FileName "Compare-$sourcePattern-$comparePattern-%DateTime%.csv")
Save-BulkCompareResults $compResultValues (Join-Path $outputFolder $fileName) $compareProps
}
}
function Start-BulkCompareExportObjects
{
Write-Log "****************************************************************"
Write-Log "Start bulk Exported Objects compare"
Write-Log "****************************************************************"
$compareObjectsResult = @()
$rootFolder = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPath" "Text")
$compareProps = $script:defaultCompareProps
foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties)
{
$compareProps.Remove($removeProp) | Out-Null
}
foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties)
{
$compareProps.Remove($removeProp) | Out-Null
}
if(-not $rootFolder)
{
[System.Windows.MessageBox]::Show("Root folder must be specified", "Error", "OK", "Error")
return
}
if([IO.Directory]::Exists($rootFolder) -eq $false)
{
[System.Windows.MessageBox]::Show("Root folder $rootFolder does not exist", "Error", "OK", "Error")
return
}
$outputType = $global:cbCompareSave.SelectedValue
Save-Setting "Compare" "SaveType" $outputType
$compResultValues = @()
foreach($item in ($global:dgObjectsToCompare.ItemsSource | where Selected -eq $true))
{
Write-Status "Compare $($item.ObjectType.Title) objects" -Force -SkipLog
Write-Log "----------------------------------------------------------------"
Write-Log "Compare $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------"
$folder = Join-Path $rootFolder $item.ObjectType.Id
if([IO.Directory]::Exists($folder))
{
Save-Setting "" "LastUsedFullPath" $folder
$url = $item.ObjectType.API
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
foreach ($fileObj in @(Get-GraphFileObjects $folder -ObjectType $item.ObjectType))
{
if(-not $fileObj.Object.Id)
{
Write-Log "Object from file '$($fileObj.FullName)' has no Id property. Compare not supported" 2
continue
}
$curObject = $graphObjects | Where { $_.Object.Id -eq $fileObj.Object.Id }
if(-not $curObject)
{
# Add objects that are exported but deleted
Write-Log "Object '$((Get-GraphObjectName $fileObj.Object $fileObj.ObjectType))' with id $($fileObj.Object.Id) not found in Intune. Deleted?" 2
$compareProperties = @([PSCustomObject]@{
Object1Value = $null
Object2Value = (Get-GraphObjectName $fileObj.Object $item.ObjectType)
Match = $false
})
}
else
{
$sourceObj = Get-GraphObject $curObject.Object $curObject.ObjectType
$compareProperties = Compare-Objects $sourceObj.Object $fileObj.Object $item.ObjectType
}
$compareObjectsResult += [PSCustomObject]@{
Object1 = $curObject.Object
Object2 = $fileObj.Object
ObjectType = $item.ObjectType
Id = $fileObj.Object.Id
Result = $compareProperties
}
}
foreach($graphObj in $graphObjects)
{
# Add objects that are not exported
if(($compareObjectsResult | Where { $_.Id -eq $graphObj.Id})) { continue }
$compareObjectsResult += [PSCustomObject]@{
Object1 = $curObject.Object
Object2 = $null
ObjectType = $item.ObjectType
Id = $graphObj.Id
Result = @([PSCustomObject]@{
Object1Value = (Get-GraphObjectName $graphObj.Object $item.ObjectType)
Object2Value = $null
Match = $false
})
}
}
if($outputType -eq "objectType")
{
$compResultValues = @()
}
foreach($compObj in @($compareObjectsResult | Where { $_.ObjectType.Id -eq $item.ObjectType.Id }))
{
$objName = Get-GraphObjectName (?? $compObj.Object1 $compObj.Object2) $item.ObjectType
foreach($compValue in $compObj.Result)
{
$compResultValues += [PSCustomObject]@{
ObjectName = $objName
Id = $compObj.Id
Type = $compObj.ObjectType.Title
ODataType = $compObj.Object1.'@OData.Type'
Property = $compValue.PropertyName
Value1 = $compValue.Object1Value
Value2 = $compValue.Object2Value
Category = $compValue.Category
SubCategory = $compValue.SubCategory
Match = $compValue.Match
}
}
}
if($outputType -eq "objectType")
{
Save-BulkCompareResults $compResultValues (Join-Path $rootFolder "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps
}
}
else
{
Write-Log "Folder $folder not found. Skipping import" 2
}
}
if($outputType -eq "all" -and $compResultValues.Count -gt 0)
{
Save-BulkCompareResults $compResultValues (Join-Path $folder "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps
}
Write-Log "****************************************************************"
Write-Log "Bulk compare Exported Objects finished"
Write-Log "****************************************************************"
Write-Status ""
if($compareObjectsResult.Count -eq 0)
{
[System.Windows.MessageBox]::Show("No objects were comparced. Verify folder and exported files", "Error", "OK", "Error")
}
}
function Save-BulkCompareResults
{
param($compResultValues, $file, $props)
if($compResultValues.Count -gt 0)
{
Write-Log "Save bulk comare results to $file"
$compResultValues | Select -Property $props | ConvertTo-Csv -NoTypeInformation | Out-File $file -Force -Encoding UTF8
}
}
function Show-CompareForm
{
param($objInfo)
@@ -49,12 +642,15 @@ function Show-CompareForm
$script:cmpForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\CompareForm.xaml") -AddVariables
if(-not $script:cmpForm) { return }
$script:cmpForm.Tag = $objInfo
$script:copareSource = $objInfo
$global:cbCompareType.ItemsSource = ("[ { Name: `"Property`",Value: `"property`" }, { Name: `"Documentation`",Value: `"doc`" }]" | ConvertFrom-Json)
$global:cbCompareType.ItemsSource = $global:comparisonTypes | Where ShowOnObject -ne $false
$global:cbCompareType.SelectedValue = (Get-Setting "Compare" "Type" "property")
$global:txtIntuneObject.Text = (Get-GraphObjectName $objInfo.Object $objInfo.ObjectType)
$global:txtIntuneObject.Tag = $objInfo
Add-XamlEvent $script:cmpForm "btnClose" "add_click" {
$script:cmpForm = $null
@@ -65,7 +661,7 @@ function Show-CompareForm
Write-Status "Compare objects"
Save-Setting "Compare" "Type" $global:cbCompareType.SelectedValue
$script:currentObjName = ""
Invoke-CompareObjects
Start-CompareExportObject
Write-Status ""
}
@@ -80,22 +676,42 @@ function Show-CompareForm
$sf.Filter = "CSV (*.csv)|*.csv|All files (*.*)| *.*"
if($sf.ShowDialog() -eq "OK")
{
$csvInfo = $global:dgCompareInfo.ItemsSource | Select PropertyName,Object1Value,Object2Value,Category,SubCategory,Match | ConvertTo-Csv -NoTypeInformation
$csvInfo = Get-CompareCsvInfo $global:dgCompareInfo.ItemsSource $script:cmpForm.Tag
$csvInfo | Out-File $sf.FileName -Force -Encoding UTF8
}
}
Add-XamlEvent $script:cmpForm "btnCompareCopy" "add_click" {
$global:dgCompareInfo.ItemsSource | Select PropertyName,Object1Value,Object2Value,Category,SubCategory,Match | ConvertTo-Csv -NoTypeInformation | Set-Clipboard
(Get-CompareCsvInfo $global:dgCompareInfo.ItemsSource $script:cmpForm.Tag) | Set-Clipboard
}
Add-XamlEvent $script:cmpForm "browseCompareObject" "add_click" {
$path = Get-Setting "" "LastUsedFullPath"
if($path)
{
$path = [IO.Directory]::GetParent($path).FullName
if($global:txtIntuneObject.Tag.ObjectType)
{
$objectTypePath = [IO.Path]::Combine($path, $global:txtIntuneObject.Tag.ObjectType.Id)
if([IO.Direcotry]::Exists($objectTypePath))
{
$path = $objectTypePath
}
}
}
$path = (?: ($global:lastCompareFile -eq $null) $path ([IO.FileInfo]$global:lastCompareFile).DirectoryName)
$of = [System.Windows.Forms.OpenFileDialog]::new()
$of.Multiselect = $false
$of.Filter = "Json files (*.json)|*.json"
$of.InitialDirectory = (?: ($global:lastCompareFile -eq $null) (Get-Setting "" "LastUsedRoot") ([IO.FileInfo]$global:lastCompareFile).DirectoryName)
if($path)
{
$of.InitialDirectory = $path
}
if($of.ShowDialog())
{
Set-XamlProperty $script:cmpForm "txtCompareFile" "Text" $of.FileName
@@ -110,18 +726,55 @@ function Show-CompareForm
Show-ModalForm "Compare Intune Objects" $script:cmpForm -HideButtons
}
function Invoke-CompareObjects
function Get-CompareCsvInfo
{
param($comareInfo, $objInfo)
$compResultValues = @()
$objName = Get-GraphObjectName $objInfo.Object $objInfo.ObjectType
foreach($compValue in $comareInfo)
{
$compResultValues += [PSCustomObject]@{
ObjectName = $objName
Id = $objInfo.Object.Id
Type = $objInfo.ObjectType.Title
ODataType = $objInfo.Object.'@OData.Type'
Property = $compValue.PropertyName
Value1 = $compValue.Object1Value
Value2 = $compValue.Object2Value
Category = $compValue.Category
SubCategory = $compValue.SubCategory
Match = $compValue.Match
}
}
$compareProps = $script:defaultCompareProps
# !!! Not supported yet
#foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties)
#{
# $compareProps.Remove($removeProp) | Out-Null
#}
foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties)
{
$compareProps.Remove($removeProp) | Out-Null
}
$compResultValues | Select -Property $compareProps | ConvertTo-Csv -NoTypeInformation
}
function Start-CompareExportObject
{
if(-not $script:copareSource) { return }
if(-not $global:txtCompareFile.Text)
{
[System.Windows.MessageBox]::Show("No file selected", "Comapre", "OK", "Error")
[System.Windows.MessageBox]::Show("No file selected", "Compare", "OK", "Error")
return
}
elseif([IO.File]::Exists($global:txtCompareFile.Text) -eq $false)
{
[System.Windows.MessageBox]::Show("File '$($global:txtCompareFile.Text)' not found", "Comapre", "OK", "Error")
[System.Windows.MessageBox]::Show("File '$($global:txtCompareFile.Text)' not found", "Compare", "OK", "Error")
return
}
@@ -138,7 +791,7 @@ function Invoke-CompareObjects
}
catch
{
[System.Windows.MessageBox]::Show("Failed to convert json file '$($global:txtCompareFile.Text)'", "Comapre", "OK", "Error")
[System.Windows.MessageBox]::Show("Failed to convert json file '$($global:txtCompareFile.Text)'", "Compare", "OK", "Error")
return
}
@@ -148,24 +801,45 @@ function Invoke-CompareObjects
if($obj.Object."@OData.Type" -ne $compareObj."@OData.Type")
{
if(([System.Windows.MessageBox]::Show("The object types does not match.`n`nDo you to compare the objects?", "Comapre", "YesNo", "Warning")) -eq "No")
if(([System.Windows.MessageBox]::Show("The object types does not match.`n`nDo you to compare the objects?", "Compare", "YesNo", "Warning")) -eq "No")
{
return
}
}
}
$compareResult = Compare-Objects $obj.Object $compareObj $obj.ObjectType
$global:dgCompareInfo.ItemsSource = $compareResult
}
function Compare-Objects
{
param($obj1, $obj2, $objectType)
$script:compareProperties = @()
if($global:cbCompareType.SelectedValue -eq "property")
if($global:cbCompareType.SelectedItem.Compare)
{
Compare-ObjectsBasedonProperty $obj.Object $compareObj $obj.ObjectType
$compareResult = & $global:cbCompareType.SelectedItem.Compare $obj1 $obj2 $objectType
}
else
{
Write-Log "Selected comparison type ($($global:cbCompareType.SelectedItem.Name)) does not have a Compare property specified" 3
}
<#
elseif($global:cbCompareType.SelectedValue -eq "property")
{
$compareResult = Compare-ObjectsBasedonProperty $obj1 $obj2 $objectType
}
elseif($global:cbCompareType.SelectedValue -eq "doc")
{
Compare-ObjectsBasedonDocumentation $obj $compareObj
$compareResult = Compare-ObjectsBasedonDocumentation $obj1 $obj2 $objectType
}
$global:dgCompareInfo.ItemsSource = $script:compareProperties
#>
$compareResult
}
function Set-ColumnVisibility
{
param($showCategory = $false, $showSubCategory = $false)
@@ -208,7 +882,7 @@ function Compare-ObjectsBasedonProperty
{
param($obj1, $obj2, $objectType)
Write-Status "Compare properties"
Write-Status "Compare objects based on property values"
Set-ColumnVisibility $false
@@ -262,14 +936,16 @@ function Compare-ObjectsBasedonProperty
$val1 = ($obj1.$propName | ConvertTo-Json -Depth 10)
$val2 = ($obj2.$propName | ConvertTo-Json -Depth 10)
Add-CompareProperty $propName $val1 $val2
}
}
$script:compareProperties
}
function Get-CompareCustomColumnsDoc
{
param($objInfo)
param($obj)
if($objInfo.Object.'@OData.Type' -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration")
if($obj.'@OData.Type' -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration")
{
Set-ColumnVisibility $true $true
}
@@ -281,23 +957,34 @@ function Get-CompareCustomColumnsDoc
function Compare-ObjectsBasedonDocumentation
{
param($obj1, $obj2)
param($obj1, $obj2, $objectType)
Get-CompareCustomColumnsDoc $obj
Write-Status "Compare objects based on documentation values"
Get-CompareCustomColumnsDoc $obj1
# ToDo: set this based on configuration value
$script:assignmentOutput = "simpleFullCompare"
$docObj1 = Invoke-ObjectDocumentation $obj1
$docObj1 = Invoke-ObjectDocumentation ([PSCustomObject]@{
Object = $obj1
ObjectType = $objectType
})
$obj2 | Add-Member Noteproperty -Name "@CompareObject" -Value $true -Force
$obj2 | Add-Member Noteproperty -Name "@ObjectFromFile" -Value $true -Force
$docObj2 = Invoke-ObjectDocumentation ([PSCustomObject]@{
Object = $obj2
ObjectType = $obj1.ObjectType
ObjectType = $objectType
})
$settingsValue = ?? $obj1.ObjectType.CompareValue "Value"
$settingsValue = ?? $objectType.CompareValue "Value"
if($docObj1.BasicInfo -and -not ($docObj1.BasicInfo | where Value -eq $obj1.Id))
{
# Make sure the Id property is included
Add-CompareProperty "Id" $obj1.Id $obj2.Id $docObj1.BasicInfo[0].Category
}
foreach ($prop in $docObj1.BasicInfo)
{
@@ -412,6 +1099,28 @@ function Compare-ObjectsBasedonDocumentation
Add-CompareProperty $applicabilityRule.Property $val1 $val2 $applicabilityRule.Category
}
$complianceActionsAdded = @()
foreach($complianceAction in $docObj1.ComplianceActions)
{
$complianceAction2 = $docObj2.ComplianceActions | Where { $_.IdStr -eq $complianceAction.IdStr }
$complianceActionsAdded += $complianceAction.IdStr
$val1 = ($complianceAction.Action + [environment]::NewLine + $complianceAction.Schedule + [environment]::NewLine + $complianceAction.MessageTemplateId + [environment]::NewLine + $complianceAction.EmailCCIds)
$val2 = ($complianceAction2.Action + [environment]::NewLine + $complianceAction2.Schedule + [environment]::NewLine + $complianceAction2.MessageTemplateId + [environment]::NewLine + $complianceAction2.EmailCCIds)
Add-CompareProperty $complianceAction.Category $val1 $val2
}
foreach($complianceAction in $docObj2.ComplianceActions)
{
if(($complianceAction.IdStr) -in $complianceActionsAdded) { continue }
$complianceAction2 = $docObj1.ComplianceActions | Where { $_.IdStr -eq $complianceAction.IdStr }
$complianceActionsAdded += $complianceAction.IdStr
$val2 = ($complianceAction.Action + [environment]::NewLine + $complianceAction.Schedule + [environment]::NewLine + $complianceAction.MessageTemplateId + [environment]::NewLine + $complianceAction.EmailCCIds)
$val1 = ($complianceAction2.Action + [environment]::NewLine + $complianceAction2.Schedule + [environment]::NewLine + $complianceAction2.MessageTemplateId + [environment]::NewLine + $complianceAction2.EmailCCIds)
Add-CompareProperty $complianceAction.Category $val1 $val2
}
$script:assignmentStr = Get-LanguageString "TableHeaders.assignment"
$script:groupsAdded = @()
@@ -449,6 +1158,8 @@ function Compare-ObjectsBasedonDocumentation
{
Add-AssignmentInfo $docObj2 $docObj1 $assignment -ReversedValue
}
$script:compareProperties
}
function Add-AssignmentInfo

173
Extensions/Copy.psm1 Normal file
View File

@@ -0,0 +1,173 @@
function Get-ModuleVersion
{
'1.0.0'
}
function Invoke-InitializeModule
{
}
function Invoke-ViewActivated
{
if($global:currentViewObject.ViewInfo.ID -ne "IntuneGraphAPI") { return }
$tmp = $mnuMain.Items | Where Name -eq "EMBulk"
if($tmp)
{
$tmp.AddChild(([System.Windows.Controls.Separator]::new())) | Out-Null
$subItem = [System.Windows.Controls.MenuItem]::new()
$subItem.Header = "Cop_y"
$subItem.Add_Click({Show-CopyBulkForm})
$tmp.AddChild($subItem)
}
}
function Show-CopyBulkForm
{
$script:form = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkCopy.xaml") -AddVariables
if(-not $script:form) { return }
$global:txtCopyFromPattern.Text = Get-Setting "Copy" "CopyFromPattern"
$global:txtCopyToPattern.Text = Get-Setting "Copy" "CopyToPattern"
$script:copyObjects = @()
foreach($objType in $global:lstMenuItems.ItemsSource)
{
if(-not $objType.Title) { continue }
$script:copyObjects += New-Object PSObject -Property @{
Title = $objType.Title
Selected = $true
ObjectType = $objType
}
}
$column = Get-GridCheckboxColumn "Selected"
$global:dgObjectsToCopy.Columns.Add($column)
$column.Header.IsChecked = $true # All items are checked by default
$column.Header.add_Click({
foreach($item in $global:dgObjectsToCopy.ItemsSource)
{
$item.Selected = $this.IsChecked
}
$global:dgObjectsToCopy.Items.Refresh()
}
)
# Add Object type column
$binding = [System.Windows.Data.Binding]::new("Title")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "Object type"
$column.IsReadOnly = $true
$column.Binding = $binding
$global:dgObjectsToCopy.Columns.Add($column)
$global:dgObjectsToCopy.ItemsSource = $script:copyObjects
Add-XamlEvent $script:form "btnClose" "add_click" {
$script:form = $null
Show-ModalObject
}
Add-XamlEvent $script:form "btnStartCopy" "add_click" {
Write-Status "Copy objects"
Start-BulkCopyObjects
Write-Status ""
}
Show-ModalForm "Bulk Copy Objects" $script:form -HideButtons
}
function Start-BulkCopyObjects
{
Write-Log "****************************************************************"
Write-Log "Start bulk copy"
Write-Log "****************************************************************"
$copyFrom = $global:txtCopyFromPattern.Text
$copyTo = $global:txtCopyToPattern.Text
if(-not $copyFrom -or -not $copyTo)
{
[System.Windows.MessageBox]::Show("Both name patterns must be specified", "Error", "OK", "Error")
return
}
Save-Setting "Copy" "CopyFromPattern" $global:txtCopyFromPattern.Text
Save-Setting "Copy" "CopyToPattern" $global:txtCopyToPattern.Text
foreach($item in ($global:dgObjectsToCopy.ItemsSource | where Selected -eq $true))
{
Write-Status "Copy $($item.ObjectType.Title) objects" -Force -SkipLog
Write-Log "----------------------------------------------------------------"
Write-Log "Copy $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------"
$url = $item.ObjectType.API
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
$nameProp = ?? $item.ObjectType.NameProperty "displayName"
foreach($graphObj in ($graphObjects | Where { $_.Object."$($nameProp)" -imatch [regex]::Escape($copyFrom) }))
{
$sourceName = $graphObj.Object."$($nameProp)"
$copyName = $sourceName -ireplace [regex]::Escape($copyFrom),$copyTo
$copyObj = $graphObjects | Where { $_.Object."$($nameProp)" -eq $copyName -and $_.Object.'@OData.Type' -eq $graphObj.Object.'@OData.Type' }
if(($copyObj | measure).Count -gt 0)
{
Write-Log "Object with name $copyName already exists. $sourceName will not be copied" 2
continue
}
else
{
Write-Status "Create $copyName from $sourceName" -Force
if($graphObj.ObjectType.PreCopyCommand)
{
if((& $graphObj.ObjectType.PreCopyCommand $graphObj.Object $graphObj.ObjectType $copyName))
{
continue
}
}
$copyFromObj = (Get-GraphObject $graphObj.Object $graphObj.ObjectType -SkipAssignments).Object
# Convert to Json and back to clone the object
$obj = ConvertTo-Json $copyFromObj -Depth 10 | ConvertFrom-Json
if($obj)
{
# Import new profile
Set-GraphObjectName $obj $graphObj.ObjectType $copyName
$newObj = Import-GraphObject $obj $graphObj.ObjectType
if($newObj)
{
if($graphObj.ObjectType.PostCopyCommand)
{
& $graphObj.ObjectType.PostCopyCommand $copyFromObj $newObj $graphObj.ObjectType
}
}
else
{
Write-log "Failed to copy $sourceName" 3
}
}
}
}
}
Write-Log "****************************************************************"
Write-Log "Bulk copy finished"
Write-Log "****************************************************************"
Write-Status ""
}

View File

@@ -20,7 +20,7 @@ $global:documentationProviders = @()
function Get-ModuleVersion
{
'1.0.1'
'1.0.2'
}
function Invoke-InitializeModule
@@ -124,7 +124,13 @@ function Get-ObjectDocumentation
{
param($documentationObj)
Write-Status "Get documentation info for $((Get-GraphObjectName $documentationObj.Object $documentationObj.ObjectType)) ($($documentationObj.ObjectType.Title))"
$additionalInfo = ""
if($documentationObj.Object.'@ObjectFromFile' -eq $true)
{
$additionalInfo = " - From File"
}
Write-Status "Get documentation info for $((Get-GraphObjectName $documentationObj.Object $documentationObj.ObjectType)) ($($documentationObj.ObjectType.Title))$additionalInfo"
$status = $null
$inputType = "Settings"
@@ -225,24 +231,8 @@ function Get-ObjectDocumentation
{
$inputType = "Property"
$processed = $true
<#
if([IO.File]::Exists(($global:AppRootFolder + "\Documentation\ObjectInfo\$($obj.'@OData.Type').json")))
{
# Process object based on OData type
$processed = Invoke-TranslateCustomProfileObject $obj "$($obj.'@OData.Type')"
}
elseif($objectType -and [IO.File]::Exists(($global:AppRootFolder + "\Documentation\ObjectInfo\#$($objectType.Id).json")))
{
# Process object based on Intune Object Type ($objectType)
# '#' is added to front of name to distinguish manually created files from generated files
$processed = Invoke-TranslateCustomProfileObject $obj "#$($objectType.Id)"
}
else
{
# Process objects based on generated Category Files and ObjectCategories.json
$processed = Invoke-TranslateProfileObject $obj
}
#>
$processed = Invoke-TranslateProfileObject $obj
if($processed -eq $false)
@@ -685,7 +675,7 @@ function Invoke-TranslateADMXObject
$categoryObj = $script:admxCategories | Where { $definitionValue.definition.id -in ($_.definitions.id) }
$category = $script:admxCategories.definitions | Where { $definitionValue.definition.id -in ($_.id) }
# Get presentation values for the current settings (with presentation object included)
if($definitionValue.presentationValues -or $obj.'@CompareObject' -eq $true) #$definitionValue.'definition@odata.bind')
if($definitionValue.presentationValues -or $obj.'@ObjectFromFile' -eq $true) #$definitionValue.'definition@odata.bind')
{
# Documenting exported json
#$presentationValues = (Invoke-GraphRequest -Url "$($definitionValue.'definition@odata.bind')/presentations?`$expand=presentation" -ODataMetadata "minimal").value
@@ -1039,7 +1029,7 @@ function Invoke-TranslateIntentObject
foreach($category in ($categories | Sort -Property displayName))
{
# Get settings for the category. This will put them in the correct order...
if($obj.'@CompareObject' -ne $true)
if($obj.'@ObjectFromFile' -ne $true)
{
$settings = (Invoke-GraphRequest "/deviceManagement/intents/$($obj.Id)/categories/$($category.Id)/settings?`$expand=Microsoft.Graph.DeviceManagementComplexSettingInstance/Value" -ODataMetadata "minimal" @params).Value
}
@@ -2662,7 +2652,9 @@ function Invoke-TranslateScheduledActionType
foreach($actionConfig in $actionRule.scheduledActionConfigurations)
{
$notificationTemplate = $null
$notificationTemplateId = $null
$additionalNotifications = $null
$additionalNotificationsList = $null
if($actionConfig.actionType -eq "notification")
{
@@ -2694,6 +2686,7 @@ function Invoke-TranslateScheduledActionType
if($actionConfig.notificationTemplateId -ne [Guid]::Empty)
{
$notificationTemplate = Get-LanguageString "ScheduledAction.Notification.selected"
$notificationTemplateId = $actionConfig.notificationTemplateId
}
else
{
@@ -2703,6 +2696,7 @@ function Invoke-TranslateScheduledActionType
if($actionConfig.notificationMessageCCList.Count -gt 0)
{
$additionalNotifications = ((Get-LanguageString "ScheduledAction.Notification.numSelected") -f $actionConfig.notificationMessageCCList.Count)
$additionalNotificationsList = $actionConfig.notificationMessageCCList -join ","
}
else
{
@@ -2710,11 +2704,26 @@ function Invoke-TranslateScheduledActionType
}
}
$objClone = $actionConfig | ConvertTo-Json -Depth 10 | ConvertFrom-Json
Remove-Property $objClone "Id"
foreach($prop in $objClone.PSObject.Properties)
{
if($prop.Name -like "*@odata*")
{
Remove-Property $objClone $prop.Name
}
}
# ToDo: Resolve MessageTemplateId and EmailCCIds to actual object names
$script:objectComplianceActionData += New-Object PSObject -Property @{
IdStr = ($objClone | ConvertTo-Json -Depth 10 -Compress)
Action = $actionType
Schedule = $schedule
MessageTemplate = $notificationTemplate
MessageTemplateId = $notificationTemplateId
EmailCC = $additionalNotifications
EmailCCIds = $additionalNotificationsList
Category=$category
RawJsonValue=($actionConfig | ConvertTo-Json -Depth 20 -Compress)
}
@@ -3455,11 +3464,10 @@ function Show-DocumentationForm
$tmpArr += $script:objectSettingsData | Select -Property $script:settingsProperties | ConvertTo-Csv -NoTypeInformation
}
if($script:objectSettingsData.Count -gt 0)
if($script:applicabilityRules.Count -gt 0)
{
$tmpArr += $script:applicabilityRules | Select -Property Rule,Property,Value,Category | ConvertTo-Csv -NoTypeInformation
}
}
if($script:objectComplianceActionData.Count -gt 0)
{

View File

@@ -10,7 +10,7 @@ This module will also document some objects based on PowerShell functions
function Get-ModuleVersion
{
'1.0.0'
'1.0.1'
}
function Invoke-InitializeModule
@@ -68,7 +68,14 @@ function Invoke-CDDocumentObject
return [PSCustomObject]@{
Properties = @("Name","Value","Category","SubCategory")
}
}
}
elseif($type -eq '#microsoft.graph.policySet')
{
Invoke-CDDocumentPolicySet $documentationObj
return [PSCustomObject]@{
Properties = @("Name","Value","Category","SubCategory")
}
}
}
function Get-CDAllManagedApps
@@ -2040,4 +2047,102 @@ function Invoke-CDDocumentConditionalAccess
EntityKey = "persistentBrowser"
})
}
}
}
#region Document Policy Sets
function Invoke-CDDocumentPolicySet
{
param($documentationObj)
$obj = $documentationObj.Object
$objectType = $documentationObj.ObjectType
$script:objectSeparator = ?? $global:cbDocumentationObjectSeparator.SelectedValue ([System.Environment]::NewLine)
$script:propertySeparator = ?? $global:cbDocumentationPropertySeparator.SelectedValue ","
###################################################
# Basic info
###################################################
Add-BasicDefaultValues $obj $objectType
Add-BasicPropertyValue (Get-LanguageString "TableHeaders.configurationType") (Get-LanguageString "SettingDetails.appConfiguration")
###################################################
# Basic info
###################################################
$addedSettings = @()
$policySetSettings = (
[PSCustomObject]@{
Types = @(
@('#microsoft.graph.mobileAppPolicySetItem','appTitle'),
@('#microsoft.graph.targetedManagedAppConfigurationPolicySetItem','appConfigurationTitle'),
@('#microsoft.graph.managedAppProtectionPolicySetItem','appProtectionTitle'),
@('#microsoft.graph.iosLobAppProvisioningConfigurationPolicySetItem','iOSAppProvisioningTitle'))
Category = (Get-LanguageString "PolicySet.appManagement")
},
[PSCustomObject]@{
Types = @(
@('#microsoft.graph.deviceConfigurationPolicySetItem','deviceConfigurationTitle'),
@('#microsoft.graph.deviceCompliancePolicyPolicySetItem','deviceComplianceTitle'),
@('#microsoft.graph.deviceManagementScriptPolicySetItem','powershellScriptTitle'))
Category = (Get-LanguageString "PolicySet.deviceManagement")
},
[PSCustomObject]@{
Types = @(
@('#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem','deviceTypeRestrictionTitle'),
@('#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem','windowsAutopilotDeploymentProfileTitle'),
@('#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem','enrollmentStatusSettingTitle'))
Category = (Get-LanguageString "PolicySet.deviceEnrollment")
}
)
foreach($policySettingType in $policySetSettings)
{
foreach($subType in $policySettingType.Types)
{
foreach($setting in ($obj.items | where '@OData.Type' -eq $subType[0]))
{
if($setting.status -eq "error")
{
Write-Log "Skipping missing $($subType[0]) type with id $($setting.id). Error code: $($setting.errorCode)"
continue
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = $setting.displayName
Value = (Get-CDDocumentPolicySetValue $setting)
EntityKey = $setting.id
Category = $policySettingType.Category
SubCategory = (Get-LanguageString "PolicySet.$($subType[1])")
})
}
}
}
}
function Get-CDDocumentPolicySetValue
{
param($policySetItem)
if($policySetItem.'@OData.Type' -eq '#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem' -or
$policySetItem.'@OData.Type' -eq '#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem')
{
return $policySetItem.Priority
}
elseif($policySetItem.'@OData.Type' -eq '#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem')
{
if($policySetItem.itemType -eq '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile')
{
return (Get-LanguageString "Autopilot.DirectoryService.azureAD")
}
elseif($policySetItem.itemType -eq '#microsoft.graph.activeDirectoryWindowsAutopilotDeploymentProfile')
{
return (Get-LanguageString "Autopilot.DirectoryService.activeDirectoryAD")
}
}
# ToDo: Add support for all PolicySet items
}
#endregion

View File

@@ -3,7 +3,7 @@
#https://docs.microsoft.com/en-us/office/vba/api/overview/word
function Get-ModuleVersion
{
'1.0.0'
'1.0.1'
}
function Invoke-InitializeModule
@@ -225,24 +225,12 @@ function Invoke-WordPostProcessItems
$script:doc.TablesOfFigures | ForEach-Object -Process { $_.Update() | Out-Null }
$fileName = $global:txtWordDocumentName.Text
[Environment]::SetEnvironmentVariable("Date",(Get-Date).ToString("yyyy-MM-dd"),[System.EnvironmentVariableTarget]::Process)
[Environment]::SetEnvironmentVariable("Organization",$global:Organization.displayName,[System.EnvironmentVariableTarget]::Process)
if(-not $fileName)
{
$fileName = "%MyDocuments%\%Organization%-%Date%.docx"
}
$fileName = [Environment]::ExpandEnvironmentVariables($fileName)
foreach($tmpFolder in ([System.Enum]::GetNames([System.Environment+SpecialFolder])))
{
$fileName = $fileName -replace "%$($tmpFolder)%",([Environment]::GetFolderPath($tmpFolder))
}
[Environment]::SetEnvironmentVariable("Date",$null,[System.EnvironmentVariableTarget]::Process)
[Environment]::SetEnvironmentVariable("Organization",$null,[System.EnvironmentVariableTarget]::Process)
$fileName
$fileName = Expand-FileName $fileName
$format = [Microsoft.Office.Interop.Word.WdSaveFormat]::wdFormatDocumentDefault

View File

@@ -10,7 +10,7 @@ This module is for the Endpoint Manager/Intune View. It manages Export/Import/Co
#>
function Get-ModuleVersion
{
'3.1.2'
'3.1.3'
}
function Invoke-InitializeModule
@@ -562,7 +562,7 @@ function Invoke-EMAuthenticateToMSAL
{
$global:EMViewObject.AppInfo = Get-GraphAppInfo "EMAzureApp" "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"
Set-MSALCurrentApp $global:EMViewObject.AppInfo
& $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) -Permissions $global:EMViewObject.Permissions
& $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser"))
}
function Invoke-EMDeactivateView
@@ -1178,7 +1178,7 @@ function Start-PostListAppProtection
param($objList, $objectType)
# App Configurations for Managed Apps are included in App Protections e.g. the /deviceAppManagement/managedAppPolicies API
# For some reason, the some $filter options is not supported to filter out these objects
# For some reason, the $filter option is not supported to filter out these objects
# e.g. not isof(...) to excluded the type, not startsWith(id, 'A_') to exlude based on Id
# These filters generates a request error so fiter them out manually in this function instead
# The portal is probably doing the same thing since these are included in the return but not in the UI

View File

@@ -10,7 +10,7 @@ This module is for the Endpoint Info View. It shows read-only objects in Intune
#>
function Get-ModuleVersion
{
'3.1.1'
'3.1.2'
}
function Invoke-InitializeModule
@@ -100,6 +100,6 @@ function Invoke-EMInfoAuthenticateToMSAL
$usr = (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser"))
if($usr)
{
& $global:msalAuthenticator.Login -Account $usr -Permissions $global:EMInfoViewObject.Permissions
& $global:msalAuthenticator.Login -Account $usr
}
}

View File

@@ -10,7 +10,7 @@ This module manages Authentication for the application with MSAL. It is also res
#>
function Get-ModuleVersion
{
'3.0.1'
'3.0.2'
}
$global:msalAuthenticator = $null
@@ -520,9 +520,7 @@ function Connect-MSALUser
[switch]
$Interactive,
$Account,
$Permissions = @() # Addidional permissions required by the current view object
$Account
)
# No login during first time the app is started
@@ -563,7 +561,7 @@ function Connect-MSALUser
$global:MSALToken = $null
}
if((Get-SettingValue "UseDefaultPermissions") -eq $true)
if((Get-SettingValue "UseDefaultPermissions") -eq $true -or ($global:currentViewObject.ViewInfo.Permissions | measure).Count -eq 0)
{
[string[]] $Scopes = "https://graph.microsoft.com/.default"
$useDefaultPermissions = $true
@@ -582,11 +580,7 @@ function Connect-MSALUser
$reqScopes += "RoleManagement.Read.Directory"
}
if($Permissions.Count -gt 0)
{
$script:curViewPermissions = $Permissions
}
$reqScopes += $script:curViewPermissions
$script:curViewPermissions = $global:currentViewObject.ViewInfo.Permissions
foreach($tmpScope in $script:curViewPermissions)
{

View File

@@ -10,7 +10,7 @@ This module manages Microsoft Grap fuctions like calling APIs, managing graph ob
#>
function Get-ModuleVersion
{
'3.1.1'
'3.1.2'
}
$global:MSGraphGlobalApps = @(
@@ -622,6 +622,7 @@ function Show-GraphExportForm
Set-XamlProperty $script:exportForm "txtExportPath" "Text" (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder"))
Set-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked" (Get-SettingValue "AddObjectType")
Set-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName")
Set-XamlProperty $script:exportForm "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments")
Set-XamlProperty $script:exportForm "btnExportSelected" "IsEnabled" ($global:dgObjects.SelectedItem -ne $null)
if(($global:dgObjects.ItemsSource | Where IsSelected -eq $true).Count -gt 0)
@@ -672,6 +673,7 @@ function Show-GraphBulkExportForm
Set-XamlProperty $script:exportForm "txtExportPath" "Text" (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder"))
Set-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName")
Set-XamlProperty $script:exportForm "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments")
Add-XamlEvent $script:exportForm "browseExportPath" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $script:exportForm "txtExportPath" "Text") "Select root folder for export"
@@ -1147,11 +1149,20 @@ function Get-GraphFileObjects
$fileArr = @()
foreach($file in (Get-Item -path "$path\*.json" @params))
{
if($ObjectType.LoadObject)
{
$graphObj = & $ObjectType.LoadObject $file.FullName
}
else
{
$graphObj = (ConvertFrom-Json (Get-Content $file.FullName -Raw))
}
$obj = New-Object PSObject -Property @{
FileName = $file.Name
FileInfo = $file
Selected = $SelectedStatus
Object = (ConvertFrom-Json (Get-Content $file.FullName -Raw))
Object = $graphObj
ObjectType = $ObjectType
}
@@ -1818,13 +1829,7 @@ function Export-GraphObject
if($chkExportAssignments.IsChecked -ne $true -and $obj.Assignments)
{
### ToDo: Fix full support for including Assignments. $extend=Assignments might not work
### E.g. Check AutoPilot
Remove-Property $obj $Assignments
}
elseif($chkExportAssignments.IsChecked -eq $true -and -not $obj.Assignments)
{
Remove-Property $obj "Assignments"
}
$obj | ConvertTo-Json -Depth 10 | Out-File ([IO.Path]::Combine($exportFolder, (Remove-InvalidFileNameChars "$((Get-GraphObjectName $obj $objectType)).json")))

View File

@@ -26,7 +26,29 @@ Objects can be compared based on property values or documented values.
The property value method is a quick way to compare objects but it will only show the names and values of the native Intune object. This is not a good comparison method for Settings objects since they have all the settings in one property.
The documentation method is a bit slower but will show the values as they are stated in the Intune portal. This is the recommended way to compare objects but note that this is only supported on object types that supports documentation.
The documentation method is a bit slower but will show the values as they are stated in the Intune portal. This is the recommended way to compare objects but note that this is only supported on object types that supports documentation.
Bulk compare is supported. This can be performed in two ways:
* **Export File** - This will read each exported file and compare it with the existing object
The result file will be stored in the exported folder structure. Either in the Object Type folder or the parent folder depending on the Save as setting.
**Note:** This cannot be used with files exported from a different environment since it used the Id as identifier
* **Named Objects** - Compare file based on patterns
This can be used in where a pattern is used separate objects between different environments e.g. [Test] Policy 1 vs [Prod] Policy 1.
Output files are by default stored in the My Documents folder.
The output CSV can either be one file for ALL objects or one file for each Object Type.
## Bulk Copy
Bulk copy can be used to clone objects based on a name pattern. This can be used in the same scenario as Bulk Compare where the object names includes an environment identifier. The application will identify all objects matching the source pattern and copy each object with a new name matching the 'Copy object name pattern'. The object will not be copied if it detects that an object already exists with the new name.
**Note:** Assignments will NOT be copied.
## Change log

View File

@@ -1,5 +1,27 @@
# Release Notes
## 3.1.3 - 2021-07-05
**New features**
- Bulk Compare
- Compare with exported files
- Compare with existing objects based on name patterns
- Bulk Copy
- Copy existing objects based on name patterns
- Support for documenting PolicySets
- Release Notes check - Check if there are any updates by comparing the local version of ReleaseNotes.md with the GitHub version
**Fixes**
* Fixed bug that caused an exception when exporting objects with an assignment and the 'Export Assignment' option disabled.
See [Issue 16](https://github.com/Micke-K/IntuneManagement/issues/16) for more info
* Export Assignments in Bulk Export and Object Export did not get default value from Settings
* Fixed issue where the required permissions were not passed during authentication
## 3.1.2 - 2021-06-20
**New features**

79
Xaml/BulkCompare.xaml Normal file
View File

@@ -0,0 +1,79 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Name="grdImportProperties">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0">
<Label Content="Compare object" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the objects to compare e.g. compare exported files with existing objects or two existing objects based on name" />
</StackPanel>
<ComboBox Name="cbCompareProvider" Margin="0,5,0,0" MinWidth="250" Grid.Row='1' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
</Grid>
<ContentControl Name="ccContentProviderOptions" Grid.Row="1" Grid.ColumnSpan="2" />
<Grid Grid.Row='2' VerticalAlignment="Stretch" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='0'>
<Label Content="Save as" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specifies how the export csv should be saved. One file per ObjectType or one file for all objects." />
</StackPanel>
<ComboBox Name="cbCompareSave" Margin="0,5,0,0" MinWidth="250" Grid.Row='0' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='1' >
<Label Content="Comparison Type" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify how objects should be compared" />
</StackPanel>
<ComboBox Name="cbCompareType" Margin="0,5,0,0" MinWidth="250" Grid.Row='1' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" />
<Grid Margin="0,0,5,0" Grid.Row='2' >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="Objects to compare" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the object types that should be compared" />
</StackPanel>
</Grid>
<DataGrid Name="dgObjectsToCompare" Margin="0,5,0,5" Grid.Row='2' Grid.Column='1' CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="White" />
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='3' Grid.ColumnSpan='2' >
<Button Name="btnStartCompare" Content="Compare" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid >

50
Xaml/BulkCopy.xaml Normal file
View File

@@ -0,0 +1,50 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Name="grdImportProperties">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<Label Content="Source object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the source objects e.g. Test -" />
</StackPanel>
<TextBox Text="" Name="txtCopyFromPattern" Grid.Column='1' Grid.Row='0' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='1'>
<Label Content="Copy object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the new object e.g. Prod -" />
</StackPanel>
<TextBox Text="" Name="txtCopyToPattern" Grid.Column='1' Grid.Row='1' Margin="0,5,5,0" />
<Grid Margin="0,0,5,0" Grid.Row='2' >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Label Content="Objects to copy" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Select the object types that should be copied" />
</StackPanel>
</Grid>
<DataGrid Name="dgObjectsToCopy" Margin="0,5,0,5" Grid.Row='2' Grid.Column='1' CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="White" />
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='3' Grid.ColumnSpan='2' >
<Button Name="btnStartCopy" Content="Copy" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid >

View File

@@ -45,7 +45,7 @@
<CheckBox Grid.Column='1' Grid.Row='1' Name='chkAddObjectType' VerticalAlignment="Center" IsEnabled="false" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0">
<Label Content="ExportAssignments" />
<Label Content="Export Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Export object assignments" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='2' Name='chkExportAssignments' VerticalAlignment="Center" IsChecked="true" />

View File

@@ -0,0 +1,27 @@
<Grid Name="grdImportProperties" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<Label Content="Export root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The root folder where exported files are stored. This sould be the Company Name folder if it was included in the export." />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='0' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="" Name="txtExportPath" />
<Button Grid.Column="2" Name="browseExportPath" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
</Grid>

View File

@@ -0,0 +1,40 @@
<Grid Name="grdImportProperties" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<Label Content="Source object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the source objects e.g. Test -" />
</StackPanel>
<TextBox Text="" Name="txtCompareSource" Grid.Column='1' Grid.Row='0' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" Grid.Row='1'>
<Label Content="Compare object name pattern" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify the pattern of the objects that the source should be compared with e.g. Prod -" />
</StackPanel>
<TextBox Text="" Name="txtCompareWith" Grid.Column='1' Grid.Row='1' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Margin="0,5,5,0" Grid.Row='2' >
<Label Content="Save folder" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The folder where compare files will be saved. Default is the MyDocuments folder" />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='2' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="" Name="txtSavePath" />
<Button Grid.Column="2" Name="browseSavePath" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
</Grid>

View File

@@ -34,6 +34,7 @@
<Menu Name="mnuMain" Padding="0,5,0,5" Grid.ColumnSpan="2" >
<MenuItem Header="_File" >
<MenuItem Header="_Settings" Name="mnuSettings" />
<MenuItem Header="_Release Notes" Name="mnuUpdates" />
<MenuItem Header="_About" Name="mnuAbout" />
<MenuItem Header="_Exit" Name="mnuExit" />
</MenuItem>

45
Xaml/UpdatesDialog.xaml Normal file
View File

@@ -0,0 +1,45 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0,5,0,5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TabControl SelectedIndex="0" Margin="0,0,0,5">
<TabItem Header="Release Notes">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Padding="0,0,5,0">
<TextBlock Name="txtReleaseNotes" />
</ScrollViewer>
</Grid>
</TabItem>
<TabItem Header="Local Release Notes" Name="tabLocalReleaseNotes">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Padding="0,0,5,0">
<TextBlock Name="txtReleaseNotesLocal" />
</ScrollViewer>
</Grid>
</TabItem>
</TabControl>
<StackPanel Name="spCompareSubMenu" Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='1'>
<TextBlock Name="txtReleaseNotesMatch">
The local and GitHub versions of ReleaseNotes.md match.
</TextBlock>
<TextBlock Name="txtReleaseNotesNoMatch">
The local and GitHub versions of ReleaseNotes.md does not match. Download the latest version from
<Hyperlink Name="linkSource" NavigateUri="https://github.com/Micke-K/IntuneManagement">
GitHub
</Hyperlink>.
</TextBlock>
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>
</Grid>