From db663cc82d5c8fe528f4659be619433ac6d6e2e9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Jan 2026 11:12:08 -0500 Subject: [PATCH] feat(configmanager): add ConnectionStrings editor with test connection support Adds a new ConnectionStrings section to ConfigManager allowing users to manage database connection strings with provider selection, connection testing, and visual feedback for connection state. --- .../JdeScoping.ConfigManager/App.axaml.cs | 3 + .../ProviderToVisibilityConverter.cs | 57 +++ .../JdeScoping.ConfigManager.csproj | 1 + .../Models/ConfigModel.cs | 2 +- .../Models/ConnectionProvider.cs | 11 + .../Models/ConnectionStringEntry.cs | 86 ++++ .../Models/ConnectionStringsSection.cs | 9 + .../Services/ConnectionTestService.cs | 97 ++++ .../Services/IConnectionTestService.cs | 21 + .../Forms/ConnectionStringEntryViewModel.cs | 325 ++++++++++++++ .../Forms/ConnectionStringsFormViewModel.cs | 260 +++++++++++ .../ViewModels/MainWindowViewModel.cs | 10 + .../Forms/ConnectionStringsFormView.axaml | 421 ++++++++++++++++++ .../Forms/ConnectionStringsFormView.axaml.cs | 11 + .../Models/ConnectionStringEntryTests.cs | 122 +++++ .../ConnectionStringEntryViewModelTests.cs | 182 ++++++++ .../ConnectionStringsFormViewModelTests.cs | 167 +++++++ .../ViewModels/MainWindowViewModelTests.cs | 5 +- PLANS/ConnectionStringsEditor-Design.md | 299 +++++++++++++ .../ConnectionStringsEditor-Implementation.md | 421 ++++++++++++++++++ 20 files changed, 2508 insertions(+), 2 deletions(-) create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Converters/ProviderToVisibilityConverter.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionProvider.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSection.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Services/ConnectionTestService.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Services/IConnectionTestService.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs create mode 100644 PLANS/ConnectionStringsEditor-Design.md create mode 100644 PLANS/ConnectionStringsEditor-Implementation.md diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs b/NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs index 063373a..36dbacb 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs @@ -74,6 +74,9 @@ public partial class App : Avalonia.Application // Runtime Validation Services services.AddSingleton(); + // Connection Testing + services.AddSingleton(); + // ViewModels services.AddTransient(); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Converters/ProviderToVisibilityConverter.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Converters/ProviderToVisibilityConverter.cs new file mode 100644 index 0000000..5d0d44b --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Converters/ProviderToVisibilityConverter.cs @@ -0,0 +1,57 @@ +using System.Globalization; +using Avalonia.Data.Converters; +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Converters; + +/// +/// Converts a ConnectionProvider value to visibility (bool) based on whether it matches the target provider. +/// +public class ProviderToVisibilityConverter : IValueConverter +{ + /// + /// Converter instance for SqlServer provider visibility. + /// + public static readonly ProviderToVisibilityConverter SqlServer = new(ConnectionProvider.SqlServer); + + /// + /// Converter instance for Oracle provider visibility. + /// + public static readonly ProviderToVisibilityConverter Oracle = new(ConnectionProvider.Oracle); + + /// + /// Converter instance for Generic provider visibility. + /// + public static readonly ProviderToVisibilityConverter Generic = new(ConnectionProvider.Generic); + + private readonly ConnectionProvider _targetProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The provider to match for visibility. + public ProviderToVisibilityConverter(ConnectionProvider targetProvider) + { + _targetProvider = targetProvider; + } + + /// + /// Converts a ConnectionProvider to a boolean indicating visibility. + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is ConnectionProvider provider) + { + return provider == _targetProvider; + } + return false; + } + + /// + /// Not implemented - this is a one-way converter. + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj b/NEW/src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj index cd40a37..7d157ba 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj +++ b/NEW/src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj @@ -17,6 +17,7 @@ + diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs index 7c12543..6fcfba8 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs @@ -40,7 +40,7 @@ public class ConfigModel /// /// Gets or sets the connection strings for external data sources. /// - public Dictionary ConnectionStrings { get; set; } = new(); + public ConnectionStringsSection ConnectionStrings { get; set; } = new(); /// /// Gets or sets the secure store configuration. diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionProvider.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionProvider.cs new file mode 100644 index 0000000..e1fa8a2 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionProvider.cs @@ -0,0 +1,11 @@ +namespace JdeScoping.ConfigManager.Models; + +/// +/// Database provider types supported by the ConnectionStrings editor. +/// +public enum ConnectionProvider +{ + Generic, + SqlServer, + Oracle +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs new file mode 100644 index 0000000..d0c2752 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs @@ -0,0 +1,86 @@ +namespace JdeScoping.ConfigManager.Models; + +/// +/// Represents a single connection string entry with provider-specific fields. +/// +public class ConnectionStringEntry +{ + public string Name { get; set; } = string.Empty; + public ConnectionProvider Provider { get; set; } = ConnectionProvider.Generic; + + // SqlServer fields + public string? Server { get; set; } + public string? Database { get; set; } + public string? UserId { get; set; } + public string? Password { get; set; } + public string Encrypt { get; set; } = "True"; + public bool TrustServerCertificate { get; set; } + public int ConnectionTimeout { get; set; } = 30; + public string? ApplicationName { get; set; } + + // Oracle fields + public string? Host { get; set; } + public int Port { get; set; } = 1521; + public string? ServiceName { get; set; } + + // Generic fields + public string? RawConnectionString { get; set; } + + /// + /// Generates the connection string based on the Provider type. + /// + public string GenerateConnectionString() + { + return Provider switch + { + ConnectionProvider.SqlServer => GenerateSqlServerConnectionString(), + ConnectionProvider.Oracle => GenerateOracleConnectionString(), + ConnectionProvider.Generic => RawConnectionString ?? string.Empty, + _ => string.Empty + }; + } + + private string GenerateSqlServerConnectionString() + { + var parts = new List(); + + if (!string.IsNullOrWhiteSpace(Server)) + parts.Add($"Server={Server}"); + if (!string.IsNullOrWhiteSpace(Database)) + parts.Add($"Database={Database}"); + if (!string.IsNullOrWhiteSpace(UserId)) + parts.Add($"User Id={UserId}"); + if (!string.IsNullOrWhiteSpace(Password)) + parts.Add($"Password={Password}"); + if (!string.IsNullOrWhiteSpace(Encrypt)) + parts.Add($"Encrypt={Encrypt}"); + if (TrustServerCertificate) + parts.Add("TrustServerCertificate=True"); + if (ConnectionTimeout != 30) + parts.Add($"Connection Timeout={ConnectionTimeout}"); + if (!string.IsNullOrWhiteSpace(ApplicationName)) + parts.Add($"Application Name={ApplicationName}"); + + return string.Join(";", parts); + } + + private string GenerateOracleConnectionString() + { + var parts = new List(); + + var host = Host ?? "localhost"; + var port = Port > 0 ? Port : 1521; + var service = ServiceName ?? ""; + + parts.Add($"Data Source=//{host}:{port}/{service}"); + + if (!string.IsNullOrWhiteSpace(UserId)) + parts.Add($"User Id={UserId}"); + if (!string.IsNullOrWhiteSpace(Password)) + parts.Add($"Password={Password}"); + if (ConnectionTimeout > 0 && ConnectionTimeout != 30) + parts.Add($"Connection Timeout={ConnectionTimeout}"); + + return string.Join(";", parts); + } +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSection.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSection.cs new file mode 100644 index 0000000..d55601b --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSection.cs @@ -0,0 +1,9 @@ +namespace JdeScoping.ConfigManager.Models; + +/// +/// Configuration section for connection strings. +/// +public class ConnectionStringsSection +{ + public List Entries { get; set; } = new(); +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConnectionTestService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConnectionTestService.cs new file mode 100644 index 0000000..ff73e96 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ConnectionTestService.cs @@ -0,0 +1,97 @@ +using System.Diagnostics; +using JdeScoping.ConfigManager.Models; +using Microsoft.Data.SqlClient; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for testing database connections. +/// +public class ConnectionTestService : IConnectionTestService +{ + private const int ConnectionTimeoutSeconds = 10; + + public async Task TestConnectionAsync( + string connectionString, + ConnectionProvider provider, + CancellationToken cancellationToken = default) + { + return provider switch + { + ConnectionProvider.SqlServer => await TestSqlServerConnectionAsync(connectionString, cancellationToken).ConfigureAwait(false), + ConnectionProvider.Oracle => new ConnectionTestResult + { + Success = false, + Message = "Oracle connection testing not implemented" + }, + ConnectionProvider.Generic => new ConnectionTestResult + { + Success = false, + Message = "Cannot test generic connection strings" + }, + _ => new ConnectionTestResult + { + Success = false, + Message = $"Unknown provider: {provider}" + } + }; + } + + private static async Task TestSqlServerConnectionAsync( + string connectionString, + CancellationToken cancellationToken) + { + var stopwatch = Stopwatch.StartNew(); + + try + { + // Ensure connection timeout is set + var builder = new SqlConnectionStringBuilder(connectionString) + { + ConnectTimeout = ConnectionTimeoutSeconds + }; + + await using var connection = new SqlConnection(builder.ConnectionString); + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + stopwatch.Stop(); + + return new ConnectionTestResult + { + Success = true, + Message = "Connection successful", + Duration = stopwatch.Elapsed + }; + } + catch (OperationCanceledException) + { + stopwatch.Stop(); + return new ConnectionTestResult + { + Success = false, + Message = "Connection test was cancelled", + Duration = stopwatch.Elapsed + }; + } + catch (SqlException ex) + { + stopwatch.Stop(); + return new ConnectionTestResult + { + Success = false, + Message = $"SQL Server error: {ex.Message}", + Duration = stopwatch.Elapsed + }; + } + catch (Exception ex) + { + stopwatch.Stop(); + return new ConnectionTestResult + { + Success = false, + Message = $"Connection failed: {ex.Message}", + Duration = stopwatch.Elapsed + }; + } + } +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConnectionTestService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConnectionTestService.cs new file mode 100644 index 0000000..6e9655b --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IConnectionTestService.cs @@ -0,0 +1,21 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Result of testing a database connection. +/// +public class ConnectionTestResult +{ + public bool Success { get; init; } + public string Message { get; init; } = string.Empty; + public TimeSpan? Duration { get; init; } +} + +/// +/// Service for testing database connections. +/// +public interface IConnectionTestService +{ + Task TestConnectionAsync(string connectionString, ConnectionProvider provider, CancellationToken cancellationToken = default); +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs new file mode 100644 index 0000000..c0d5d02 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs @@ -0,0 +1,325 @@ +using System.Windows.Input; +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.ViewModels.Forms; + +/// +/// ViewModel for editing a single connection string entry. +/// +public class ConnectionStringEntryViewModel : ViewModelBase +{ + private readonly ConnectionStringEntry _model; + private readonly Action _onChanged; + private bool _isPasswordVisible; + + /// + /// Initializes a new instance of the class. + /// + /// The connection string entry model. + /// The action to invoke when any property changes. + public ConnectionStringEntryViewModel(ConnectionStringEntry model, Action onChanged) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged)); + + TogglePasswordVisibilityCommand = new RelayCommand(() => IsPasswordVisible = !IsPasswordVisible); + } + + /// + /// Gets or sets the connection string name. + /// + public string Name + { + get => _model.Name; + set + { + if (_model.Name != value) + { + _model.Name = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the database provider type. + /// + public ConnectionProvider Provider + { + get => _model.Provider; + set + { + if (_model.Provider != value) + { + _model.Provider = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ProviderDisplay)); + OnPropertyChanged(nameof(ServerDisplay)); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the SQL Server server name. + /// + public string? Server + { + get => _model.Server; + set + { + if (_model.Server != value) + { + _model.Server = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ServerDisplay)); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the database name. + /// + public string? Database + { + get => _model.Database; + set + { + if (_model.Database != value) + { + _model.Database = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the user ID. + /// + public string? UserId + { + get => _model.UserId; + set + { + if (_model.UserId != value) + { + _model.UserId = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the password. + /// + public string? Password + { + get => _model.Password; + set + { + if (_model.Password != value) + { + _model.Password = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the encrypt setting for SQL Server. + /// + public string Encrypt + { + get => _model.Encrypt; + set + { + if (_model.Encrypt != value) + { + _model.Encrypt = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets whether to trust the server certificate for SQL Server. + /// + public bool TrustServerCertificate + { + get => _model.TrustServerCertificate; + set + { + if (_model.TrustServerCertificate != value) + { + _model.TrustServerCertificate = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the connection timeout in seconds. + /// + public int ConnectionTimeout + { + get => _model.ConnectionTimeout; + set + { + if (_model.ConnectionTimeout != value) + { + _model.ConnectionTimeout = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the application name for SQL Server. + /// + public string? ApplicationName + { + get => _model.ApplicationName; + set + { + if (_model.ApplicationName != value) + { + _model.ApplicationName = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the Oracle host name. + /// + public string? Host + { + get => _model.Host; + set + { + if (_model.Host != value) + { + _model.Host = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ServerDisplay)); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the Oracle port number. + /// + public int Port + { + get => _model.Port; + set + { + if (_model.Port != value) + { + _model.Port = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the Oracle service name. + /// + public string? ServiceName + { + get => _model.ServiceName; + set + { + if (_model.ServiceName != value) + { + _model.ServiceName = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets the raw connection string for generic providers. + /// + public string? RawConnectionString + { + get => _model.RawConnectionString; + set + { + if (_model.RawConnectionString != value) + { + _model.RawConnectionString = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(GeneratedConnectionString)); + _onChanged(); + } + } + } + + /// + /// Gets or sets whether the password is visible. + /// + public bool IsPasswordVisible + { + get => _isPasswordVisible; + set + { + if (_isPasswordVisible != value) + { + _isPasswordVisible = value; + OnPropertyChanged(); + } + } + } + + /// + /// Gets the command to toggle password visibility. + /// + public ICommand TogglePasswordVisibilityCommand { get; } + + /// + /// Gets the generated connection string based on current property values. + /// + public string GeneratedConnectionString => _model.GenerateConnectionString(); + + /// + /// Gets the display string for the provider type. + /// + public string ProviderDisplay => Provider.ToString(); + + /// + /// Gets the server/host display value based on provider type. + /// Returns Server for SqlServer, Host for Oracle, or "-" for Generic. + /// + public string ServerDisplay => Provider switch + { + ConnectionProvider.SqlServer => Server ?? string.Empty, + ConnectionProvider.Oracle => Host ?? string.Empty, + _ => "-" + }; +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs new file mode 100644 index 0000000..d4184b7 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs @@ -0,0 +1,260 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.Services; + +namespace JdeScoping.ConfigManager.ViewModels.Forms; + +/// +/// ViewModel for editing the ConnectionStrings configuration section. +/// Manages a collection of connection string entries with add, delete, validate, and test commands. +/// +public class ConnectionStringsFormViewModel : ViewModelBase +{ + private readonly ConnectionStringsSection _model; + private readonly Action _onChanged; + private readonly IDialogService _dialogService; + private readonly IConnectionTestService _connectionTestService; + private ConnectionStringEntryViewModel? _selectedConnection; + private bool _isTesting; + + /// + /// Initializes a new instance of the class. + /// + /// The connection strings section model. + /// The action to invoke when any property changes. + /// The dialog service for showing messages and confirmations. + /// The service for testing database connections. + public ConnectionStringsFormViewModel( + ConnectionStringsSection model, + Action onChanged, + IDialogService dialogService, + IConnectionTestService connectionTestService) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged)); + _dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); + _connectionTestService = connectionTestService ?? throw new ArgumentNullException(nameof(connectionTestService)); + + Connections = new ObservableCollection(); + + // Initialize view models from model entries + foreach (var entry in _model.Entries) + { + Connections.Add(new ConnectionStringEntryViewModel(entry, _onChanged)); + } + + // Initialize commands + AddConnectionCommand = new RelayCommand(AddConnection); + DeleteConnectionCommand = new AsyncRelayCommand(DeleteConnectionAsync, () => HasSelection); + ValidateConnectionCommand = new AsyncRelayCommand(ValidateConnectionAsync, () => HasSelection); + TestConnectionCommand = new AsyncRelayCommand(TestConnectionAsync, () => HasSelection && !IsTesting); + } + + /// + /// Gets the collection of connection string entry view models. + /// + public ObservableCollection Connections { get; } + + /// + /// Gets or sets the currently selected connection. + /// + public ConnectionStringEntryViewModel? SelectedConnection + { + get => _selectedConnection; + set + { + if (SetProperty(ref _selectedConnection, value)) + { + OnPropertyChanged(nameof(HasSelection)); + RaiseCommandsCanExecuteChanged(); + } + } + } + + /// + /// Gets a value indicating whether a connection is selected. + /// + public bool HasSelection => SelectedConnection != null; + + /// + /// Gets a value indicating whether a connection test is in progress. + /// + public bool IsTesting + { + get => _isTesting; + private set + { + if (SetProperty(ref _isTesting, value)) + { + RaiseCommandsCanExecuteChanged(); + } + } + } + + /// + /// Gets the number of connections in the collection. + /// + public int ConnectionCount => Connections.Count; + + /// + /// Gets the list of available connection providers. + /// + public static IReadOnlyList AvailableProviders { get; } = + Enum.GetValues().ToList().AsReadOnly(); + + /// + /// Gets the list of available encrypt options for SQL Server connections. + /// + public static IReadOnlyList EncryptOptions { get; } = + new List { "True", "False", "Strict" }.AsReadOnly(); + + /// + /// Gets the command for adding a new connection. + /// + public ICommand AddConnectionCommand { get; } + + /// + /// Gets the command for deleting the selected connection. + /// + public ICommand DeleteConnectionCommand { get; } + + /// + /// Gets the command for validating the selected connection string. + /// + public ICommand ValidateConnectionCommand { get; } + + /// + /// Gets the command for testing the selected connection. + /// + public ICommand TestConnectionCommand { get; } + + /// + /// Adds a new connection entry with default values. + /// + private void AddConnection() + { + var entry = new ConnectionStringEntry + { + Name = "NewConnection" + }; + + _model.Entries.Add(entry); + + var viewModel = new ConnectionStringEntryViewModel(entry, _onChanged); + Connections.Add(viewModel); + + SelectedConnection = viewModel; + OnPropertyChanged(nameof(ConnectionCount)); + _onChanged(); + } + + /// + /// Deletes the selected connection after user confirmation. + /// + private async Task DeleteConnectionAsync() + { + if (SelectedConnection == null) + return; + + var name = SelectedConnection.Name; + var confirmed = await _dialogService.ShowConfirmationAsync( + "Delete Connection", + $"Delete connection '{name}'?"); + + if (!confirmed) + return; + + // Find the model entry to remove + var modelEntry = _model.Entries.FirstOrDefault(e => e.Name == name); + if (modelEntry != null) + { + _model.Entries.Remove(modelEntry); + } + + Connections.Remove(SelectedConnection); + SelectedConnection = null; + OnPropertyChanged(nameof(ConnectionCount)); + _onChanged(); + } + + /// + /// Validates that the selected connection has a non-empty generated connection string. + /// + private async Task ValidateConnectionAsync() + { + if (SelectedConnection == null) + return; + + var connectionString = SelectedConnection.GeneratedConnectionString; + + if (string.IsNullOrWhiteSpace(connectionString)) + { + await _dialogService.ShowMessageAsync( + "Validation Failed", + $"Connection '{SelectedConnection.Name}' has an empty connection string. Please configure the connection properties."); + } + else + { + await _dialogService.ShowMessageAsync( + "Validation Passed", + $"Connection '{SelectedConnection.Name}' has a valid connection string format."); + } + } + + /// + /// Tests the selected connection by attempting to connect to the database. + /// + private async Task TestConnectionAsync() + { + if (SelectedConnection == null) + return; + + var connectionString = SelectedConnection.GeneratedConnectionString; + if (string.IsNullOrWhiteSpace(connectionString)) + { + await _dialogService.ShowMessageAsync( + "Test Connection", + "Cannot test connection: connection string is empty. Please configure the connection properties first."); + return; + } + + IsTesting = true; + try + { + var result = await _connectionTestService.TestConnectionAsync( + connectionString, + SelectedConnection.Provider); + + if (result.Success) + { + var durationText = result.Duration.HasValue + ? $" ({result.Duration.Value.TotalMilliseconds:F0}ms)" + : ""; + await _dialogService.ShowMessageAsync( + "Connection Successful", + $"Successfully connected to '{SelectedConnection.Name}'{durationText}."); + } + else + { + await _dialogService.ShowMessageAsync( + "Connection Failed", + $"Failed to connect to '{SelectedConnection.Name}':\n\n{result.Message}"); + } + } + finally + { + IsTesting = false; + } + } + + /// + /// Raises CanExecuteChanged for all commands that depend on selection state. + /// + private void RaiseCommandsCanExecuteChanged() + { + (DeleteConnectionCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); + (ValidateConnectionCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); + (TestConnectionCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); + } +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs index 4d1fef2..533e0fa 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs @@ -25,6 +25,7 @@ public class MainWindowViewModel : ViewModelBase private readonly ISecureStoreManager _secureStoreManager; private readonly IClipboardService _clipboardService; private readonly IRuntimeConfigValidationService _runtimeValidationService; + private readonly IConnectionTestService _connectionTestService; private readonly ILogger? _logger; private string _configFolderPath = "No folder selected"; @@ -203,6 +204,7 @@ public class MainWindowViewModel : ViewModelBase ISecureStoreManager secureStoreManager, IClipboardService clipboardService, IRuntimeConfigValidationService runtimeValidationService, + IConnectionTestService connectionTestService, ILogger? logger) { _fileSystem = fileSystem; @@ -214,6 +216,7 @@ public class MainWindowViewModel : ViewModelBase _secureStoreManager = secureStoreManager; _clipboardService = clipboardService; _runtimeValidationService = runtimeValidationService; + _connectionTestService = connectionTestService; _logger = logger; OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync); @@ -257,6 +260,7 @@ public class MainWindowViewModel : ViewModelBase new SecureStoreManager(), new NullClipboardService(), new RuntimeConfigValidationService(new SecureStoreManager()), + new ConnectionTestService(), null) { } @@ -457,6 +461,7 @@ public class MainWindowViewModel : ViewModelBase settingsFolder.Children.Add(new TreeNodeViewModel("Ldap", "πŸ‘₯", TreeNodeType.SettingsSection) { SectionKey = "Ldap" }); settingsFolder.Children.Add(new TreeNodeViewModel("Search", "πŸ”", TreeNodeType.SettingsSection) { SectionKey = "Search" }); settingsFolder.Children.Add(new TreeNodeViewModel("ExcelExport", "πŸ“Š", TreeNodeType.SettingsSection) { SectionKey = "ExcelExport" }); + settingsFolder.Children.Add(new TreeNodeViewModel("ConnectionStrings", "πŸ”—", TreeNodeType.SettingsSection) { SectionKey = "ConnectionStrings" }); TreeNodes.Add(settingsFolder); // Pipelines folder @@ -563,6 +568,11 @@ public class MainWindowViewModel : ViewModelBase "Ldap" => new LdapFormViewModel(_appSettings.Ldap, MarkAsChanged), "Search" => new SearchFormViewModel(_appSettings.Search, MarkAsChanged), "ExcelExport" => new ExcelExportFormViewModel(_appSettings.ExcelExport, MarkAsChanged), + "ConnectionStrings" when _dialogService != null => new ConnectionStringsFormViewModel( + _appSettings.ConnectionStrings, + MarkAsChanged, + _dialogService, + _connectionTestService), _ when _selectedNode.NodeType == TreeNodeType.Pipeline && _pipelines != null => _pipelines.Pipelines.TryGetValue(_selectedNode.SectionKey!, out var pipeline) ? new PipelineEditorViewModel(_selectedNode.SectionKey!, pipeline, GetAvailableConnections(), MarkAsChanged) diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml b/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml new file mode 100644 index 0000000..08bbbd1 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml @@ -0,0 +1,421 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml.cs new file mode 100644 index 0000000..0202f20 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace JdeScoping.ConfigManager.Views.Forms; + +public partial class ConnectionStringsFormView : UserControl +{ + public ConnectionStringsFormView() + { + InitializeComponent(); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs new file mode 100644 index 0000000..ef4329a --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs @@ -0,0 +1,122 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Tests.Models; + +public class ConnectionStringEntryTests +{ + [Fact] + public void GenerateConnectionString_SqlServer_ProducesCorrectFormat() + { + // Arrange + var entry = new ConnectionStringEntry + { + Provider = ConnectionProvider.SqlServer, + Server = "localhost\\SQLEXPRESS", + Database = "TestDb", + UserId = "sa", + Password = "secret123", + Encrypt = "True", + TrustServerCertificate = true, + ConnectionTimeout = 60, + ApplicationName = "TestApp" + }; + + // Act + var result = entry.GenerateConnectionString(); + + // Assert + result.ShouldContain("Server=localhost\\SQLEXPRESS"); + result.ShouldContain("Database=TestDb"); + result.ShouldContain("User Id=sa"); + result.ShouldContain("Password=secret123"); + result.ShouldContain("Encrypt=True"); + result.ShouldContain("TrustServerCertificate=True"); + result.ShouldContain("Connection Timeout=60"); + result.ShouldContain("Application Name=TestApp"); + } + + [Fact] + public void GenerateConnectionString_SqlServer_OmitsDefaultTimeout() + { + // Arrange + var entry = new ConnectionStringEntry + { + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + Database = "TestDb", + UserId = "user", + Password = "pass", + ConnectionTimeout = 30 // Default value + }; + + // Act + var result = entry.GenerateConnectionString(); + + // Assert + result.ShouldNotContain("Connection Timeout="); + } + + [Fact] + public void GenerateConnectionString_Oracle_ProducesEZConnectFormat() + { + // Arrange + var entry = new ConnectionStringEntry + { + Provider = ConnectionProvider.Oracle, + Host = "oracle-server.example.com", + Port = 1522, + ServiceName = "ORCL", + UserId = "scott", + Password = "tiger" + }; + + // Act + var result = entry.GenerateConnectionString(); + + // Assert + result.ShouldContain("Data Source=//oracle-server.example.com:1522/ORCL"); + result.ShouldContain("User Id=scott"); + result.ShouldContain("Password=tiger"); + } + + [Fact] + public void GenerateConnectionString_Generic_ReturnsRawString() + { + // Arrange + var rawConnString = "Driver={ODBC Driver};Server=myserver;Database=mydb;"; + var entry = new ConnectionStringEntry + { + Provider = ConnectionProvider.Generic, + RawConnectionString = rawConnString + }; + + // Act + var result = entry.GenerateConnectionString(); + + // Assert + result.ShouldBe(rawConnString); + } + + [Fact] + public void DefaultValues_AreCorrect() + { + // Act + var entry = new ConnectionStringEntry(); + + // Assert + entry.Name.ShouldBe(string.Empty); + entry.Provider.ShouldBe(ConnectionProvider.Generic); + entry.Server.ShouldBeNull(); + entry.Database.ShouldBeNull(); + entry.UserId.ShouldBeNull(); + entry.Password.ShouldBeNull(); + entry.Encrypt.ShouldBe("True"); + entry.TrustServerCertificate.ShouldBeFalse(); + entry.ConnectionTimeout.ShouldBe(30); + entry.ApplicationName.ShouldBeNull(); + entry.Host.ShouldBeNull(); + entry.Port.ShouldBe(1521); + entry.ServiceName.ShouldBeNull(); + entry.RawConnectionString.ShouldBeNull(); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs new file mode 100644 index 0000000..68dba00 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs @@ -0,0 +1,182 @@ +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.ViewModels.Forms; + +namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms; + +public class ConnectionStringEntryViewModelTests +{ + [Fact] + public void Constructor_InitializesFromModel() + { + // Arrange + var model = new ConnectionStringEntry + { + Name = "TestConnection", + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + Database = "TestDb", + UserId = "testuser", + Password = "testpass", + Encrypt = "Strict", + TrustServerCertificate = true, + ConnectionTimeout = 45, + ApplicationName = "TestApp" + }; + + // Act + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Assert + sut.Name.ShouldBe("TestConnection"); + sut.Provider.ShouldBe(ConnectionProvider.SqlServer); + sut.Server.ShouldBe("localhost"); + sut.Database.ShouldBe("TestDb"); + sut.UserId.ShouldBe("testuser"); + sut.Password.ShouldBe("testpass"); + sut.Encrypt.ShouldBe("Strict"); + sut.TrustServerCertificate.ShouldBeTrue(); + sut.ConnectionTimeout.ShouldBe(45); + sut.ApplicationName.ShouldBe("TestApp"); + } + + [Fact] + public void Constructor_ThrowsOnNullModel() + { + // Act & Assert + Should.Throw(() => new ConnectionStringEntryViewModel(null!, () => { })); + } + + [Fact] + public void Constructor_ThrowsOnNullOnChanged() + { + // Arrange + var model = new ConnectionStringEntry(); + + // Act & Assert + Should.Throw(() => new ConnectionStringEntryViewModel(model, null!)); + } + + [Fact] + public void PropertyChange_UpdatesModel() + { + // Arrange + var model = new ConnectionStringEntry(); + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Act + sut.Name = "UpdatedName"; + sut.Server = "newserver"; + sut.Database = "newdb"; + + // Assert + model.Name.ShouldBe("UpdatedName"); + model.Server.ShouldBe("newserver"); + model.Database.ShouldBe("newdb"); + } + + [Fact] + public void PropertyChange_InvokesOnChanged() + { + // Arrange + var model = new ConnectionStringEntry(); + var changedInvoked = false; + var sut = new ConnectionStringEntryViewModel(model, () => changedInvoked = true); + + // Act + sut.Name = "NewName"; + + // Assert + changedInvoked.ShouldBeTrue(); + } + + [Fact] + public void TogglePasswordVisibility_TogglesIsPasswordVisible() + { + // Arrange + var model = new ConnectionStringEntry(); + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Assert initial state + sut.IsPasswordVisible.ShouldBeFalse(); + + // Act - first toggle + sut.TogglePasswordVisibilityCommand.Execute(null); + + // Assert + sut.IsPasswordVisible.ShouldBeTrue(); + + // Act - second toggle + sut.TogglePasswordVisibilityCommand.Execute(null); + + // Assert + sut.IsPasswordVisible.ShouldBeFalse(); + } + + [Fact] + public void ProviderDisplay_ReturnsCorrectString() + { + // Arrange + var model = new ConnectionStringEntry { Provider = ConnectionProvider.SqlServer }; + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Assert + sut.ProviderDisplay.ShouldBe("SqlServer"); + + // Act + sut.Provider = ConnectionProvider.Oracle; + + // Assert + sut.ProviderDisplay.ShouldBe("Oracle"); + + // Act + sut.Provider = ConnectionProvider.Generic; + + // Assert + sut.ProviderDisplay.ShouldBe("Generic"); + } + + [Fact] + public void ServerDisplay_ReturnsServerForSqlServer() + { + // Arrange + var model = new ConnectionStringEntry + { + Provider = ConnectionProvider.SqlServer, + Server = "sql-server-host" + }; + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Assert + sut.ServerDisplay.ShouldBe("sql-server-host"); + } + + [Fact] + public void ServerDisplay_ReturnsHostForOracle() + { + // Arrange + var model = new ConnectionStringEntry + { + Provider = ConnectionProvider.Oracle, + Host = "oracle-host" + }; + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Assert + sut.ServerDisplay.ShouldBe("oracle-host"); + } + + [Fact] + public void ServerDisplay_ReturnsDashForGeneric() + { + // Arrange + var model = new ConnectionStringEntry + { + Provider = ConnectionProvider.Generic, + RawConnectionString = "some connection string" + }; + var sut = new ConnectionStringEntryViewModel(model, () => { }); + + // Assert + sut.ServerDisplay.ShouldBe("-"); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs new file mode 100644 index 0000000..87b42fd --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs @@ -0,0 +1,167 @@ +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.Services; +using JdeScoping.ConfigManager.ViewModels.Forms; + +namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms; + +public class ConnectionStringsFormViewModelTests +{ + private readonly IDialogService _dialogService; + private readonly IConnectionTestService _connectionTestService; + + public ConnectionStringsFormViewModelTests() + { + _dialogService = Substitute.For(); + _connectionTestService = Substitute.For(); + } + + [Fact] + public void Constructor_InitializesFromModel() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + new ConnectionStringEntry + { + Name = "Connection1", + Provider = ConnectionProvider.SqlServer, + Server = "server1" + }, + new ConnectionStringEntry + { + Name = "Connection2", + Provider = ConnectionProvider.Oracle, + Host = "oracle-host" + } + } + }; + + // Act + var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + + // Assert + sut.Connections.Count.ShouldBe(2); + sut.Connections[0].Name.ShouldBe("Connection1"); + sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer); + sut.Connections[0].Server.ShouldBe("server1"); + sut.Connections[1].Name.ShouldBe("Connection2"); + sut.Connections[1].Provider.ShouldBe(ConnectionProvider.Oracle); + sut.Connections[1].Host.ShouldBe("oracle-host"); + } + + [Fact] + public void Constructor_ThrowsOnNullModel() + { + // Act & Assert + Should.Throw(() => + new ConnectionStringsFormViewModel(null!, () => { }, _dialogService, _connectionTestService)); + } + + [Fact] + public void Constructor_ThrowsOnNullOnChanged() + { + // Arrange + var model = new ConnectionStringsSection(); + + // Act & Assert + Should.Throw(() => + new ConnectionStringsFormViewModel(model, null!, _dialogService, _connectionTestService)); + } + + [Fact] + public void Constructor_ThrowsOnNullConnectionTestService() + { + // Arrange + var model = new ConnectionStringsSection(); + + // Act & Assert + Should.Throw(() => + new ConnectionStringsFormViewModel(model, () => { }, _dialogService, null!)); + } + + [Fact] + public void AddConnection_CreatesNewEntryAndSelectsIt() + { + // Arrange + var model = new ConnectionStringsSection(); + var changedInvoked = false; + var sut = new ConnectionStringsFormViewModel(model, () => changedInvoked = true, _dialogService, _connectionTestService); + + // Act + sut.AddConnectionCommand.Execute(null); + + // Assert + sut.Connections.Count.ShouldBe(1); + sut.Connections[0].Name.ShouldBe("NewConnection"); + sut.SelectedConnection.ShouldBe(sut.Connections[0]); + model.Entries.Count.ShouldBe(1); + changedInvoked.ShouldBeTrue(); + } + + [Fact] + public void HasSelection_IsFalseWhenNothingSelected() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + new ConnectionStringEntry { Name = "Conn1" } + } + }; + var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + + // Assert - no selection by default + sut.SelectedConnection.ShouldBeNull(); + sut.HasSelection.ShouldBeFalse(); + } + + [Fact] + public void HasSelection_IsTrueWhenConnectionSelected() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + new ConnectionStringEntry { Name = "Conn1" } + } + }; + var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + + // Act + sut.SelectedConnection = sut.Connections[0]; + + // Assert + sut.HasSelection.ShouldBeTrue(); + } + + [Fact] + public void ConnectionCount_ReflectsCollectionSize() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + new ConnectionStringEntry { Name = "Conn1" }, + new ConnectionStringEntry { Name = "Conn2" }, + new ConnectionStringEntry { Name = "Conn3" } + } + }; + + // Act + var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + + // Assert + sut.ConnectionCount.ShouldBe(3); + + // Act - add another + sut.AddConnectionCommand.Execute(null); + + // Assert + sut.ConnectionCount.ShouldBe(4); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs index e0fdd5b..bddc241 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs @@ -19,6 +19,7 @@ public class MainWindowViewModelTests private readonly ISecureStoreManager _secureStoreManager; private readonly IClipboardService _clipboardService; private readonly IRuntimeConfigValidationService _runtimeValidationService; + private readonly IConnectionTestService _connectionTestService; private readonly ILogger _logger; public MainWindowViewModelTests() @@ -32,6 +33,7 @@ public class MainWindowViewModelTests _secureStoreManager = Substitute.For(); _clipboardService = Substitute.For(); _runtimeValidationService = Substitute.For(); + _connectionTestService = Substitute.For(); _logger = Substitute.For>(); _validationService.ValidateAppSettings(Arg.Any()) @@ -284,7 +286,7 @@ public class MainWindowViewModelTests // Without a configured/open SecureStore, only Settings and Pipelines appear sut.TreeNodes.Count.ShouldBe(2); // Settings, Pipelines (no Secure Store when not configured) sut.TreeNodes[0].Name.ShouldBe("Settings"); - sut.TreeNodes[0].Children.Count.ShouldBe(6); // DataSync, DataAccess, Auth, Ldap, Search, ExcelExport + sut.TreeNodes[0].Children.Count.ShouldBe(7); // ConnectionStrings, DataSync, DataAccess, Auth, Ldap, Search, ExcelExport sut.TreeNodes[1].Name.ShouldBe("Pipelines"); } @@ -382,6 +384,7 @@ public class MainWindowViewModelTests _secureStoreManager, _clipboardService, _runtimeValidationService, + _connectionTestService, _logger); } } diff --git a/PLANS/ConnectionStringsEditor-Design.md b/PLANS/ConnectionStringsEditor-Design.md new file mode 100644 index 0000000..2b8f8ef --- /dev/null +++ b/PLANS/ConnectionStringsEditor-Design.md @@ -0,0 +1,299 @@ +# ConnectionStrings Editor - Design Plan + +## Overview + +Add a ConnectionStrings section under Settings in the ConfigManager tree view. This section provides a master-detail editor for managing database connection strings with provider-specific field editing. + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Storage location | appsettings.json | Consistent with existing settings pattern | +| Field editing | Provider-specific parsed fields | Better UX than raw connection string editing | +| Supported providers | Generic, Oracle, SqlServer | Covers existing JDE/CMS/MSSQL needs | +| SQL Server auth | SQL Authentication only | Windows auth not needed for this use case | +| Password display | Masked with reveal toggle | Security + usability balance | +| Edit workflow | Inline panel editing | Matches existing form patterns | +| Delete behavior | Confirmation dialog, immediate | Prevents accidents, clear feedback | + +## Architecture + +### Tree Structure + +``` +Settings/ +β”œβ”€β”€ DataSync +β”œβ”€β”€ DataAccess +β”œβ”€β”€ Auth +β”œβ”€β”€ Ldap +β”œβ”€β”€ Search +β”œβ”€β”€ ExcelExport +└── ConnectionStrings <-- NEW +``` + +### Data Model + +**ConnectionStringEntry** - represents a single connection string: + +```csharp +public class ConnectionStringEntry +{ + public string Name { get; set; } = string.Empty; + public ConnectionProvider Provider { get; set; } = ConnectionProvider.Generic; + + // SqlServer fields + public string? Server { get; set; } + public string? Database { get; set; } + public string? UserId { get; set; } + public string? Password { get; set; } + public string Encrypt { get; set; } = "True"; + public bool TrustServerCertificate { get; set; } + public int ConnectionTimeout { get; set; } = 30; + public string? ApplicationName { get; set; } + + // Oracle fields + public string? Host { get; set; } + public int Port { get; set; } = 1521; + public string? ServiceName { get; set; } + + // Generic fields + public string? RawConnectionString { get; set; } + + // Generates the final connection string based on Provider + public string GenerateConnectionString() { ... } +} + +public enum ConnectionProvider +{ + Generic, + SqlServer, + Oracle +} +``` + +**ConnectionStringsSection** - the config model section: + +```csharp +public class ConnectionStringsSection +{ + public List Entries { get; set; } = new(); +} +``` + +### ViewModel Structure + +``` +ConnectionStringsFormViewModel +β”œβ”€β”€ Connections : ObservableCollection +β”œβ”€β”€ SelectedConnection : ConnectionStringEntryViewModel? +β”œβ”€β”€ HasSelection : bool +β”œβ”€β”€ AvailableProviders : IReadOnlyList +β”œβ”€β”€ AddConnectionCommand +β”œβ”€β”€ DeleteConnectionCommand +β”œβ”€β”€ ValidateConnectionCommand +β”œβ”€β”€ TestConnectionCommand +``` + +**ConnectionStringEntryViewModel** - wraps each entry for editing: + +```csharp +public class ConnectionStringEntryViewModel : ViewModelBase +{ + // All fields from ConnectionStringEntry exposed as properties + // Provider property triggers template switching + // GeneratedConnectionString property for preview + // IsPasswordVisible for password reveal toggle +} +``` + +### Connection String Formats + +**SqlServer:** +``` +Server={server};Database={database};User Id={userId};Password={password};Encrypt={encrypt};TrustServerCertificate={trust};Connection Timeout={timeout};Application Name={appName}; +``` + +**Oracle (EZConnect):** +``` +Data Source=//{host}:{port}/{serviceName};User Id={userId};Password={password};Connection Timeout={timeout}; +``` + +**Generic:** +Raw connection string as entered by user. + +## UI Design + +### Layout Structure + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Connection Strings β”‚ +β”‚ ──────────────────────────────────────────────────────── β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Connections (3) β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Name β”‚ Provider β”‚ Server β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ +β”‚ β”‚ β”‚ jde β”‚ Oracle β”‚ jdeserver.company.com β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ mssql β”‚ SqlServerβ”‚ localhost\SQLEXPRESS β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ cms β”‚ Generic β”‚ - β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ [+ Add] [Delete] β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Edit Connection: mssql β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Name * Provider * β”‚ β”‚ +β”‚ β”‚ [mssql ] [SqlServer β–Ό] β”‚ β”‚ +β”‚ β”‚ ──────────────────────────────────────────────────── β”‚ β”‚ +β”‚ β”‚ Server * Database * β”‚ β”‚ +β”‚ β”‚ [localhost\SQLEXPRESS] [ScopingTool ] β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ User Id Password β”‚ β”‚ +β”‚ β”‚ [sa ] [β€’β€’β€’β€’β€’β€’β€’β€’ ] [πŸ‘] β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Encrypt ☐ Trust Server Certificate β”‚ β”‚ +β”‚ β”‚ [True β–Ό] (Skip cert validation) β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Connection Timeout Application Name β”‚ β”‚ +β”‚ β”‚ [30 ] [JdeScopingTool ] β”‚ β”‚ +β”‚ β”‚ ──────────────────────────────────────────────────── β”‚ β”‚ +β”‚ β”‚ CONNECTION STRING PREVIEW β”‚ β”‚ +β”‚ β”‚ Server=localhost\SQLEXPRESS;Database=ScopingTool;... β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ [Validate] [Test Connection] β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Provider-Specific Forms + +**SqlServer fields:** +- Server (text, required) +- Database (text, required) +- User Id (text) +- Password (password with reveal toggle) +- Encrypt (dropdown: True/False/Strict) +- TrustServerCertificate (checkbox) +- Connection Timeout (numeric, default 30) +- Application Name (text) + +**Oracle fields:** +- Host (text, required) +- Port (numeric, default 1521) +- Service Name (text, required) +- User Id (text, required) +- Password (password with reveal toggle, required) +- Connection Timeout (numeric) + +**Generic fields:** +- Connection String (multiline text, required) + +### Button Behaviors + +| Button | Action | +|--------|--------| +| Add | Creates new entry with default values, selects it | +| Delete | Shows confirmation dialog, removes entry on confirm | +| Validate | Validates connection string syntax (no network call) | +| Test | Attempts actual database connection, shows modal with result | + +### State Transitions + +``` +No connections β†’ Empty state with "Add First Connection" button +Connections exist, none selected β†’ Table visible, placeholder in edit area +Connection selected β†’ Edit form visible with provider-specific fields +Field changed β†’ Entry marked dirty, generates preview string +Save (via main Save) β†’ All changes persisted to appsettings.json +``` + +## Integration Points + +### ConfigModel Integration + +Add `ConnectionStrings` property to `ConfigModel`: + +```csharp +public class ConfigModel +{ + // Existing properties... + public ConnectionStringsSection ConnectionStrings { get; set; } = new(); +} +``` + +### MainWindowViewModel Integration + +- Add ConnectionStrings node to tree under Settings folder +- Handle node selection to load `ConnectionStringsFormViewModel` +- Mark node as modified when connection strings change + +### Serialization + +Connection strings serialize to appsettings.json as: + +```json +{ + "ConnectionStrings": { + "Entries": [ + { + "Name": "mssql", + "Provider": "SqlServer", + "Server": "localhost\\SQLEXPRESS", + "Database": "ScopingTool", + "UserId": "sa", + "Password": "secretpassword", + "Encrypt": "True", + "TrustServerCertificate": true, + "ConnectionTimeout": 30, + "ApplicationName": "JdeScopingTool" + }, + { + "Name": "jde", + "Provider": "Oracle", + "Host": "jdeserver.company.com", + "Port": 1521, + "ServiceName": "JDEPROD", + "UserId": "jde_readonly", + "Password": "oraclepassword", + "ConnectionTimeout": 60 + } + ] + } +} +``` + +## Testing Strategy + +### Unit Tests + +1. **ConnectionStringEntry tests:** + - GenerateConnectionString produces correct format per provider + - Default values are correct + - Required field validation + +2. **ConnectionStringEntryViewModel tests:** + - Property changes raise PropertyChanged + - Provider change clears irrelevant fields + - Password visibility toggle works + +3. **ConnectionStringsFormViewModel tests:** + - Add creates new entry and selects it + - Delete removes selected entry + - Selection change updates form + - HasSelection reflects state correctly + +### Integration Tests + +1. **Serialization round-trip:** + - Save and reload preserves all fields + - Provider enum serializes correctly + +2. **Connection testing:** + - SqlServer test connection works + - Oracle test connection works + - Error messages displayed correctly diff --git a/PLANS/ConnectionStringsEditor-Implementation.md b/PLANS/ConnectionStringsEditor-Implementation.md new file mode 100644 index 0000000..7c4bbbb --- /dev/null +++ b/PLANS/ConnectionStringsEditor-Implementation.md @@ -0,0 +1,421 @@ +# ConnectionStrings Editor - Implementation Plan + +## Overview + +Step-by-step implementation guide for adding ConnectionStrings editor to ConfigManager. + +**Estimated tasks:** 12 tasks in 4 batches + +--- + +## Batch 1: Data Models + +### Task 1: Create ConnectionProvider enum + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionProvider.cs` + +```csharp +namespace JdeScoping.ConfigManager.Models; + +/// +/// Database provider types supported by the ConnectionStrings editor. +/// +public enum ConnectionProvider +{ + Generic, + SqlServer, + Oracle +} +``` + +**Verification:** Build succeeds. + +--- + +### Task 2: Create ConnectionStringEntry model + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringEntry.cs` + +```csharp +namespace JdeScoping.ConfigManager.Models; + +/// +/// Represents a single connection string entry with provider-specific fields. +/// +public class ConnectionStringEntry +{ + public string Name { get; set; } = string.Empty; + public ConnectionProvider Provider { get; set; } = ConnectionProvider.Generic; + + // SqlServer fields + public string? Server { get; set; } + public string? Database { get; set; } + public string? UserId { get; set; } + public string? Password { get; set; } + public string Encrypt { get; set; } = "True"; + public bool TrustServerCertificate { get; set; } + public int ConnectionTimeout { get; set; } = 30; + public string? ApplicationName { get; set; } + + // Oracle fields + public string? Host { get; set; } + public int Port { get; set; } = 1521; + public string? ServiceName { get; set; } + + // Generic fields + public string? RawConnectionString { get; set; } + + /// + /// Generates the connection string based on the Provider type. + /// + public string GenerateConnectionString() + { + return Provider switch + { + ConnectionProvider.SqlServer => GenerateSqlServerConnectionString(), + ConnectionProvider.Oracle => GenerateOracleConnectionString(), + ConnectionProvider.Generic => RawConnectionString ?? string.Empty, + _ => string.Empty + }; + } + + private string GenerateSqlServerConnectionString() + { + var parts = new List(); + + if (!string.IsNullOrWhiteSpace(Server)) + parts.Add($"Server={Server}"); + if (!string.IsNullOrWhiteSpace(Database)) + parts.Add($"Database={Database}"); + if (!string.IsNullOrWhiteSpace(UserId)) + parts.Add($"User Id={UserId}"); + if (!string.IsNullOrWhiteSpace(Password)) + parts.Add($"Password={Password}"); + if (!string.IsNullOrWhiteSpace(Encrypt)) + parts.Add($"Encrypt={Encrypt}"); + if (TrustServerCertificate) + parts.Add("TrustServerCertificate=True"); + if (ConnectionTimeout != 30) + parts.Add($"Connection Timeout={ConnectionTimeout}"); + if (!string.IsNullOrWhiteSpace(ApplicationName)) + parts.Add($"Application Name={ApplicationName}"); + + return string.Join(";", parts); + } + + private string GenerateOracleConnectionString() + { + var parts = new List(); + + var host = Host ?? "localhost"; + var port = Port > 0 ? Port : 1521; + var service = ServiceName ?? ""; + + parts.Add($"Data Source=//{host}:{port}/{service}"); + + if (!string.IsNullOrWhiteSpace(UserId)) + parts.Add($"User Id={UserId}"); + if (!string.IsNullOrWhiteSpace(Password)) + parts.Add($"Password={Password}"); + if (ConnectionTimeout > 0 && ConnectionTimeout != 30) + parts.Add($"Connection Timeout={ConnectionTimeout}"); + + return string.Join(";", parts); + } +} +``` + +**Verification:** Build succeeds. + +--- + +### Task 3: Create ConnectionStringsSection model + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSection.cs` + +```csharp +namespace JdeScoping.ConfigManager.Models; + +/// +/// Configuration section for connection strings. +/// +public class ConnectionStringsSection +{ + public List Entries { get; set; } = new(); +} +``` + +**Verification:** Build succeeds. + +--- + +### Task 4: Add ConnectionStrings to ConfigModel + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Models/ConfigModel.cs` + +Add property to existing ConfigModel class: + +```csharp +public ConnectionStringsSection ConnectionStrings { get; set; } = new(); +``` + +**Verification:** Build succeeds. + +--- + +## Batch 2: ViewModels + +### Task 5: Create ConnectionStringEntryViewModel + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringEntryViewModel.cs` + +Create ViewModel that wraps ConnectionStringEntry with: +- All properties exposed with change notification +- `GeneratedConnectionString` computed property +- `IsPasswordVisible` toggle property +- `TogglePasswordVisibilityCommand` +- `ProviderDisplay` and `ServerDisplay` for table columns + +Key implementation details: +- Constructor takes `ConnectionStringEntry model` and `Action onChanged` +- All setters call `OnPropertyChanged()` and `_onChanged()` +- When Provider changes, also notify `GeneratedConnectionString` +- ServerDisplay returns Server (SqlServer), Host (Oracle), or "-" (Generic) + +**Verification:** Build succeeds. + +--- + +### Task 6: Create ConnectionStringsFormViewModel + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs` + +Create main ViewModel with: +- `Connections : ObservableCollection` +- `SelectedConnection : ConnectionStringEntryViewModel?` +- `HasSelection : bool` (computed from SelectedConnection != null) +- `ConnectionCount : int` (computed from Connections.Count) +- `AvailableProviders : IReadOnlyList` +- `EncryptOptions : IReadOnlyList` = ["True", "False", "Strict"] + +Commands: +- `AddConnectionCommand` - creates new entry with default name "NewConnection", selects it +- `DeleteConnectionCommand` - requires confirmation via IDialogService, removes selected +- `ValidateConnectionCommand` - validates syntax, shows result via IDialogService +- `TestConnectionCommand` - tests actual connection, shows modal result + +Constructor takes: +- `ConnectionStringsSection model` +- `Action onChanged` +- `IDialogService dialogService` + +**Verification:** Build succeeds. + +--- + +### Task 7: Create IConnectionTestService interface and implementation + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/IConnectionTestService.cs` + +```csharp +namespace JdeScoping.ConfigManager.Services; + +public interface IConnectionTestService +{ + Task TestConnectionAsync(string connectionString, ConnectionProvider provider); +} + +public class ConnectionTestResult +{ + public bool Success { get; init; } + public string Message { get; init; } = string.Empty; + public TimeSpan? Duration { get; init; } +} +``` + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/ConnectionTestService.cs` + +Implementation that: +- Uses `Microsoft.Data.SqlClient` for SqlServer +- Uses `Oracle.ManagedDataAccess.Client` for Oracle (or stub if not available) +- Returns success/failure with timing and error message + +**Verification:** Build succeeds. + +--- + +## Batch 3: Views (AXAML) + +### Task 8: Create ConnectionStringsFormView.axaml + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml` + +Create view with: +1. Header section ("Connection Strings" with separator) +2. Connections list section: + - DataGrid with Name, Provider, Server columns + - Height="200", selection mode single + - Toolbar with Add, Delete buttons +3. Placeholder section (when no selection): + - "Select a connection string to edit" text +4. Edit form section (when selection exists): + - Name + Provider fields (always visible) + - ContentControl with DataTemplates for provider-specific fields +5. Action buttons section: + - Validate, Test Connection buttons + +Use existing form styling: +- Background="#0D0F12", BorderBrush="#2D3540" +- Input Background="#232A35" +- FontFamily="JetBrains Mono" for values +- MaxWidth="800" (wider than other forms for table) + +**Verification:** Build succeeds and view renders. + +--- + +### Task 9: Create provider-specific DataTemplates + +Within ConnectionStringsFormView.axaml, create DataTemplates in UserControl.Resources: + +**SqlServerTemplate:** +- Server, Database fields (row) +- UserId, Password fields (row, password with reveal button) +- Encrypt dropdown, TrustServerCertificate checkbox (row) +- ConnectionTimeout, ApplicationName fields (row) +- Connection string preview box + +**OracleTemplate:** +- Host, Port fields (row) +- ServiceName field +- UserId, Password fields (row, password with reveal button) +- ConnectionTimeout field +- Connection string preview box + +**GenericTemplate:** +- Info banner explaining generic usage +- Connection String multiline TextBox + +Use ContentControl with binding to switch templates based on Provider. + +**Verification:** Build succeeds, templates render correctly. + +--- + +### Task 10: Create ConnectionStringsFormView.axaml.cs code-behind + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/ConnectionStringsFormView.axaml.cs` + +Standard Avalonia code-behind: + +```csharp +using Avalonia.Controls; + +namespace JdeScoping.ConfigManager.Views.Forms; + +public partial class ConnectionStringsFormView : UserControl +{ + public ConnectionStringsFormView() + { + InitializeComponent(); + } +} +``` + +**Verification:** Build succeeds. + +--- + +## Batch 4: Integration & Tests + +### Task 11: Integrate into MainWindowViewModel + +**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs` + +Updates needed: + +1. In `BuildSettingsNodes()` method, add ConnectionStrings node: + ```csharp + new TreeNodeViewModel("ConnectionStrings", "πŸ”—", TreeNodeType.SettingsSection) + { + SectionKey = "ConnectionStrings" + } + ``` + +2. In `LoadFormViewModelForNode()` method, add case for ConnectionStrings: + ```csharp + "ConnectionStrings" => new ConnectionStringsFormViewModel( + _appSettingsConfig!.ConnectionStrings, + MarkCurrentNodeModified, + _dialogService, + _connectionTestService), + ``` + +3. Register `IConnectionTestService` in DI (App.axaml.cs or Program.cs) + +**Verification:** +- Build succeeds +- ConnectionStrings appears in Settings tree +- Clicking node shows form + +--- + +### Task 12: Add unit tests + +**File:** `NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringEntryTests.cs` + +Test cases: +- `GenerateConnectionString_SqlServer_ProducesCorrectFormat` +- `GenerateConnectionString_SqlServer_OmitsDefaultTimeout` +- `GenerateConnectionString_Oracle_ProducesEZConnectFormat` +- `GenerateConnectionString_Generic_ReturnsRawString` +- `DefaultValues_AreCorrect` + +**File:** `NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringEntryViewModelTests.cs` + +Test cases: +- `Constructor_InitializesFromModel` +- `PropertyChange_UpdatesModel` +- `PropertyChange_InvokesOnChanged` +- `TogglePasswordVisibility_TogglesIsPasswordVisible` +- `ProviderDisplay_ReturnsCorrectString` +- `ServerDisplay_ReturnsServerForSqlServer` +- `ServerDisplay_ReturnsHostForOracle` +- `ServerDisplay_ReturnsDashForGeneric` + +**File:** `NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs` + +Test cases: +- `Constructor_InitializesFromModel` +- `AddConnection_CreatesNewEntryAndSelectsIt` +- `DeleteConnection_RemovesSelectedEntry` +- `SelectedConnection_UpdatesHasSelection` +- `HasSelection_IsFalseWhenNothingSelected` +- `ConnectionCount_ReflectsCollectionSize` + +**Verification:** All tests pass. + +--- + +## Summary + +| Batch | Tasks | Description | +|-------|-------|-------------| +| 1 | 1-4 | Data models (ConnectionProvider, ConnectionStringEntry, ConnectionStringsSection, ConfigModel update) | +| 2 | 5-7 | ViewModels (ConnectionStringEntryViewModel, ConnectionStringsFormViewModel, IConnectionTestService) | +| 3 | 8-10 | Views (ConnectionStringsFormView, DataTemplates, code-behind) | +| 4 | 11-12 | Integration (MainWindowViewModel, DI registration) and unit tests | + +## Post-Implementation Verification + +After all tasks complete: + +1. `dotnet build NEW/JdeScoping.slnx` - should succeed +2. `dotnet test NEW/JdeScoping.slnx` - all tests should pass +3. Run ConfigManager app: + - Open a config folder + - Navigate to Settings β†’ ConnectionStrings + - Add a new SqlServer connection + - Fill in fields, verify preview updates + - Test connection works + - Save config, verify appsettings.json updated