diff --git a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml index 38bdec0..ce477f1 100644 --- a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml +++ b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml @@ -2,8 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="PlanViewer.App.Dialogs.ConnectionDialog" Title="Connect to Server" - Width="480" Height="520" - MinWidth="400" MinHeight="480" + Width="480" Height="588" + MinWidth="400" MinHeight="548" WindowStartupLocation="CenterOwner" CanResize="False" Icon="avares://PlanViewer.App/EDD.ico" @@ -85,6 +85,15 @@ Foreground="{DynamicResource ForegroundBrush}" ToolTip.Tip="Sets ApplicationIntent=ReadOnly. Required when connecting via an AG listener or Azure failover group endpoint to route to a readable secondary (including Query Store on secondary replicas)." FontSize="12"/> + + + + + + diff --git a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs index 5f32da3..cf0c93f 100644 --- a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs +++ b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs @@ -78,6 +78,7 @@ private void ApplySavedConnection(ServerConnection saved) TrustCertBox.IsChecked = saved.TrustServerCertificate; ReadOnlyIntentCheckBox.IsChecked = saved.ApplicationIntentReadOnly; + DatabaseInputBox.Text = saved.DatabaseName ?? ""; // Load stored credentials var cred = _credentialService.GetCredential(saved.Id); @@ -125,6 +126,11 @@ private async void TestConnection_Click(object? sender, RoutedEventArgs e) return; } + // For Azure SQL DB / JIT access the login often can't open master, so connect + // through the database the user named (if any) instead of the hardcoded master. + var typedDatabase = DatabaseInputBox.Text?.Trim(); + var connectDatabase = string.IsNullOrEmpty(typedDatabase) ? "master" : typedDatabase; + StatusText.Text = "Connecting..."; StatusText.Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(0xE4, 0xE6, 0xEB)); TestButton.IsEnabled = false; @@ -135,12 +141,13 @@ private async void TestConnection_Click(object? sender, RoutedEventArgs e) var connectionString = connection.GetConnectionString( LoginBox.Text?.Trim(), PasswordBox.Text, - "master"); + connectDatabase); await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(); - // Fetch databases + // Fetch databases the login can see. On Azure SQL DB connected to a single user + // database this returns master + that database, which is expected. var databases = new List(); using var cmd = new SqlCommand( "SELECT name FROM sys.databases WHERE state_desc = 'ONLINE' ORDER BY name", conn); @@ -148,13 +155,21 @@ private async void TestConnection_Click(object? sender, RoutedEventArgs e) while (await reader.ReadAsync()) databases.Add(reader.GetString(0)); + // The named database is reachable (OpenAsync succeeded), so make sure it's + // selectable even if enumeration didn't surface it (restricted JIT permissions). + if (!string.IsNullOrEmpty(typedDatabase) && + !databases.Contains(typedDatabase, StringComparer.OrdinalIgnoreCase)) + databases.Insert(0, typedDatabase); + DatabaseBox.ItemsSource = databases; DatabaseBox.IsEnabled = true; ConnectButton.IsEnabled = true; - // Default to master if available - var masterIdx = databases.IndexOf("master"); - if (masterIdx >= 0) DatabaseBox.SelectedIndex = masterIdx; + // Pre-select the named database when given, otherwise default to master. + var preferred = string.IsNullOrEmpty(typedDatabase) ? "master" : typedDatabase; + var preferredIdx = databases.FindIndex(d => d.Equals(preferred, StringComparison.OrdinalIgnoreCase)); + if (preferredIdx >= 0) DatabaseBox.SelectedIndex = preferredIdx; + else if (databases.Count > 0) DatabaseBox.SelectedIndex = 0; StatusText.Text = $"Connected ({databases.Count} databases)"; StatusText.Foreground = Avalonia.Media.Brushes.LimeGreen; @@ -213,11 +228,13 @@ private void Cancel_Click(object? sender, RoutedEventArgs e) private ServerConnection BuildServerConnection() { var serverName = ServerNameBox.Text?.Trim() ?? ""; + var databaseName = DatabaseInputBox.Text?.Trim(); return new ServerConnection { Id = serverName, ServerName = serverName, DisplayName = serverName, + DatabaseName = string.IsNullOrEmpty(databaseName) ? null : databaseName, AuthenticationType = GetSelectedAuthType(), TrustServerCertificate = TrustCertBox.IsChecked == true, EncryptMode = GetSelectedEncryptMode(), diff --git a/src/PlanViewer.Core/Models/ServerConnection.cs b/src/PlanViewer.Core/Models/ServerConnection.cs index 9bf6877..16f1487 100644 --- a/src/PlanViewer.Core/Models/ServerConnection.cs +++ b/src/PlanViewer.Core/Models/ServerConnection.cs @@ -18,6 +18,13 @@ public class ServerConnection public bool TrustServerCertificate { get; set; } = false; public bool ApplicationIntentReadOnly { get; set; } = false; + /// + /// Optional database name for the initial connection. Use for Azure SQL Database or + /// Just-In-Time access where the login can't open master. + /// Leave empty for on-premises SQL Server (defaults to master). + /// + public string? DatabaseName { get; set; } + [JsonIgnore] public string AuthenticationDisplay => AuthenticationType switch {