Private/Show-ProgressGui.ps1
|
Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName PresentationCore Add-Type -AssemblyName WindowsBase function Show-ProgressGui { param( [array]$Devices, [string]$ScriptName, [string]$ScriptId ) $useParallel = $Devices.Count -gt 50 $concurrency = 10 # Get theme colors $t = Get-IRODThemeColors $xaml = @" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Running Remediation" Height="450" Width="850" WindowStartupLocation="CenterScreen" Topmost="True" Background="$($t.WindowBackground)"> <Window.Resources> <Style TargetType="Button"> <Setter Property="Background" Value="$($t.ButtonBackground)"/> <Setter Property="Foreground" Value="$($t.TextPrimary)"/> <Setter Property="BorderBrush" Value="$($t.Border)"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Padding" Value="12,6"/> <Setter Property="FontSize" Value="13"/> <Setter Property="Cursor" Value="Hand"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="$($t.ButtonHover)"/> </Trigger> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Background" Value="$($t.ControlBackgroundHover)"/> <Setter Property="Foreground" Value="$($t.TextPlaceholder)"/> </Trigger> </Style.Triggers> </Style> <Style TargetType="TextBlock"> <Setter Property="Foreground" Value="$($t.TextPrimary)"/> </Style> <Style TargetType="ProgressBar"> <Setter Property="Background" Value="$($t.ControlBackground)"/> <Setter Property="Foreground" Value="$($t.AccentGreen)"/> <Setter Property="BorderBrush" Value="$($t.Border)"/> <Setter Property="BorderThickness" Value="1"/> </Style> <Style TargetType="ListBox"> <Setter Property="Background" Value="$($t.ListBackground)"/> <Setter Property="Foreground" Value="$($t.AccentGreen)"/> <Setter Property="BorderBrush" Value="$($t.Border)"/> <Setter Property="BorderThickness" Value="1"/> </Style> <Style TargetType="ListBoxItem"> <Setter Property="Foreground" Value="$($t.SuccessGreen)"/> <Setter Property="Background" Value="Transparent"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="$($t.ListItemHover)"/> </Trigger> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="$($t.SelectedBackground)"/> <Setter Property="Foreground" Value="$($t.SelectedText)"/> </Trigger> </Style.Triggers> </Style> </Window.Resources> <Grid Margin="15"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- Header --> <StackPanel Grid.Row="0" Margin="0,0,0,15"> <TextBlock Text="Running Remediation Script" FontSize="16" FontWeight="Bold"/> <TextBlock Name="ScriptNameText" FontSize="12" Margin="0,5,0,0"/> <TextBlock Name="ModeText" FontSize="11" Foreground="$($t.TextPlaceholder)" Margin="0,3,0,0"/> </StackPanel> <!-- Progress Bar --> <StackPanel Grid.Row="1" Margin="0,0,0,15"> <ProgressBar Name="ProgressBar" Height="20" Minimum="0" Maximum="100" Value="0"/> <TextBlock Name="ProgressText" Text="0 / 0 devices processed" HorizontalAlignment="Center" Margin="0,8,0,0"/> </StackPanel> <!-- Results List --> <ListBox Name="ResultsList" Grid.Row="2" FontFamily="Consolas" FontSize="14" ScrollViewer.VerticalScrollBarVisibility="Auto"/> <!-- Close Button --> <Button Name="CloseBtn" Grid.Row="3" Content="Close" Width="100" Height="30" HorizontalAlignment="Right" Margin="0,10,0,0" IsEnabled="False"/> </Grid> </Window> "@ $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) $window = [System.Windows.Markup.XamlReader]::Load($reader) $scriptNameText = $window.FindName("ScriptNameText") $modeText = $window.FindName("ModeText") $progressBar = $window.FindName("ProgressBar") $progressText = $window.FindName("ProgressText") $resultsList = $window.FindName("ResultsList") $closeBtn = $window.FindName("CloseBtn") $scriptNameText.Text = "Script: $ScriptName" $progressBar.Maximum = $Devices.Count if ($useParallel) { $modeText.Text = "Parallel mode: $concurrency concurrent requests (devices > 50)" } else { $modeText.Text = "Sequential mode (devices <= 50)" } $closeBtn.Add_Click({ $window.Close() }) # Process devices $window.Add_ContentRendered({ $total = $Devices.Count $completed = 0 $succeeded = 0 $failed = 0 $failedDevices = @() if ($useParallel) { # Parallel execution using runspace pool $resultsList.Items.Add("[$(Get-Date -Format 'HH:mm:ss')] Starting parallel processing of $total devices...") $resultsList.ScrollIntoView($resultsList.Items[$resultsList.Items.Count - 1]) [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) # Process in batches $batchSize = $concurrency $batches = [Math]::Ceiling($total / $batchSize) for ($batchNum = 0; $batchNum -lt $batches; $batchNum++) { $startIdx = $batchNum * $batchSize $endIdx = [Math]::Min($startIdx + $batchSize - 1, $total - 1) $batchDevices = $Devices[$startIdx..$endIdx] $resultsList.Items.Add("[$(Get-Date -Format 'HH:mm:ss')] Processing batch $($batchNum + 1)/$batches ($($batchDevices.Count) devices)...") $resultsList.ScrollIntoView($resultsList.Items[$resultsList.Items.Count - 1]) [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) # Create runspace pool $runspacePool = [runspacefactory]::CreateRunspacePool(1, $concurrency) $runspacePool.Open() $runspaces = @() foreach ($device in $batchDevices) { $scriptBlock = { param($DeviceId, $DeviceName, $ScriptId, $GraphBaseUrl) try { # Invoke remediation $uri = "$GraphBaseUrl/deviceManagement/managedDevices/$DeviceId/initiateOnDemandProactiveRemediation" $body = @{ scriptPolicyId = $ScriptId } | ConvertTo-Json -Depth 10 Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body | Out-Null # Sync device $syncUri = "$GraphBaseUrl/deviceManagement/managedDevices/$DeviceId/syncDevice" Invoke-MgGraphRequest -Method POST -Uri $syncUri | Out-Null return @{ Success = $true; DeviceName = $DeviceName; Error = $null } } catch { return @{ Success = $false; DeviceName = $DeviceName; Error = $_.Exception.Message } } } $powershell = [powershell]::Create().AddScript($scriptBlock) $powershell.AddArgument($device.Id) $powershell.AddArgument($device.DeviceName) $powershell.AddArgument($ScriptId) $powershell.AddArgument($script:GraphBaseUrl) $powershell.RunspacePool = $runspacePool $runspaces += @{ PowerShell = $powershell Handle = $powershell.BeginInvoke() DeviceName = $device.DeviceName } } # Wait for batch to complete and collect results foreach ($rs in $runspaces) { $result = $rs.PowerShell.EndInvoke($rs.Handle) $rs.PowerShell.Dispose() $completed++ if ($result.Success) { $succeeded++ } else { $failed++ $failedDevices += @{ Name = $result.DeviceName; Error = $result.Error } } } $runspacePool.Close() $runspacePool.Dispose() # Update progress $progressBar.Value = $completed $progressText.Text = "$completed / $total devices processed (Succeeded: $succeeded | Failed: $failed)" [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) } # Show summary $resultsList.Items.Add("") $resultsList.Items.Add("[$(Get-Date -Format 'HH:mm:ss')] COMPLETED: $succeeded succeeded, $failed failed out of $total devices") if ($failedDevices.Count -gt 0 -and $failedDevices.Count -le 20) { $resultsList.Items.Add("") $resultsList.Items.Add("Failed devices:") foreach ($fd in $failedDevices) { $resultsList.Items.Add(" - $($fd.Name): $($fd.Error)") } } elseif ($failedDevices.Count -gt 20) { $resultsList.Items.Add("") $resultsList.Items.Add("$($failedDevices.Count) devices failed. First 20:") foreach ($fd in ($failedDevices | Select-Object -First 20)) { $resultsList.Items.Add(" - $($fd.Name): $($fd.Error)") } } $resultsList.ScrollIntoView($resultsList.Items[$resultsList.Items.Count - 1]) } else { # Sequential execution (original behavior) foreach ($device in $Devices) { $deviceName = $device.DeviceName $deviceId = $device.Id $resultsList.Items.Add("[$(Get-Date -Format 'HH:mm:ss')] Processing: $deviceName...") $resultsList.ScrollIntoView($resultsList.Items[$resultsList.Items.Count - 1]) [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) try { Invoke-Remediation -ScriptId $ScriptId -DeviceId $deviceId Sync-Device -DeviceId $deviceId $resultsList.Items.Add("[$(Get-Date -Format 'HH:mm:ss')] SUCCESS: $deviceName - Remediation triggered and sync initiated") $succeeded++ } catch { $resultsList.Items.Add("[$(Get-Date -Format 'HH:mm:ss')] FAILED: $deviceName - $($_.Exception.Message)") $failed++ } $completed++ $progressBar.Value = $completed $progressText.Text = "$completed / $total devices processed" $resultsList.ScrollIntoView($resultsList.Items[$resultsList.Items.Count - 1]) [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) } $resultsList.Items.Add("") $resultsList.Items.Add("COMPLETED: $succeeded succeeded, $failed failed out of $total devices") $resultsList.ScrollIntoView($resultsList.Items[$resultsList.Items.Count - 1]) } $closeBtn.IsEnabled = $true $closeBtn.Focus() }) $window.ShowDialog() | Out-Null } |