Jun 2, 2026, 3:54 PM

This commit is contained in:
Paweł Domański
2026-06-02 13:54:37 +00:00
parent 3d99d13a2c
commit 04b2b6eeb7
3 changed files with 255 additions and 19 deletions
+168 -3
View File
@@ -10,7 +10,25 @@ $configPath = Join-Path $PSScriptRoot "Config\settings.json"
if (Test-Path $configPath) { if (Test-Path $configPath) {
$settings = Get-Content $configPath | ConvertFrom-Json $settings = Get-Content $configPath | ConvertFrom-Json
} else { } else {
$settings = [pscustomobject]@{ LastServer = ""; Theme = "Light" } $settings = [pscustomobject]@{ LastServer = ""; Theme = "Light"; Servers = @("localhost") }
}
# Ensure Servers is a collection and initialized
if ($null -eq $settings.Servers) {
if ($settings -is [System.Management.Automation.PSCustomObject]) {
$props = [ordered]@{
LastServer = $settings.LastServer
Theme = $settings.Theme
Servers = @("localhost")
}
$settings = [pscustomobject]$props
} else {
$settings = [pscustomobject]@{ LastServer = ""; Theme = "Light"; Servers = @("localhost") }
}
}
if ($settings.Servers -is [string]) {
$settings.Servers = @($settings.Servers)
} }
# 1. Path to XAML # 1. Path to XAML
@@ -26,6 +44,9 @@ $btnConnect = $Window.FindName("btnConnect")
$txtServerName = $Window.FindName("txtServerName") $txtServerName = $Window.FindName("txtServerName")
$txtLogs = $Window.FindName("txtLogs") $txtLogs = $Window.FindName("txtLogs")
$dgServerInfo = $Window.FindName("dgServerInfo") $dgServerInfo = $Window.FindName("dgServerInfo")
$tvServers = $Window.FindName("tvServers")
$btnAddServer = $Window.FindName("btnAddServer")
$btnRemoveServer = $Window.FindName("btnRemoveServer")
# Set initial values from settings # Set initial values from settings
$txtServerName.Text = $settings.LastServer $txtServerName.Text = $settings.LastServer
@@ -40,6 +61,150 @@ function Write-Log {
}) })
} }
# 4.1. TreeView population and refresh
function Refresh-ServerTree {
$tvServers.Dispatcher.Invoke({
$tvServers.Items.Clear()
$rootNode = New-Object System.Windows.Controls.TreeViewItem
$rootNode.Header = "SQL Servers"
$rootNode.IsExpanded = $true
$rootNode.Tag = "__ROOT__"
foreach ($server in $settings.Servers) {
$serverNode = New-Object System.Windows.Controls.TreeViewItem
$serverNode.Header = $server
$serverNode.Tag = $server
# Add a dummy loading node so the expansion arrow is displayed
$dummyNode = New-Object System.Windows.Controls.TreeViewItem
$dummyNode.Header = "Loading..."
$serverNode.Items.Add($dummyNode)
# Event handler when a server node is expanded
$serverNode.Add_Expanded({
param($sender, $e)
# Prevent this handler from running for nested child expansions (bubbling)
if ($e.OriginalSource -ne $sender) { return }
$node = $sender
if ($node.Items.Count -eq 1 -and $node.Items[0].Header -eq "Loading...") {
$node.Items.Clear()
$srvName = $node.Tag
Write-Log "Fetching databases for $srvName..."
# Force UI to process the pending write-log before connection blocks the thread
[System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke(
[System.Windows.Threading.DispatcherPriority]::Background,
[System.Action]{}
)
try {
$dbs = Get-SQLDatabases -ServerInstance $srvName
if ($dbs -and $dbs.Count -gt 0) {
foreach ($db in $dbs) {
$dbNode = New-Object System.Windows.Controls.TreeViewItem
$dbNode.Header = "$($db.Name) ($($db.Status))"
$dbNode.Tag = $db
$node.Items.Add($dbNode)
}
Write-Log "Successfully loaded $($dbs.Count) databases for $srvName."
} else {
$noDbNode = New-Object System.Windows.Controls.TreeViewItem
$noDbNode.Header = "(No databases found)"
$node.Items.Add($noDbNode)
Write-Log "No databases found on $srvName."
}
} catch {
Write-Log "Error fetching databases: $($_.Exception.Message)"
$errNode = New-Object System.Windows.Controls.TreeViewItem
$errNode.Header = "(Error loading databases)"
$node.Items.Add($errNode)
}
}
})
$rootNode.Items.Add($serverNode)
}
$tvServers.Items.Add($rootNode)
})
}
# Handle TreeView selection
$tvServers.Add_SelectedItemChanged({
param($sender, $e)
$selectedItem = $tvServers.SelectedItem
if ($null -ne $selectedItem -and $null -ne $selectedItem.Tag) {
if ($selectedItem.Tag -is [string]) {
if ($selectedItem.Tag -eq "__ROOT__") { return }
# It's a server node, copy its name to TextBox
$txtServerName.Text = $selectedItem.Tag
Write-Log "Selected server: $($selectedItem.Tag)"
} elseif ($selectedItem.Tag -is [System.Management.Automation.PSCustomObject]) {
# It's a database node
$db = $selectedItem.Tag
Write-Log "Selected database: $($db.Name) (Size: $($db.Size), Status: $($db.Status))"
}
}
})
# Handle Add Server button
$btnAddServer.Add_Click({
$server = ""
try {
Add-Type -AssemblyName Microsoft.VisualBasic
$server = [Microsoft.VisualBasic.Interaction]::InputBox("Enter SQL Server Instance Name (e.g., localhost, srv\SQLEXPRESS):", "Add Server", "")
} catch {
# Fallback using current textbox value
$server = $txtServerName.Text
if ([string]::IsNullOrWhiteSpace($server)) {
[System.Windows.MessageBox]::Show("Visual Basic assembly not found. Please type the server name in 'Server Name' textbox first, and then click 'Add Server'.", "Add Server Fallback", "OK", "Information")
return
}
}
if ([string]::IsNullOrWhiteSpace($server)) { return }
$server = $server.Trim()
if ($settings.Servers -contains $server) {
[System.Windows.MessageBox]::Show("Server '$server' is already in the list.", "Server Already Exists", "OK", "Information")
return
}
# Save to settings
$settings.Servers = $settings.Servers + $server
$settings | ConvertTo-Json | Set-Content $configPath
Refresh-ServerTree
Write-Log "Successfully added server to list: $server"
})
# Handle Remove Server button
$btnRemoveServer.Add_Click({
$selectedItem = $tvServers.SelectedItem
if ($null -eq $selectedItem -or $selectedItem.Tag -eq "__ROOT__") {
[System.Windows.MessageBox]::Show("Please select a server node in the tree to remove.", "Select Server", "OK", "Warning")
return
}
$serverName = $selectedItem.Tag
# Verify that we selected a server node (it has a string Tag and is in settings.Servers)
if ($serverName -is [string] -and ($settings.Servers -contains $serverName)) {
$confirm = [System.Windows.MessageBox]::Show("Are you sure you want to remove '$serverName' from the list?", "Confirm Server Removal", "YesNo", "Question")
if ($confirm -eq "Yes") {
$settings.Servers = $settings.Servers | Where-Object { $_ -ne $serverName }
$settings | ConvertTo-Json | Set-Content $configPath
Refresh-ServerTree
Write-Log "Removed server from list: $serverName"
}
} else {
[System.Windows.MessageBox]::Show("Only server nodes can be removed.", "Invalid Selection", "OK", "Warning")
}
})
# 5. Connection logic # 5. Connection logic
$btnConnect.Add_Click({ $btnConnect.Add_Click({
$server = $txtServerName.Text $server = $txtServerName.Text
@@ -71,13 +236,13 @@ $btnConnect.Add_Click({
} }
}) })
# Startup Dependency Check # Startup Dependency Check and Tree Initialization
try { try {
Import-DBAToolBoxDependencies Import-DBAToolBoxDependencies
Write-Log "DBAToolBox started. dbatools module loaded." Write-Log "DBAToolBox started. dbatools module loaded."
Refresh-ServerTree
} catch { } catch {
[System.Windows.MessageBox]::Show($_.Exception.Message, "Dependency Error", "OK", "Error") [System.Windows.MessageBox]::Show($_.Exception.Message, "Dependency Error", "OK", "Error")
# In some environments ShowDialog might fail if no UI thread, but for WPF it should work.
$Window.Close() $Window.Close()
} }
+29 -1
View File
@@ -44,4 +44,32 @@ function Test-SQLConnection {
} }
} }
Export-ModuleMember -Function Import-DBAToolBoxDependencies, Test-SQLConnection function Get-SQLDatabases {
<#
.SYNOPSIS
Gets database information for a SQL Server instance.
#>
param(
[Parameter(Mandatory=$true)]
[string]$ServerInstance
)
try {
# Using Get-DbaDatabase from dbatools
$databases = Get-DbaDatabase -SqlInstance $ServerInstance -ErrorAction Stop
$dbList = @()
foreach ($db in $databases) {
$dbList += [pscustomobject]@{
Name = $db.Name
Status = $db.Status
Size = $db.Size
RecoveryModel = $db.RecoveryModel
}
}
return $dbList
} catch {
throw "Failed to get databases: $($_.Exception.Message)"
}
}
Export-ModuleMember -Function Import-DBAToolBoxDependencies, Test-SQLConnection, Get-SQLDatabases
+58 -15
View File
@@ -1,30 +1,73 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DBAToolBox - MS SQL Management" Height="450" Width="600" Background="#F0F0F0"> Title="DBAToolBox - MS SQL Management" Height="550" Width="850" Background="#F0F0F0">
<Grid Margin="10"> <Grid Margin="10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Header --> <!-- Header -->
<TextBlock Text="DBAToolBox" FontSize="24" FontWeight="Bold" Foreground="#2D3E50" Margin="0,0,0,10"/> <TextBlock Text="DBAToolBox" FontSize="24" FontWeight="Bold" Foreground="#2D3E50" Margin="0,0,0,10"/>
<!-- Connection Panel --> <!-- Main Workspace divided into Left (Tree View) and Right (Details) -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10"> <Grid Grid.Row="1">
<Label Content="Server Name:" VerticalAlignment="Center"/> <Grid.ColumnDefinitions>
<TextBox x:Name="txtServerName" Width="250" VerticalAlignment="Center" Margin="5,0,10,0" Padding="3"/> <ColumnDefinition Width="220" MinWidth="180"/>
<Button x:Name="btnConnect" Content="Connect &amp; Test" Width="120" Padding="5" Background="#007ACC" Foreground="White" BorderThickness="0"/> <ColumnDefinition Width="5"/>
</StackPanel> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Main Content Area --> <!-- Left Panel: SQL Servers Tree -->
<GroupBox Grid.Row="2" Header="Server Information" Margin="0,0,0,10"> <GroupBox Header="SQL Server Directory" Grid.Column="0" Margin="0,0,5,0" Foreground="#2D3E50" FontWeight="SemiBold">
<DataGrid x:Name="dgServerInfo" AutoGenerateColumns="True" IsReadOnly="True" Background="White"/> <Grid Margin="5">
</GroupBox> <Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Log Area --> <TreeView x:Name="tvServers" Margin="0,0,0,5" Background="White" BorderBrush="#CCCCCC" BorderThickness="1">
<TextBox Grid.Row="3" x:Name="txtLogs" Height="100" IsReadOnly="True" VerticalScrollBarVisibility="Auto" Background="#E8E8E8" FontFamily="Consolas" FontSize="11" TextWrapping="Wrap"/> <!-- Items will be populated dynamically from PowerShell -->
</TreeView>
<!-- Buttons to manage servers list -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="btnAddServer" Content="Add Server" Grid.Column="0" Margin="0,0,3,0" Padding="5,4" Background="#007ACC" Foreground="White" BorderThickness="0" FontWeight="Normal" Cursor="Hand"/>
<Button x:Name="btnRemoveServer" Content="Remove" Grid.Column="1" Margin="3,0,0,0" Padding="5,4" Background="#D13438" Foreground="White" BorderThickness="0" FontWeight="Normal" Cursor="Hand"/>
</Grid>
</Grid>
</GroupBox>
<!-- GridSplitter to resize columns -->
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Background="#D8D8D8" ResizeBehavior="PreviousAndNext" Cursor="SizeWE"/>
<!-- Right Panel: Connection and Main Workspace -->
<Grid Grid.Column="2" Margin="10,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Connection Panel -->
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<Label Content="Server Name:" VerticalAlignment="Center" FontWeight="SemiBold"/>
<TextBox x:Name="txtServerName" Width="250" VerticalAlignment="Center" Margin="5,0,10,0" Padding="4"/>
<Button x:Name="btnConnect" Content="Connect &amp; Test" Width="120" Padding="5" Background="#007ACC" Foreground="White" BorderThickness="0" Cursor="Hand"/>
</StackPanel>
<!-- Main Content Area -->
<GroupBox Grid.Row="1" Header="Server Information" Margin="0,0,0,10" Foreground="#2D3E50" FontWeight="SemiBold">
<DataGrid x:Name="dgServerInfo" AutoGenerateColumns="True" IsReadOnly="True" Background="White" BorderBrush="#CCCCCC" Margin="5"/>
</GroupBox>
<!-- Log Area -->
<TextBox Grid.Row="2" x:Name="txtLogs" Height="120" IsReadOnly="True" VerticalScrollBarVisibility="Auto" Background="#E8E8E8" FontFamily="Consolas" FontSize="11" TextWrapping="Wrap" BorderBrush="#CCCCCC" Padding="5"/>
</Grid>
</Grid>
</Grid> </Grid>
</Window> </Window>