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.
This commit is contained in:
@@ -74,6 +74,9 @@ public partial class App : Avalonia.Application
|
|||||||
// Runtime Validation Services
|
// Runtime Validation Services
|
||||||
services.AddSingleton<IRuntimeConfigValidationService, RuntimeConfigValidationService>();
|
services.AddSingleton<IRuntimeConfigValidationService, RuntimeConfigValidationService>();
|
||||||
|
|
||||||
|
// Connection Testing
|
||||||
|
services.AddSingleton<IConnectionTestService, ConnectionTestService>();
|
||||||
|
|
||||||
// ViewModels
|
// ViewModels
|
||||||
services.AddTransient<MainWindowViewModel>();
|
services.AddTransient<MainWindowViewModel>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.Converters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a ConnectionProvider value to visibility (bool) based on whether it matches the target provider.
|
||||||
|
/// </summary>
|
||||||
|
public class ProviderToVisibilityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter instance for SqlServer provider visibility.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ProviderToVisibilityConverter SqlServer = new(ConnectionProvider.SqlServer);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converter instance for Oracle provider visibility.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ProviderToVisibilityConverter Oracle = new(ConnectionProvider.Oracle);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converter instance for Generic provider visibility.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ProviderToVisibilityConverter Generic = new(ConnectionProvider.Generic);
|
||||||
|
|
||||||
|
private readonly ConnectionProvider _targetProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProviderToVisibilityConverter"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetProvider">The provider to match for visibility.</param>
|
||||||
|
public ProviderToVisibilityConverter(ConnectionProvider targetProvider)
|
||||||
|
{
|
||||||
|
_targetProvider = targetProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a ConnectionProvider to a boolean indicating visibility.
|
||||||
|
/// </summary>
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is ConnectionProvider provider)
|
||||||
|
{
|
||||||
|
return provider == _targetProvider;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Not implemented - this is a one-way converter.
|
||||||
|
/// </summary>
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.*" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.*" />
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.*" Condition="'$(Configuration)' == 'Debug'" />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.*" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.*" />
|
<PackageReference Include="DiffPlex" Version="1.7.*" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.*" />
|
||||||
<PackageReference Include="MessageBox.Avalonia" Version="3.1.*" />
|
<PackageReference Include="MessageBox.Avalonia" Version="3.1.*" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.*" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.*" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class ConfigModel
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the connection strings for external data sources.
|
/// Gets or sets the connection strings for external data sources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, string> ConnectionStrings { get; set; } = new();
|
public ConnectionStringsSection ConnectionStrings { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the secure store configuration.
|
/// Gets or sets the secure store configuration.
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace JdeScoping.ConfigManager.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Database provider types supported by the ConnectionStrings editor.
|
||||||
|
/// </summary>
|
||||||
|
public enum ConnectionProvider
|
||||||
|
{
|
||||||
|
Generic,
|
||||||
|
SqlServer,
|
||||||
|
Oracle
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
namespace JdeScoping.ConfigManager.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single connection string entry with provider-specific fields.
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the connection string based on the Provider type.
|
||||||
|
/// </summary>
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace JdeScoping.ConfigManager.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration section for connection strings.
|
||||||
|
/// </summary>
|
||||||
|
public class ConnectionStringsSection
|
||||||
|
{
|
||||||
|
public List<ConnectionStringEntry> Entries { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for testing database connections.
|
||||||
|
/// </summary>
|
||||||
|
public class ConnectionTestService : IConnectionTestService
|
||||||
|
{
|
||||||
|
private const int ConnectionTimeoutSeconds = 10;
|
||||||
|
|
||||||
|
public async Task<ConnectionTestResult> 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<ConnectionTestResult> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of testing a database connection.
|
||||||
|
/// </summary>
|
||||||
|
public class ConnectionTestResult
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public string Message { get; init; } = string.Empty;
|
||||||
|
public TimeSpan? Duration { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for testing database connections.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConnectionTestService
|
||||||
|
{
|
||||||
|
Task<ConnectionTestResult> TestConnectionAsync(string connectionString, ConnectionProvider provider, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
+325
@@ -0,0 +1,325 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.ViewModels.Forms;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for editing a single connection string entry.
|
||||||
|
/// </summary>
|
||||||
|
public class ConnectionStringEntryViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly ConnectionStringEntry _model;
|
||||||
|
private readonly Action _onChanged;
|
||||||
|
private bool _isPasswordVisible;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ConnectionStringEntryViewModel"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The connection string entry model.</param>
|
||||||
|
/// <param name="onChanged">The action to invoke when any property changes.</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the connection string name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _model.Name;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.Name != value)
|
||||||
|
{
|
||||||
|
_model.Name = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the database provider type.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the SQL Server server name.
|
||||||
|
/// </summary>
|
||||||
|
public string? Server
|
||||||
|
{
|
||||||
|
get => _model.Server;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.Server != value)
|
||||||
|
{
|
||||||
|
_model.Server = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(ServerDisplay));
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the database name.
|
||||||
|
/// </summary>
|
||||||
|
public string? Database
|
||||||
|
{
|
||||||
|
get => _model.Database;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.Database != value)
|
||||||
|
{
|
||||||
|
_model.Database = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user ID.
|
||||||
|
/// </summary>
|
||||||
|
public string? UserId
|
||||||
|
{
|
||||||
|
get => _model.UserId;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.UserId != value)
|
||||||
|
{
|
||||||
|
_model.UserId = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password.
|
||||||
|
/// </summary>
|
||||||
|
public string? Password
|
||||||
|
{
|
||||||
|
get => _model.Password;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.Password != value)
|
||||||
|
{
|
||||||
|
_model.Password = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the encrypt setting for SQL Server.
|
||||||
|
/// </summary>
|
||||||
|
public string Encrypt
|
||||||
|
{
|
||||||
|
get => _model.Encrypt;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.Encrypt != value)
|
||||||
|
{
|
||||||
|
_model.Encrypt = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to trust the server certificate for SQL Server.
|
||||||
|
/// </summary>
|
||||||
|
public bool TrustServerCertificate
|
||||||
|
{
|
||||||
|
get => _model.TrustServerCertificate;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.TrustServerCertificate != value)
|
||||||
|
{
|
||||||
|
_model.TrustServerCertificate = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the connection timeout in seconds.
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionTimeout
|
||||||
|
{
|
||||||
|
get => _model.ConnectionTimeout;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.ConnectionTimeout != value)
|
||||||
|
{
|
||||||
|
_model.ConnectionTimeout = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the application name for SQL Server.
|
||||||
|
/// </summary>
|
||||||
|
public string? ApplicationName
|
||||||
|
{
|
||||||
|
get => _model.ApplicationName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.ApplicationName != value)
|
||||||
|
{
|
||||||
|
_model.ApplicationName = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Oracle host name.
|
||||||
|
/// </summary>
|
||||||
|
public string? Host
|
||||||
|
{
|
||||||
|
get => _model.Host;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.Host != value)
|
||||||
|
{
|
||||||
|
_model.Host = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(ServerDisplay));
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Oracle port number.
|
||||||
|
/// </summary>
|
||||||
|
public int Port
|
||||||
|
{
|
||||||
|
get => _model.Port;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.Port != value)
|
||||||
|
{
|
||||||
|
_model.Port = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Oracle service name.
|
||||||
|
/// </summary>
|
||||||
|
public string? ServiceName
|
||||||
|
{
|
||||||
|
get => _model.ServiceName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.ServiceName != value)
|
||||||
|
{
|
||||||
|
_model.ServiceName = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the raw connection string for generic providers.
|
||||||
|
/// </summary>
|
||||||
|
public string? RawConnectionString
|
||||||
|
{
|
||||||
|
get => _model.RawConnectionString;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model.RawConnectionString != value)
|
||||||
|
{
|
||||||
|
_model.RawConnectionString = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(GeneratedConnectionString));
|
||||||
|
_onChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the password is visible.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsPasswordVisible
|
||||||
|
{
|
||||||
|
get => _isPasswordVisible;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isPasswordVisible != value)
|
||||||
|
{
|
||||||
|
_isPasswordVisible = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command to toggle password visibility.
|
||||||
|
/// </summary>
|
||||||
|
public ICommand TogglePasswordVisibilityCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the generated connection string based on current property values.
|
||||||
|
/// </summary>
|
||||||
|
public string GeneratedConnectionString => _model.GenerateConnectionString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the display string for the provider type.
|
||||||
|
/// </summary>
|
||||||
|
public string ProviderDisplay => Provider.ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the server/host display value based on provider type.
|
||||||
|
/// Returns Server for SqlServer, Host for Oracle, or "-" for Generic.
|
||||||
|
/// </summary>
|
||||||
|
public string ServerDisplay => Provider switch
|
||||||
|
{
|
||||||
|
ConnectionProvider.SqlServer => Server ?? string.Empty,
|
||||||
|
ConnectionProvider.Oracle => Host ?? string.Empty,
|
||||||
|
_ => "-"
|
||||||
|
};
|
||||||
|
}
|
||||||
+260
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for editing the ConnectionStrings configuration section.
|
||||||
|
/// Manages a collection of connection string entries with add, delete, validate, and test commands.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ConnectionStringsFormViewModel"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The connection strings section model.</param>
|
||||||
|
/// <param name="onChanged">The action to invoke when any property changes.</param>
|
||||||
|
/// <param name="dialogService">The dialog service for showing messages and confirmations.</param>
|
||||||
|
/// <param name="connectionTestService">The service for testing database connections.</param>
|
||||||
|
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<ConnectionStringEntryViewModel>();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of connection string entry view models.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<ConnectionStringEntryViewModel> Connections { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the currently selected connection.
|
||||||
|
/// </summary>
|
||||||
|
public ConnectionStringEntryViewModel? SelectedConnection
|
||||||
|
{
|
||||||
|
get => _selectedConnection;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedConnection, value))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(HasSelection));
|
||||||
|
RaiseCommandsCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether a connection is selected.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasSelection => SelectedConnection != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether a connection test is in progress.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsTesting
|
||||||
|
{
|
||||||
|
get => _isTesting;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _isTesting, value))
|
||||||
|
{
|
||||||
|
RaiseCommandsCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of connections in the collection.
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionCount => Connections.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of available connection providers.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<ConnectionProvider> AvailableProviders { get; } =
|
||||||
|
Enum.GetValues<ConnectionProvider>().ToList().AsReadOnly();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of available encrypt options for SQL Server connections.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<string> EncryptOptions { get; } =
|
||||||
|
new List<string> { "True", "False", "Strict" }.AsReadOnly();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command for adding a new connection.
|
||||||
|
/// </summary>
|
||||||
|
public ICommand AddConnectionCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command for deleting the selected connection.
|
||||||
|
/// </summary>
|
||||||
|
public ICommand DeleteConnectionCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command for validating the selected connection string.
|
||||||
|
/// </summary>
|
||||||
|
public ICommand ValidateConnectionCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command for testing the selected connection.
|
||||||
|
/// </summary>
|
||||||
|
public ICommand TestConnectionCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new connection entry with default values.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the selected connection after user confirmation.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the selected connection has a non-empty generated connection string.
|
||||||
|
/// </summary>
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests the selected connection by attempting to connect to the database.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raises CanExecuteChanged for all commands that depend on selection state.
|
||||||
|
/// </summary>
|
||||||
|
private void RaiseCommandsCanExecuteChanged()
|
||||||
|
{
|
||||||
|
(DeleteConnectionCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(ValidateConnectionCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(TestConnectionCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
private readonly ISecureStoreManager _secureStoreManager;
|
private readonly ISecureStoreManager _secureStoreManager;
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly IRuntimeConfigValidationService _runtimeValidationService;
|
private readonly IRuntimeConfigValidationService _runtimeValidationService;
|
||||||
|
private readonly IConnectionTestService _connectionTestService;
|
||||||
private readonly ILogger<MainWindowViewModel>? _logger;
|
private readonly ILogger<MainWindowViewModel>? _logger;
|
||||||
|
|
||||||
private string _configFolderPath = "No folder selected";
|
private string _configFolderPath = "No folder selected";
|
||||||
@@ -203,6 +204,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
ISecureStoreManager secureStoreManager,
|
ISecureStoreManager secureStoreManager,
|
||||||
IClipboardService clipboardService,
|
IClipboardService clipboardService,
|
||||||
IRuntimeConfigValidationService runtimeValidationService,
|
IRuntimeConfigValidationService runtimeValidationService,
|
||||||
|
IConnectionTestService connectionTestService,
|
||||||
ILogger<MainWindowViewModel>? logger)
|
ILogger<MainWindowViewModel>? logger)
|
||||||
{
|
{
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
@@ -214,6 +216,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
_secureStoreManager = secureStoreManager;
|
_secureStoreManager = secureStoreManager;
|
||||||
_clipboardService = clipboardService;
|
_clipboardService = clipboardService;
|
||||||
_runtimeValidationService = runtimeValidationService;
|
_runtimeValidationService = runtimeValidationService;
|
||||||
|
_connectionTestService = connectionTestService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync);
|
OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync);
|
||||||
@@ -257,6 +260,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
new SecureStoreManager(),
|
new SecureStoreManager(),
|
||||||
new NullClipboardService(),
|
new NullClipboardService(),
|
||||||
new RuntimeConfigValidationService(new SecureStoreManager()),
|
new RuntimeConfigValidationService(new SecureStoreManager()),
|
||||||
|
new ConnectionTestService(),
|
||||||
null)
|
null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -457,6 +461,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
settingsFolder.Children.Add(new TreeNodeViewModel("Ldap", "👥", TreeNodeType.SettingsSection) { SectionKey = "Ldap" });
|
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("Search", "🔍", TreeNodeType.SettingsSection) { SectionKey = "Search" });
|
||||||
settingsFolder.Children.Add(new TreeNodeViewModel("ExcelExport", "📊", TreeNodeType.SettingsSection) { SectionKey = "ExcelExport" });
|
settingsFolder.Children.Add(new TreeNodeViewModel("ExcelExport", "📊", TreeNodeType.SettingsSection) { SectionKey = "ExcelExport" });
|
||||||
|
settingsFolder.Children.Add(new TreeNodeViewModel("ConnectionStrings", "🔗", TreeNodeType.SettingsSection) { SectionKey = "ConnectionStrings" });
|
||||||
TreeNodes.Add(settingsFolder);
|
TreeNodes.Add(settingsFolder);
|
||||||
|
|
||||||
// Pipelines folder
|
// Pipelines folder
|
||||||
@@ -563,6 +568,11 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
"Ldap" => new LdapFormViewModel(_appSettings.Ldap, MarkAsChanged),
|
"Ldap" => new LdapFormViewModel(_appSettings.Ldap, MarkAsChanged),
|
||||||
"Search" => new SearchFormViewModel(_appSettings.Search, MarkAsChanged),
|
"Search" => new SearchFormViewModel(_appSettings.Search, MarkAsChanged),
|
||||||
"ExcelExport" => new ExcelExportFormViewModel(_appSettings.ExcelExport, 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
|
_ when _selectedNode.NodeType == TreeNodeType.Pipeline && _pipelines != null
|
||||||
=> _pipelines.Pipelines.TryGetValue(_selectedNode.SectionKey!, out var pipeline)
|
=> _pipelines.Pipelines.TryGetValue(_selectedNode.SectionKey!, out var pipeline)
|
||||||
? new PipelineEditorViewModel(_selectedNode.SectionKey!, pipeline, GetAvailableConnections(), MarkAsChanged)
|
? new PipelineEditorViewModel(_selectedNode.SectionKey!, pipeline, GetAvailableConnections(), MarkAsChanged)
|
||||||
|
|||||||
@@ -0,0 +1,421 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Forms"
|
||||||
|
xmlns:models="using:JdeScoping.ConfigManager.Models"
|
||||||
|
xmlns:local="using:JdeScoping.ConfigManager.Converters"
|
||||||
|
x:Class="JdeScoping.ConfigManager.Views.Forms.ConnectionStringsFormView"
|
||||||
|
x:DataType="vm:ConnectionStringsFormViewModel">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<!-- SqlServer Provider Template -->
|
||||||
|
<DataTemplate x:Key="SqlServerTemplate" x:DataType="vm:ConnectionStringEntryViewModel">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<!-- Server & Database Row -->
|
||||||
|
<Grid ColumnDefinitions="*,16,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Server" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding Server}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="localhost\SQLEXPRESS"/>
|
||||||
|
<TextBlock Text="Server name or IP with optional instance"
|
||||||
|
Foreground="#5C6A7A" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Database" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding Database}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="ScopingTool"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- UserId & Password Row -->
|
||||||
|
<Grid ColumnDefinitions="*,16,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="User Id" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding UserId}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="sa"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Password" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
Text="{Binding Password}"
|
||||||
|
PasswordChar="•"
|
||||||
|
RevealPassword="{Binding IsPasswordVisible}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"/>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Command="{Binding TogglePasswordVisibilityCommand}"
|
||||||
|
Background="#3D4550" Foreground="#E6EDF5"
|
||||||
|
Width="36" Height="36" Margin="8,0,0,0"
|
||||||
|
CornerRadius="4" Padding="0"
|
||||||
|
ToolTip.Tip="Show/Hide password">
|
||||||
|
<TextBlock Text="👁" FontSize="14" HorizontalAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Encrypt & TrustServerCertificate Row -->
|
||||||
|
<Grid ColumnDefinitions="*,16,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Encrypt" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<ComboBox ItemsSource="{Binding $parent[UserControl].((vm:ConnectionStringsFormViewModel)DataContext).EncryptOptions}"
|
||||||
|
SelectedItem="{Binding Encrypt}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
<TextBlock Text="Options: True, False, Strict"
|
||||||
|
Foreground="#5C6A7A" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text=" " FontSize="12"/>
|
||||||
|
<CheckBox IsChecked="{Binding TrustServerCertificate}"
|
||||||
|
Foreground="#E6EDF5" Height="36"
|
||||||
|
VerticalContentAlignment="Center">
|
||||||
|
<TextBlock Text="Trust Server Certificate" Foreground="#E6EDF5"/>
|
||||||
|
</CheckBox>
|
||||||
|
<TextBlock Text="Skip certificate validation (dev only)"
|
||||||
|
Foreground="#FF6B6B" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Timeout & ApplicationName Row -->
|
||||||
|
<Grid ColumnDefinitions="*,16,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Connection Timeout (seconds)" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<NumericUpDown Value="{Binding ConnectionTimeout}"
|
||||||
|
Minimum="1" Maximum="600"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"/>
|
||||||
|
<TextBlock Text="Default: 30 seconds"
|
||||||
|
Foreground="#5C6A7A" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Application Name" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding ApplicationName}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="JdeScopingTool"/>
|
||||||
|
<TextBlock Text="Identifies app in SQL Server logs"
|
||||||
|
Foreground="#5C6A7A" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Connection String Preview -->
|
||||||
|
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||||
|
CornerRadius="4" Padding="12" Margin="0,8,0,0">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="CONNECTION STRING PREVIEW"
|
||||||
|
Foreground="#5C6A7A" FontSize="11" FontWeight="Medium"/>
|
||||||
|
<TextBlock Text="{Binding GeneratedConnectionString}"
|
||||||
|
Foreground="#9BA8B8" FontSize="11"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Oracle Provider Template -->
|
||||||
|
<DataTemplate x:Key="OracleTemplate" x:DataType="vm:ConnectionStringEntryViewModel">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<!-- Host & Port Row -->
|
||||||
|
<Grid ColumnDefinitions="*,16,120">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Host" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding Host}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="oracle-server.company.com"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Port" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<NumericUpDown Value="{Binding Port}"
|
||||||
|
Minimum="1" Maximum="65535"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"/>
|
||||||
|
<TextBlock Text="Default: 1521"
|
||||||
|
Foreground="#5C6A7A" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Service Name -->
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Service Name" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding ServiceName}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="JDEPROD"/>
|
||||||
|
<TextBlock Text="Oracle service name (not SID)"
|
||||||
|
Foreground="#5C6A7A" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- UserId & Password Row -->
|
||||||
|
<Grid ColumnDefinitions="*,16,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="User Id" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding UserId}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="jde_readonly"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Password" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
Text="{Binding Password}"
|
||||||
|
PasswordChar="•"
|
||||||
|
RevealPassword="{Binding IsPasswordVisible}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"/>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Command="{Binding TogglePasswordVisibilityCommand}"
|
||||||
|
Background="#3D4550" Foreground="#E6EDF5"
|
||||||
|
Width="36" Height="36" Margin="8,0,0,0"
|
||||||
|
CornerRadius="4" Padding="0"
|
||||||
|
ToolTip.Tip="Show/Hide password">
|
||||||
|
<TextBlock Text="👁" FontSize="14" HorizontalAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Connection Timeout -->
|
||||||
|
<StackPanel Spacing="4" MaxWidth="200" HorizontalAlignment="Left">
|
||||||
|
<TextBlock Text="Connection Timeout (seconds)" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<NumericUpDown Value="{Binding ConnectionTimeout}"
|
||||||
|
Minimum="1" Maximum="600"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Connection String Preview -->
|
||||||
|
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||||
|
CornerRadius="4" Padding="12" Margin="0,8,0,0">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="CONNECTION STRING PREVIEW"
|
||||||
|
Foreground="#5C6A7A" FontSize="11" FontWeight="Medium"/>
|
||||||
|
<TextBlock Text="{Binding GeneratedConnectionString}"
|
||||||
|
Foreground="#9BA8B8" FontSize="11"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Generic Provider Template -->
|
||||||
|
<DataTemplate x:Key="GenericTemplate" x:DataType="vm:ConnectionStringEntryViewModel">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<!-- Info Banner -->
|
||||||
|
<Border Background="#1A2233" BorderBrush="#3B82F6" BorderThickness="1"
|
||||||
|
CornerRadius="4" Padding="12">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||||
|
<TextBlock Text="ℹ" FontSize="16" Foreground="#3B82F6" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Use Generic for unsupported providers or custom connection strings"
|
||||||
|
Foreground="#9BA8B8" FontSize="12" TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Connection String TextArea -->
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Connection String" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding RawConnectionString}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550"
|
||||||
|
FontFamily="JetBrains Mono" FontSize="12"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
MinHeight="120"
|
||||||
|
Watermark="Data Source=myserver;Initial Catalog=mydb;..."/>
|
||||||
|
<TextBlock Text="Enter the full connection string"
|
||||||
|
Foreground="#5C6A7A" FontSize="11"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="24" MaxWidth="800">
|
||||||
|
<!-- Header -->
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Connection Strings"
|
||||||
|
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Connections List Section -->
|
||||||
|
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="16">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<!-- Section Header with Count Badge -->
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<TextBlock Text="Connections" Foreground="#E6EDF5"
|
||||||
|
FontWeight="SemiBold" FontSize="14"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Border Background="#3D4550" CornerRadius="10"
|
||||||
|
Padding="8,2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding ConnectionCount}"
|
||||||
|
Foreground="#9BA8B8" FontSize="11"
|
||||||
|
FontFamily="JetBrains Mono"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- DataGrid -->
|
||||||
|
<DataGrid ItemsSource="{Binding Connections}"
|
||||||
|
SelectedItem="{Binding SelectedConnection}"
|
||||||
|
SelectionMode="Single"
|
||||||
|
Height="200"
|
||||||
|
Background="#0D0F12"
|
||||||
|
RowBackground="#0D0F12"
|
||||||
|
BorderBrush="#2D3540"
|
||||||
|
BorderThickness="1"
|
||||||
|
GridLinesVisibility="Horizontal"
|
||||||
|
HorizontalGridLinesBrush="#2D3540"
|
||||||
|
HeadersVisibility="Column"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
CanUserReorderColumns="False"
|
||||||
|
CanUserResizeColumns="True"
|
||||||
|
CanUserSortColumns="True">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Name"
|
||||||
|
Binding="{Binding Name}"
|
||||||
|
Width="*"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Foreground="#E6EDF5"/>
|
||||||
|
<DataGridTextColumn Header="Provider"
|
||||||
|
Binding="{Binding ProviderDisplay}"
|
||||||
|
Width="100"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Foreground="#9BA8B8"/>
|
||||||
|
<DataGridTextColumn Header="Server"
|
||||||
|
Binding="{Binding ServerDisplay}"
|
||||||
|
Width="150"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Foreground="#9BA8B8"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Command="{Binding AddConnectionCommand}"
|
||||||
|
Background="#3B82F6" Foreground="White"
|
||||||
|
Padding="12,6" CornerRadius="4">
|
||||||
|
<TextBlock Text="Add" FontWeight="Medium"/>
|
||||||
|
</Button>
|
||||||
|
<Button Command="{Binding DeleteConnectionCommand}"
|
||||||
|
Background="#DC2626" Foreground="White"
|
||||||
|
Padding="12,6" CornerRadius="4">
|
||||||
|
<TextBlock Text="Delete" FontWeight="Medium"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Placeholder Section (when no connection is selected) -->
|
||||||
|
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="48"
|
||||||
|
IsVisible="{Binding !HasSelection}">
|
||||||
|
<TextBlock Text="Select a connection string to edit"
|
||||||
|
Foreground="#5C6A7A" FontSize="14"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Edit Form Section (when a connection is selected) -->
|
||||||
|
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="16"
|
||||||
|
IsVisible="{Binding HasSelection}">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<!-- Edit Header -->
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<TextBlock Text="Edit Connection:" Foreground="#E6EDF5"
|
||||||
|
FontWeight="SemiBold" FontSize="14"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding SelectedConnection.Name}"
|
||||||
|
Foreground="#3B82F6" FontSize="14"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Name and Provider Fields -->
|
||||||
|
<Grid ColumnDefinitions="*,16,*">
|
||||||
|
<!-- Name -->
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Name"
|
||||||
|
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<TextBox Text="{Binding SelectedConnection.Name}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
FontFamily="JetBrains Mono"
|
||||||
|
Watermark="ConnectionName"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Provider -->
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Provider"
|
||||||
|
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||||
|
<ComboBox ItemsSource="{Binding AvailableProviders}"
|
||||||
|
SelectedItem="{Binding SelectedConnection.Provider}"
|
||||||
|
Background="#232A35" Foreground="#E6EDF5"
|
||||||
|
BorderBrush="#3D4550" Height="36"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<Border Height="1" Background="#2D3540" Margin="0,8"/>
|
||||||
|
|
||||||
|
<!-- Provider-specific fields - SqlServer -->
|
||||||
|
<ContentControl Content="{Binding SelectedConnection}"
|
||||||
|
ContentTemplate="{StaticResource SqlServerTemplate}"
|
||||||
|
IsVisible="{Binding SelectedConnection.Provider, Converter={x:Static local:ProviderToVisibilityConverter.SqlServer}}"/>
|
||||||
|
|
||||||
|
<!-- Provider-specific fields - Oracle -->
|
||||||
|
<ContentControl Content="{Binding SelectedConnection}"
|
||||||
|
ContentTemplate="{StaticResource OracleTemplate}"
|
||||||
|
IsVisible="{Binding SelectedConnection.Provider, Converter={x:Static local:ProviderToVisibilityConverter.Oracle}}"/>
|
||||||
|
|
||||||
|
<!-- Provider-specific fields - Generic -->
|
||||||
|
<ContentControl Content="{Binding SelectedConnection}"
|
||||||
|
ContentTemplate="{StaticResource GenericTemplate}"
|
||||||
|
IsVisible="{Binding SelectedConnection.Provider, Converter={x:Static local:ProviderToVisibilityConverter.Generic}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Action Buttons Section (when a connection is selected) -->
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
IsVisible="{Binding HasSelection}">
|
||||||
|
<Button Command="{Binding ValidateConnectionCommand}"
|
||||||
|
Background="#3D4550" Foreground="#E6EDF5"
|
||||||
|
Padding="16,8" CornerRadius="4">
|
||||||
|
<TextBlock Text="Validate" FontWeight="Medium"/>
|
||||||
|
</Button>
|
||||||
|
<Button Command="{Binding TestConnectionCommand}"
|
||||||
|
Background="#10B981" Foreground="White"
|
||||||
|
Padding="16,8" CornerRadius="4">
|
||||||
|
<TextBlock Text="Test Connection" FontWeight="Medium"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.Views.Forms;
|
||||||
|
|
||||||
|
public partial class ConnectionStringsFormView : UserControl
|
||||||
|
{
|
||||||
|
public ConnectionStringsFormView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+182
@@ -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<ArgumentNullException>(() => new ConnectionStringEntryViewModel(null!, () => { }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ThrowsOnNullOnChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ConnectionStringEntry();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Should.Throw<ArgumentNullException>(() => 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("-");
|
||||||
|
}
|
||||||
|
}
|
||||||
+167
@@ -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<IDialogService>();
|
||||||
|
_connectionTestService = Substitute.For<IConnectionTestService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_InitializesFromModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ConnectionStringsSection
|
||||||
|
{
|
||||||
|
Entries = new List<ConnectionStringEntry>
|
||||||
|
{
|
||||||
|
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<ArgumentNullException>(() =>
|
||||||
|
new ConnectionStringsFormViewModel(null!, () => { }, _dialogService, _connectionTestService));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ThrowsOnNullOnChanged()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ConnectionStringsSection();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Should.Throw<ArgumentNullException>(() =>
|
||||||
|
new ConnectionStringsFormViewModel(model, null!, _dialogService, _connectionTestService));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ThrowsOnNullConnectionTestService()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new ConnectionStringsSection();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Should.Throw<ArgumentNullException>(() =>
|
||||||
|
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<ConnectionStringEntry>
|
||||||
|
{
|
||||||
|
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<ConnectionStringEntry>
|
||||||
|
{
|
||||||
|
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<ConnectionStringEntry>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ public class MainWindowViewModelTests
|
|||||||
private readonly ISecureStoreManager _secureStoreManager;
|
private readonly ISecureStoreManager _secureStoreManager;
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly IRuntimeConfigValidationService _runtimeValidationService;
|
private readonly IRuntimeConfigValidationService _runtimeValidationService;
|
||||||
|
private readonly IConnectionTestService _connectionTestService;
|
||||||
private readonly ILogger<MainWindowViewModel> _logger;
|
private readonly ILogger<MainWindowViewModel> _logger;
|
||||||
|
|
||||||
public MainWindowViewModelTests()
|
public MainWindowViewModelTests()
|
||||||
@@ -32,6 +33,7 @@ public class MainWindowViewModelTests
|
|||||||
_secureStoreManager = Substitute.For<ISecureStoreManager>();
|
_secureStoreManager = Substitute.For<ISecureStoreManager>();
|
||||||
_clipboardService = Substitute.For<IClipboardService>();
|
_clipboardService = Substitute.For<IClipboardService>();
|
||||||
_runtimeValidationService = Substitute.For<IRuntimeConfigValidationService>();
|
_runtimeValidationService = Substitute.For<IRuntimeConfigValidationService>();
|
||||||
|
_connectionTestService = Substitute.For<IConnectionTestService>();
|
||||||
_logger = Substitute.For<ILogger<MainWindowViewModel>>();
|
_logger = Substitute.For<ILogger<MainWindowViewModel>>();
|
||||||
|
|
||||||
_validationService.ValidateAppSettings(Arg.Any<ConfigModel>())
|
_validationService.ValidateAppSettings(Arg.Any<ConfigModel>())
|
||||||
@@ -284,7 +286,7 @@ public class MainWindowViewModelTests
|
|||||||
// Without a configured/open SecureStore, only Settings and Pipelines appear
|
// 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.Count.ShouldBe(2); // Settings, Pipelines (no Secure Store when not configured)
|
||||||
sut.TreeNodes[0].Name.ShouldBe("Settings");
|
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");
|
sut.TreeNodes[1].Name.ShouldBe("Pipelines");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,6 +384,7 @@ public class MainWindowViewModelTests
|
|||||||
_secureStoreManager,
|
_secureStoreManager,
|
||||||
_clipboardService,
|
_clipboardService,
|
||||||
_runtimeValidationService,
|
_runtimeValidationService,
|
||||||
|
_connectionTestService,
|
||||||
_logger);
|
_logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ConnectionStringEntry> Entries { get; set; } = new();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ViewModel Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ConnectionStringsFormViewModel
|
||||||
|
├── Connections : ObservableCollection<ConnectionStringEntryViewModel>
|
||||||
|
├── SelectedConnection : ConnectionStringEntryViewModel?
|
||||||
|
├── HasSelection : bool
|
||||||
|
├── AvailableProviders : IReadOnlyList<ConnectionProvider>
|
||||||
|
├── 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
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Database provider types supported by the ConnectionStrings editor.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single connection string entry with provider-specific fields.
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the connection string based on the Provider type.
|
||||||
|
/// </summary>
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration section for connection strings.
|
||||||
|
/// </summary>
|
||||||
|
public class ConnectionStringsSection
|
||||||
|
{
|
||||||
|
public List<ConnectionStringEntry> 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<ConnectionStringEntryViewModel>`
|
||||||
|
- `SelectedConnection : ConnectionStringEntryViewModel?`
|
||||||
|
- `HasSelection : bool` (computed from SelectedConnection != null)
|
||||||
|
- `ConnectionCount : int` (computed from Connections.Count)
|
||||||
|
- `AvailableProviders : IReadOnlyList<ConnectionProvider>`
|
||||||
|
- `EncryptOptions : IReadOnlyList<string>` = ["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<ConnectionTestResult> 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
|
||||||
Reference in New Issue
Block a user