Modules/CyberConfigApp/CyberConfigAppHelpers/CyberConfigAppGraphHelper.psm1
|
Function Update-GraphStatusIndicator { <# .SYNOPSIS Updates the Graph connection status indicator in the UI. .DESCRIPTION Updates the visual indicator to show whether Microsoft Graph is connected or disconnected. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "IsConnected")] param( [bool]$IsConnected = $syncHash.GraphConnected ) try { $syncHash.Window.Dispatcher.Invoke([Action]{ if ($IsConnected) { # Connected state - Green indicator $syncHash.GraphStatusIndicator.Fill = [System.Windows.Media.Brushes]::Green $syncHash.GraphStatusText.Text = $syncHash.UIConfigs.localeStatusMessages.GraphConnected $syncHash.GraphStatusText.Foreground = [System.Windows.Media.Brushes]::DarkGreen $syncHash.GraphStatusBorder.Background = [System.Windows.Media.Brushes]::LightGreen $syncHash.GraphStatusBorder.ToolTip = "Microsoft Graph is connected and ready for data queries" Write-DebugOutput -Message "Graph status indicator updated: Connected" -Source $MyInvocation.MyCommand -Level "Info" } else { # Disconnected state - Red indicator $syncHash.GraphStatusIndicator.Fill = [System.Windows.Media.Brushes]::Red $syncHash.GraphStatusText.Text = $syncHash.UIConfigs.localeStatusMessages.GraphDisconnected $syncHash.GraphStatusText.Foreground = [System.Windows.Media.Brushes]::DarkRed $syncHash.GraphStatusBorder.Background = [System.Windows.Media.Brushes]::LightPink $syncHash.GraphStatusBorder.ToolTip = "Microsoft Graph is not connected - some features may be limited" Write-DebugOutput -Message "Graph status indicator updated: Disconnected" -Source $MyInvocation.MyCommand -Level "Info" } }) } catch { Write-DebugOutput -Message "Error updating Graph status indicator: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } } Function Initialize-GraphStatusIndicator { <# .SYNOPSIS Initializes the Graph status indicator with click functionality. .DESCRIPTION Sets up the Graph status indicator and adds click event for connection management. #> # Add click event to the status border for connection management $syncHash.GraphStatusBorder.Add_MouseLeftButtonUp({ if ($syncHash.GraphConnected) { # Show disconnect option $result = $syncHash.ShowMessageBox.Invoke( "Do you want to disconnect from Microsoft Graph?`n`nThis will disable dynamic data queries but won't affect your current configuration.", "Disconnect Graph", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question ) if ($result -eq [System.Windows.MessageBoxResult]::Yes) { try { Disconnect-MgGraph -ErrorAction Stop $syncHash.GraphConnected = $false Update-GraphStatusIndicator -IsConnected $false # Remove dynamic Graph buttons # You might want to add a function to clean up dynamic buttons $syncHash.ShowMessageBox.Invoke( "Successfully disconnected from Microsoft Graph.", "Graph Disconnected", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information ) } catch { Write-DebugOutput -Message "Error disconnecting from Graph: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" $syncHash.ShowMessageBox.Invoke( "Error disconnecting from Graph: $($_.Exception.Message)", "Disconnect Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error ) } } } else { # Show connect information $syncHash.ShowMessageBox.Invoke( $syncHash.UIConfigs.LocalePopupMessages.GraphNotConnected, "Graph Connection", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information ) } }) # Set initial status Update-GraphStatusIndicator -IsConnected $syncHash.GraphConnected Write-DebugOutput -Message "Graph status indicator initialized" -Source $MyInvocation.MyCommand -Level "Info" } # Enhanced Graph Query Function with Filter Support Function Invoke-GraphQueryWithFilter { <# .SYNOPSIS Executes Microsoft Graph API queries with filtering support in a background thread. .DESCRIPTION This Function performs asynchronous Microsoft Graph API queries with optional filtering, returning data for users, groups, or other Graph entities. #> param( [string]$QueryType, $GraphConfig, [string]$FilterString, [int]$Top = 999 ) Write-DebugOutput "Starting Graph query - Type: $QueryType, Filter: $FilterString, Top: $Top" -Source "Invoke-GraphQueryWithFilter" -Level "Debug" # Create runspace $runspace = [runspacefactory]::CreateRunspace() $runspace.Open() Write-DebugOutput "Created new runspace for Graph query" -Source "Invoke-GraphQueryWithFilter" -Level "Verbose" # Create PowerShell instance $powershell = [powershell]::Create() $powershell.Runspace = $runspace # Add script block $scriptBlock = { param($QueryType, $GraphConfig, $FilterString, $Top) try { # Get query configuration $queryConfig = $GraphConfig.$QueryType if (-not $queryConfig) { #Write-DebugOutput -Message "Query configuration not found for: $QueryType" -Source $MyInvocation.MyCommand -Level "Error" } # Build query parameters $queryParams = @{ Uri = $queryConfig.endpoint Method = "Get" } # Build query string $queryStringParts = @() # Add existing query parameters from config if ($queryConfig.queryParameters) { foreach ($param in $queryConfig.queryParameters.psobject.properties.name) { $queryStringParts += "$param=$($queryConfig.queryParameters.$param)" } } # Add filter if provided if (![string]::IsNullOrWhiteSpace($FilterString)) { $queryStringParts += "`$filter=$FilterString" } # Add top parameter $queryStringParts += "`$top=$Top" # Combine query string if ($queryStringParts.Count -gt 0) { $queryParams.Uri += $syncHash.GraphEndpoint + "?" + ($queryStringParts -join "&") } #Write-DebugOutput -Message "Graph Query URI: $($queryParams.Uri)" -Source $MyInvocation.MyCommand -Level "Information" # Execute the Graph request $result = Invoke-MgGraphRequest @queryParams -OutputType PSObject # Return the result return @{ Success = $true Data = $result QueryConfig = $queryConfig Message = "Successfully retrieved $($result.value.Count) items" FilterApplied = ![string]::IsNullOrWhiteSpace($FilterString) } } catch { #Write-DebugOutput -Message "Error executing Graph query: $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" return @{ Success = $false Error = $_.Exception.Message Message = "Failed to retrieve data from uri [{0}]: {1}" -f $queryParams.Uri, $($_.Exception.Message) FilterApplied = ![string]::IsNullOrWhiteSpace($FilterString) } } } # Add parameters and start execution $powershell.AddScript($scriptBlock).AddParameter("QueryType", $QueryType).AddParameter("GraphConfig", $GraphConfig).AddParameter("FilterString", $FilterString).AddParameter("Top", $Top) $asyncResult = $powershell.BeginInvoke() return @{ PowerShell = $powershell AsyncResult = $asyncResult Runspace = $runspace } } Function Get-GraphEntityConfig { <# .SYNOPSIS Dynamically builds entity configurations from the JSON graphQueries configuration. .DESCRIPTION This function reads the graphQueries section from the UI configuration and creates the entity configurations needed for the Graph selector dialogs. #> param( [string]$entityType ) # Check if the specific entity type exists in the configuration $queryProperty = $syncHash.UIConfigs.graphQueries.PSObject.Properties | Where-Object { $_.Name -eq $entityType } if (-not $queryProperty) { Write-DebugOutput -Message "Entity type '$entityType' not found in graphQueries configuration" -Source $MyInvocation.MyCommand -Level "Error" return $null } $queryConfig = $queryProperty.Value # Build the configuration for the specific entity type $config = @{ Title = $queryConfig.windowTitle SearchPlaceholder = "Search by $($queryConfig.searchProperty.ToLower())..." LoadingMessage = "Loading $($queryConfig.name.ToLower())..." NoResultsMessage = "No $($queryConfig.name.ToLower()) found matching the search criteria." NoResultsTitle = "No $($queryConfig.name) Found" FilterProperty = $queryConfig.queryfilterProperty SearchProperty = $queryConfig.searchProperty QueryType = $entityType AllowMultiple = $queryConfig.allowMultipleSelection } # Build column configuration from displayColumnOrder $columnConfig = [ordered]@{} foreach ($column in $queryConfig.displayColumnOrder) { $columnConfig[$column.value] = @{ Header = $column.name Width = 200 # Default width, you could make this configurable too } } $config.ColumnConfig = $columnConfig # Create data transform script block with columns captured in closure $columns = $queryConfig.displayColumnOrder $config.DataTransform = { param($item) $result = [PSCustomObject]@{ OriginalObject = $item } foreach ($column in $columns) { $propName = $column.value # Handle special cases for group types if ($propName -eq "groupTypes" -and $item.GroupTypes) { $groupType = "Distribution" if ($item.SecurityEnabled) { $groupType = "Security" } if ($item.GroupTypes -contains "Unified") { $groupType = "Microsoft 365" } $result | Add-Member -MemberType NoteProperty -Name "GroupType" -Value $groupType } else { $result | Add-Member -MemberType NoteProperty -Name $propName -Value $item.$propName } } return $result }.GetNewClosure() return $config } Function Show-GraphProgressWindow { <# .SYNOPSIS Displays a progress window while executing Graph queries and shows results in a selection interface. .DESCRIPTION This Function shows a progress dialog during Graph API operations and presents the results in a searchable, selectable data grid for users and groups. #> param( [string]$GraphEntityType, [string]$SearchTerm = "", [int]$Top = 100 ) try { # Get configuration for the specified entity type $config = Get-GraphEntityConfig -entityType $GraphEntityType if ($config) { Write-DebugOutput -Message "Using configuration for graph entity type: $GraphEntityType" -Source $MyInvocation.MyCommand -Level "Verbose" }else{ Write-DebugOutput -Message "Unsupported graph entity type: $GraphEntityType" -Source $MyInvocation.MyCommand -Level "Error" } # Build filter string $filterString = $null if (![string]::IsNullOrWhiteSpace($SearchTerm)) { $filterString = "startswith($($config.FilterProperty),'$SearchTerm')" } # Show progress window $progressWindow = New-Object System.Windows.Window $progressWindow.Title = $config.Title $progressWindow.Width = 300 $progressWindow.Height = 120 $progressWindow.WindowStartupLocation = "CenterOwner" $progressWindow.Owner = $syncHash.Window $progressWindow.Background = [System.Windows.Media.Brushes]::White $progressWindow.Icon = $syncHash.ImgPath $progressPanel = New-Object System.Windows.Controls.StackPanel $progressPanel.Margin = "20" $progressPanel.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center $progressPanel.VerticalAlignment = [System.Windows.VerticalAlignment]::Center $progressLabel = New-Object System.Windows.Controls.Label $progressLabel.Content = $config.LoadingMessage $progressLabel.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center $progressBar = New-Object System.Windows.Controls.ProgressBar $progressBar.Width = 200 $progressBar.Height = 20 $progressBar.IsIndeterminate = $true [void]$progressPanel.Children.Add($progressLabel) [void]$progressPanel.Children.Add($progressBar) $progressWindow.Content = $progressPanel # Start async operation Write-DebugOutput -Message "Starting async operation for graph query type: $($config.QueryType) with filter: $filterString" -Source $MyInvocation.MyCommand -Level "Verbose" $asyncOp = Invoke-GraphQueryWithFilter ` -QueryType $config.QueryType ` -GraphConfig $syncHash.UIConfigs.graphQueries ` -FilterString $filterString -Top $Top # Show progress window $progressWindow.Show() # Wait for completion while (-not $asyncOp.AsyncResult.IsCompleted) { [System.Windows.Forms.Application]::DoEvents() Start-Sleep -Milliseconds 100 } # Close progress window $progressWindow.Close() # Get results $result = $asyncOp.PowerShell.EndInvoke($asyncOp.AsyncResult) $asyncOp.PowerShell.Dispose() $asyncOp.Runspace.Close() $asyncOp.Runspace.Dispose() if ($result.Success) { Write-DebugOutput -Message "Graph query successful for entity type: $GraphEntityType, items found: $($result.Data.value.Count)" -Source $MyInvocation.MyCommand -Level "Verbose" $items = $result.Data.value if (-not $items -or $items.Count -eq 0) { $syncHash.ShowMessageBox.Invoke($config.NoResultsMessage, $config.NoResultsTitle, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information) return $null } # Transform data using entity-specific transformer $displayItems = $items | ForEach-Object { & $config.DataTransform $_ } | Sort-Object DisplayName # Show selector using the universal selection window $selectedItems = Show-UISelectionWindow ` -Title $config.Title ` -SearchPlaceholder $config.SearchPlaceholder ` -Items $displayItems ` -ColumnConfig $config.ColumnConfig ` -SearchProperty $config.SearchProperty ` -DisplayOrder $config.ColumnConfig.Keys ` -AllowMultiple:$config.AllowMultiple return $selectedItems } else { Write-DebugOutput -Message "Graph query failed for entity type: $GraphEntityType, error: $($result.Error)" -Source $MyInvocation.MyCommand -Level "Error" $syncHash.ShowMessageBox.Invoke($result.Message, $syncHash.UIConfigs.localeTitles.Error, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) return $null } } catch { $syncHash.ShowMessageBox.Invoke(("{0} {1}: {2}" -f $syncHash.UIConfigs.localeErrorMessages.WindowError,$GraphEntityType, $_.Exception.Message), $syncHash.UIConfigs.localeTitles.Error, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) return $null } } Function Show-GraphSelector { <# .SYNOPSIS Shows a Graph entity selector with optional search Functionality. .DESCRIPTION This Function displays a selector interface for Microsoft Graph entities (users, groups) with optional search term filtering and result limiting. #> param( [string]$GraphEntityType, [string]$SearchTerm = "", [int]$Top = 100 ) If($syncHash.GraphConnected){ If([string]::IsNullOrWhiteSpace($SearchTerm)) { Write-DebugOutput -Message "Showing $($GraphEntityType.ToLower()) selector with top: $Top" -Source $MyInvocation.MyCommand -Level "Info" }Else { Write-DebugOutput -Message "Showing $($GraphEntityType.ToLower()) selector with search term: $SearchTerm, top: $Top" -Source $MyInvocation.MyCommand -Level "Info" } return Show-GraphProgressWindow -GraphEntityType $GraphEntityType -SearchTerm $SearchTerm -Top $Top }Else{ Write-DebugOutput -Message "Microsoft Graph is not connected" -Source $MyInvocation.MyCommand -Level "Warning" # Show an error message to the user $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.GraphConnectionError, $syncHash.UIConfigs.localeTitles.ConnectionError, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) } } #build UI selection window Function Show-UISelectionWindow { <# .SYNOPSIS Creates a universal selection window with search and filtering capabilities. .DESCRIPTION This Function generates a reusable selection dialog with a searchable data grid, supporting single or multiple selection modes for various data types. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "SearchProperty")] param( [Parameter(Mandatory)] [string]$Title, [Parameter(Mandatory)] [string]$SearchPlaceholder, [Parameter(Mandatory)] [array]$Items, [Parameter(Mandatory)] [hashtable]$ColumnConfig, [Parameter()] [string[]]$DisplayOrder, [Parameter()] [string]$SearchProperty = "DisplayName", [Parameter()] [int]$WindowWidth = 1000, [Parameter()] [string]$ReturnProperty, [Parameter()] [switch]$AllowMultiple ) #[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | out-null #[System.Reflection.Assembly]::LoadWithPartialName('System.Security') | out-null try { # Create selection window $selectionWindow = New-Object System.Windows.Window $selectionWindow.Title = $Title $selectionWindow.Width = $WindowWidth $selectionWindow.Height = 500 $selectionWindow.WindowStartupLocation = "CenterOwner" $selectionWindow.Owner = $syncHash.Window $selectionWindow.Background = [System.Windows.Media.Brushes]::White $selectionWindow.Icon = $syncHash.ImgPath # Create main grid $mainGrid = New-Object System.Windows.Controls.Grid $rowDef1 = New-Object System.Windows.Controls.RowDefinition $rowDef1.Height = [System.Windows.GridLength]::Auto $rowDef2 = New-Object System.Windows.Controls.RowDefinition $rowDef2.Height = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) $rowDef3 = New-Object System.Windows.Controls.RowDefinition $rowDef3.Height = [System.Windows.GridLength]::Auto [void]$mainGrid.RowDefinitions.Add($rowDef1) [void]$mainGrid.RowDefinitions.Add($rowDef2) [void]$mainGrid.RowDefinitions.Add($rowDef3) # Search panel $searchPanel = New-Object System.Windows.Controls.StackPanel $searchPanel.Orientation = [System.Windows.Controls.Orientation]::Horizontal $searchPanel.Margin = "10" $searchLabel = New-Object System.Windows.Controls.Label $searchLabel.Content = "Search:" $searchLabel.Width = 60 $searchLabel.VerticalAlignment = [System.Windows.VerticalAlignment]::Center $searchBox = New-Object System.Windows.Controls.TextBox $searchBox.Width = 300 $searchBox.Height = 25 $searchBox.Text = $SearchPlaceholder $searchBox.Foreground = [System.Windows.Media.Brushes]::Gray $searchBox.FontStyle = [System.Windows.FontStyles]::Italic $searchBox.Margin = "5,0" # Search box placeholder Functionality $searchBox.Add_GotFocus({ if ($searchBox.Text -eq $SearchPlaceholder) { $searchBox.Text = "" $searchBox.Foreground = [System.Windows.Media.Brushes]::Black $searchBox.FontStyle = [System.Windows.FontStyles]::Normal } }) $searchBox.Add_LostFocus({ if ([string]::IsNullOrWhiteSpace($searchBox.Text)) { $searchBox.Text = $SearchPlaceholder $searchBox.Foreground = [System.Windows.Media.Brushes]::Gray $searchBox.FontStyle = [System.Windows.FontStyles]::Italic } }) [void]$searchPanel.Children.Add($searchLabel) [void]$searchPanel.Children.Add($searchBox) [System.Windows.Controls.Grid]::SetRow($searchPanel, 0) [void]$mainGrid.Children.Add($searchPanel) # Create DataGrid $dataGrid = New-Object System.Windows.Controls.DataGrid $dataGrid.AutoGenerateColumns = $false $dataGrid.CanUserAddRows = $false $dataGrid.CanUserDeleteRows = $false $dataGrid.IsReadOnly = $true $dataGrid.SelectionMode = if ($AllowMultiple) { [System.Windows.Controls.DataGridSelectionMode]::Extended } else { [System.Windows.Controls.DataGridSelectionMode]::Single } $dataGrid.GridLinesVisibility = [System.Windows.Controls.DataGridGridLinesVisibility]::Horizontal $dataGrid.HeadersVisibility = [System.Windows.Controls.DataGridHeadersVisibility]::Column $dataGrid.Margin = "10" # Display order handling if specified if ($DisplayOrder -and $DisplayOrder.Count -gt 0) { $keyOrder = $DisplayOrder } else { # Use keys from ColumnConfig if no display order specified $keyOrder = $ColumnConfig.Keys | Sort-Object } # Create columns based on ColumnConfig foreach ($columnKey in $keyOrder) { if ($ColumnConfig.Contains($columnKey)) { $column = New-Object System.Windows.Controls.DataGridTextColumn $column.Header = $ColumnConfig[$columnKey].Header $column.Binding = New-Object System.Windows.Data.Binding($columnKey) $column.Width = $ColumnConfig[$columnKey].Width $dataGrid.Columns.Add($column) } else { Write-DebugOutput -Message "Column configuration for '$columnKey' not found in ColumnConfig." -Source $MyInvocation.MyCommand -Level "Error" } } # Store original items for filtering $originalItems = $Items # Filter Function $FilterItems = { $searchText = $searchBox.Text.ToLower() if ([string]::IsNullOrWhiteSpace($searchText) -or $searchText -eq $SearchPlaceholder.ToLower()) { $dataGrid.ItemsSource = $originalItems } else { $filteredItems = @($originalItems | Where-Object { $_.$SearchProperty.ToLower().Contains($searchText) }) $dataGrid.ItemsSource = $filteredItems } } # Initial load $dataGrid.ItemsSource = $originalItems # Search on text change $searchBox.Add_TextChanged($FilterItems) [System.Windows.Controls.Grid]::SetRow($dataGrid, 1) [void]$mainGrid.Children.Add($dataGrid) # Create button panel $buttonPanel = New-Object System.Windows.Controls.StackPanel $buttonPanel.Orientation = [System.Windows.Controls.Orientation]::Horizontal $buttonPanel.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right $buttonPanel.Margin = "10" $selectButton = New-Object System.Windows.Controls.Button $selectButton.Content = "Select" $selectButton.Width = 80 $selectButton.Height = 30 $selectButton.Margin = "0,0,10,0" $selectButton.IsDefault = $true $cancelButton = New-Object System.Windows.Controls.Button $cancelButton.Content = "Cancel" $cancelButton.Width = 80 $cancelButton.Height = 30 $cancelButton.IsCancel = $true [void]$buttonPanel.Children.Add($selectButton) [void]$buttonPanel.Children.Add($cancelButton) [System.Windows.Controls.Grid]::SetRow($buttonPanel, 2) [void]$mainGrid.Children.Add($buttonPanel) $selectionWindow.Content = $mainGrid # Event handlers $selectButton.Add_Click({ if ($dataGrid.SelectedItems) { $selectedResults = @() foreach ($selectedItem in $dataGrid.SelectedItems) { $selectedResults += $selectedItem } $selectionWindow.Tag = $selectedResults $selectionWindow.DialogResult = $true $selectionWindow.Close() } else { $syncHash.ShowMessageBox.Invoke($syncHash.UIConfigs.localeErrorMessages.PleaseSelectItem, $syncHash.UIConfigs.localeTitles.NoSelection, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning) } }) $cancelButton.Add_Click({ $selectionWindow.DialogResult = $false $selectionWindow.Close() }) $dataGrid.Add_MouseDoubleClick({ if ($dataGrid.SelectedItem) { $selectionWindow.Tag = @($dataGrid.SelectedItem) $selectionWindow.DialogResult = $true $selectionWindow.Close() } }) # Show dialog $result = $selectionWindow.ShowDialog() if ($result -eq $true) { If($ReturnProperty) { # Return the specified property from the selected items $returnValues = @() foreach ($item in $selectionWindow.Tag) { if ($item -is [PSCustomObject] -and $item.PSObject.Properties[$ReturnProperty]) { $returnValues += $item.$ReturnProperty } else { Write-DebugOutput -Message "Selected item does not have property '$ReturnProperty': $($item | ConvertTo-Json -Compress)" -Source $MyInvocation.MyCommand -Level "Error" } } return $returnValues }Else{ return $selectionWindow.Tag } } return $null } catch { $syncHash.ShowMessageBox.Invoke( ("{0} {1}: {2}" -f $syncHash.UIConfigs.localeErrorMessages.WindowError, $Title, $_.Exception.Message), $syncHash.UIConfigs.localeTitles.Error, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error) return $null } } Function Add-GraphButton { <# .SYNOPSIS Dynamically adds Graph query buttons to TextBoxes based on graphQueries configuration. .DESCRIPTION This function scans all graphQueries configurations and automatically adds "Get" buttons next to any TextBox controls that have matching names when Graph is connected. #> # Get all available graph query configurations $graphQueryConfigs = $syncHash.UIConfigs.graphQueries.PSObject.Properties foreach ($queryConfig in $graphQueryConfigs) { $textBoxName = $queryConfig.Name $graphQueryData = $queryConfig.Value # Check if corresponding TextBox exists in syncHash if (-not $syncHash.$textBoxName) { Write-DebugOutput -Message "TextBox '$textBoxName' not found in syncHash - skipping" -Source $MyInvocation.MyCommand -Level "Verbose" continue } $textBox = $syncHash.$textBoxName # Verify it's actually a TextBox if ($textBox.GetType().Name -ne "TextBox") { Write-DebugOutput -Message "Control '$textBoxName' is not a TextBox - skipping" -Source $MyInvocation.MyCommand -Level "Verbose" continue } # Add the Graph button to this TextBox Add-GraphButtonToTextBox -TextBox $textBox -TextBoxName $textBoxName -GraphQueryData $graphQueryData } } Function Add-GraphButtonToTextBox { <# .SYNOPSIS Adds a Graph query button to a specific TextBox. .DESCRIPTION Creates and inserts a Graph query button next to the specified TextBox using the provided configuration. #> param( [Parameter(Mandatory)] [System.Windows.Controls.TextBox]$TextBox, [Parameter(Mandatory)] [string]$TextBoxName, [Parameter(Mandatory)] [PSObject]$GraphQueryData ) # Find the parent container (should be a StackPanel or Grid) $parentContainer = $TextBox.Parent if (-not $parentContainer) { Write-DebugOutput -Message "TextBox '$TextBoxName' has no parent container" -Source $MyInvocation.MyCommand -Level "Error" return } # Check if button already exists to prevent duplicates $buttonName = "Get$($TextBoxName.Replace('_TextBox', ''))Button" $existingButton = $null if ($parentContainer.GetType().Name -eq "StackPanel") { $existingButton = $parentContainer.Children | Where-Object { $_.GetType().Name -eq "Button" -and $_.Name -eq $buttonName } } elseif ($parentContainer.GetType().Name -eq "Grid") { $existingButton = $parentContainer.Children | Where-Object { $_.GetType().Name -eq "Button" -and $_.Name -eq $buttonName } } if ($existingButton) { Write-DebugOutput -Message "Graph button '$buttonName' already exists for '$TextBoxName'" -Source $MyInvocation.MyCommand -Level "Verbose" return } # Create the Graph query button $graphButton = New-Object System.Windows.Controls.Button $graphButton.Content = "Get $($GraphQueryData.name)" $graphButton.Name = $buttonName $graphButton.Width = 100 $graphButton.Height = 28 $graphButton.Margin = "8,0,0,0" $graphButton.Style = $syncHash.Window.FindResource("SecondaryButton") $graphButton.ToolTip = "Select $($GraphQueryData.name.ToLower()) from Microsoft Graph" # Add global event handlers Add-UIControlEventHandler -Control $graphButton # Create the click event handler $graphButton.Add_Click({ try { # Get search term from TextBox if it has content (excluding placeholder text) $searchTerm = "" $textBoxValue = $TextBox.Text $placeholderKey = "$TextBoxName" $placeholderText = $syncHash.UIConfigs.localePlaceholder.$placeholderKey if (![string]::IsNullOrWhiteSpace($textBoxValue) -and $textBoxValue -ne $placeholderText) { $searchTerm = $textBoxValue } Write-DebugOutput -Message "Opening Graph selector for $TextBoxName with search term: '$searchTerm'" -Source $MyInvocation.MyCommand -Level "Info" # Show the entity selector $selectedItems = Show-GraphSelector -GraphEntityType $TextBoxName -SearchTerm $searchTerm if ($null -ne $selectedItems) { # Get the first selected item $selectedItem = $selectedItems # Set the value in the textbox using the configured output property if ($selectedItem.($GraphQueryData.outProperty)) { $TextBox.Text = $selectedItem.($GraphQueryData.outProperty) $TextBox.Foreground = [System.Windows.Media.Brushes]::Black $TextBox.FontStyle = [System.Windows.FontStyles]::Normal # Get display name for logging/feedback $displayName = if ($GraphQueryData.tipProperty -and $selectedItem.($GraphQueryData.tipProperty)) { $selectedItem.($GraphQueryData.tipProperty) } else { $selectedItem.($GraphQueryData.outProperty) } Write-DebugOutput -Message "Selected $($GraphQueryData.name.ToLower()): $displayName with value: $($selectedItem.($GraphQueryData.outProperty))" -Source $MyInvocation.MyCommand -Level "Info" # Show success message $syncHash.ShowMessageBox.Invoke( "$($GraphQueryData.name) selected: $displayName", "$($GraphQueryData.name) Selected", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information ) } else { Write-DebugOutput -Message "Selected $($GraphQueryData.name.ToLower()) missing required property: $($GraphQueryData.outProperty)" -Source $MyInvocation.MyCommand -Level "Error" $syncHash.ShowMessageBox.Invoke( "Selected item is missing required data property.", "Invalid Selection", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning ) } } else { Write-DebugOutput -Message "No $($GraphQueryData.name.ToLower()) selected from Graph query" -Source $MyInvocation.MyCommand -Level "Info" } } catch { Write-DebugOutput -Message "Error in Dynamic Graph button click for $($GraphQueryData.name): $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" $syncHash.ShowMessageBox.Invoke( ($syncHash.UIConfigs.localePopupMessages.GraphError -f $_.Exception.Message), $syncHash.UIConfigs.localeTitles.GraphError, [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error ) } }.GetNewClosure()) # Add the button to the parent container try { if ($parentContainer.GetType().Name -eq "StackPanel") { # For StackPanel (horizontal layout), just add to children [void]$parentContainer.Children.Add($graphButton) } elseif ($parentContainer.GetType().Name -eq "Grid") { # For Grid, we need to be more careful about positioning # Try to place it in the same row as the TextBox, next column $textBoxRow = [System.Windows.Controls.Grid]::GetRow($TextBox) $textBoxColumn = [System.Windows.Controls.Grid]::GetColumn($TextBox) [System.Windows.Controls.Grid]::SetRow($graphButton, $textBoxRow) [System.Windows.Controls.Grid]::SetColumn($graphButton, $textBoxColumn + 1) [void]$parentContainer.Children.Add($graphButton) } else { Write-DebugOutput -Message "Unsupported parent container type: $($parentContainer.GetType().Name) for TextBox '$TextBoxName'" -Source $MyInvocation.MyCommand -Level "Error" return } Write-DebugOutput -Message "Added Graph button '$buttonName' for TextBox '$TextBoxName'" -Source $MyInvocation.MyCommand -Level "Info" } catch { Write-DebugOutput -Message "Failed to add Graph button to container for '$TextBoxName': $($_.Exception.Message)" -Source $MyInvocation.MyCommand -Level "Error" } } |