connection string/url parsing and generation in the server configuration dialog
This commit is contained in:
parent
a5cb6ef7d4
commit
1d53ca2fc2
11 changed files with 410 additions and 62 deletions
|
|
@ -37,5 +37,7 @@
|
|||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
|
||||
<PackageVersion Include="Avalonia.Headless" Version="11.3.4" />
|
||||
<PackageVersion Include="Avalonia.Headless.XUnit" Version="11.3.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -38,30 +38,42 @@ public sealed class NpgsqlCodec : IConnectionStringCodec
|
|||
// Hosts and Ports
|
||||
if (dict.TryGetValue("Host", out var hostVal) || dict.TryGetValue("Server", out hostVal) || dict.TryGetValue("Servers", out hostVal))
|
||||
{
|
||||
var hosts = SplitList(hostVal).ToList();
|
||||
List<ushort?> portsPerHost = new();
|
||||
var rawHosts = SplitList(hostVal).ToList();
|
||||
var hosts = new List<string>(rawHosts.Count);
|
||||
var portsPerHost = new List<ushort?>(rawHosts.Count);
|
||||
|
||||
// First, extract inline ports from each host entry (e.g., host:5432 or [::1]:5432)
|
||||
foreach (var raw in rawHosts)
|
||||
{
|
||||
ParseHostPort(raw, out var hostOnly, out var inlinePort);
|
||||
hosts.Add(hostOnly);
|
||||
portsPerHost.Add(inlinePort);
|
||||
}
|
||||
|
||||
// Then, merge values from Port key: single port applies to all hosts missing a port;
|
||||
// list of ports applies 1:1 for hosts that still miss a port. Inline ports take precedence.
|
||||
if (dict.TryGetValue("Port", out var portVal))
|
||||
{
|
||||
var ports = SplitList(portVal).ToList();
|
||||
if (ports.Count == 1 && ushort.TryParse(ports[0], out var singlePort))
|
||||
if (ports.Count == 1 && ushort.TryParse(ports[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var singlePort))
|
||||
{
|
||||
foreach (var _ in hosts) portsPerHost.Add(singlePort);
|
||||
for (int i = 0; i < portsPerHost.Count; i++)
|
||||
if (!portsPerHost[i].HasValue)
|
||||
portsPerHost[i] = singlePort;
|
||||
}
|
||||
else if (ports.Count == hosts.Count)
|
||||
{
|
||||
foreach (var p in ports)
|
||||
for (int i = 0; i < ports.Count; i++)
|
||||
{
|
||||
if (ushort.TryParse(p, NumberStyles.Integer, CultureInfo.InvariantCulture, out var up))
|
||||
portsPerHost.Add(up);
|
||||
else
|
||||
portsPerHost.Add(null);
|
||||
if (!portsPerHost[i].HasValue && ushort.TryParse(ports[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var up))
|
||||
portsPerHost[i] = up;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < hosts.Count; i++)
|
||||
{
|
||||
ushort? port = i < portsPerHost.Count ? portsPerHost[i] : null;
|
||||
descriptor.AddHost(hosts[i], port);
|
||||
descriptor.AddHost(hosts[i], i < portsPerHost.Count ? portsPerHost[i] : null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +176,42 @@ public sealed class NpgsqlCodec : IConnectionStringCodec
|
|||
return s.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
private static void ParseHostPort(string hostPart, out string host, out ushort? port)
|
||||
{
|
||||
host = hostPart;
|
||||
port = null;
|
||||
if (string.IsNullOrWhiteSpace(hostPart)) return;
|
||||
|
||||
// IPv6 in brackets: [::1]:5432
|
||||
if (hostPart[0] == '[')
|
||||
{
|
||||
int end = hostPart.IndexOf(']');
|
||||
if (end > 0)
|
||||
{
|
||||
host = hostPart.Substring(1, end - 1);
|
||||
if (end + 1 < hostPart.Length && hostPart[end + 1] == ':')
|
||||
{
|
||||
var ps = hostPart.Substring(end + 2);
|
||||
if (ushort.TryParse(ps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
|
||||
port = p;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-IPv6: split on last ':' and ensure right side is numeric
|
||||
int colon = hostPart.LastIndexOf(':');
|
||||
if (colon > 0 && colon < hostPart.Length - 1)
|
||||
{
|
||||
var ps = hostPart.Substring(colon + 1);
|
||||
if (ushort.TryParse(ps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
|
||||
{
|
||||
host = hostPart.Substring(0, colon);
|
||||
port = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetFirst(Dictionary<string, string> dict, out string value, params string[] keys)
|
||||
{
|
||||
foreach (var k in keys)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ public sealed class LibpqCodec : IConnectionStringCodec
|
|||
{
|
||||
try
|
||||
{
|
||||
// Reject Npgsql-style strings that use ';' separators when forcing libpq
|
||||
if (input.IndexOf(';') >= 0)
|
||||
return Result.Fail<ConnectionDescriptor>("Semicolons are not valid separators in libpq connection strings");
|
||||
var kv = new PqConnectionStringParser(new PqConnectionStringTokenizer(input)).Parse();
|
||||
|
||||
// libpq keywords are case-insensitive; normalize to lower for lookup
|
||||
|
|
|
|||
|
|
@ -67,13 +67,29 @@ public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer
|
|||
{
|
||||
while (position < input.Length && char.IsWhiteSpace(input[position]))
|
||||
position++;
|
||||
// If a semicolon is encountered between pairs (which is not valid in libpq),
|
||||
// treat as immediate EOF so parser stops and leaves trailing data unparsed.
|
||||
if (position < input.Length && input[position] == ';')
|
||||
{
|
||||
position = input.Length; // force EOF
|
||||
}
|
||||
}
|
||||
|
||||
private string UnquotedString(bool forKeyword)
|
||||
{
|
||||
int start = position;
|
||||
while (++position < input.Length && !char.IsWhiteSpace(input[position]) && (!forKeyword || input[position] != '='))
|
||||
{ }
|
||||
while (++position < input.Length)
|
||||
{
|
||||
char c = input[position];
|
||||
// Libpq syntax does not use semicolons as pair separators; treat ';' as invalid here
|
||||
if (c == ';')
|
||||
{
|
||||
// Force tokenizer to stop and later cause a parse error by making GetValue/keyword incomplete
|
||||
break;
|
||||
}
|
||||
if (char.IsWhiteSpace(c)) break;
|
||||
if (forKeyword && c == '=') break;
|
||||
}
|
||||
return input.Substring(start, position - start);
|
||||
}
|
||||
|
||||
|
|
|
|||
87
pgLabII.Tests/Views/EditServerConfigurationWindowTests.cs
Normal file
87
pgLabII.Tests/Views/EditServerConfigurationWindowTests.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.Threading;
|
||||
using pgLabII.Model;
|
||||
using pgLabII.ViewModels;
|
||||
using pgLabII.Views;
|
||||
using Xunit;
|
||||
|
||||
namespace pgLabII.Tests.Views;
|
||||
|
||||
public class EditServerConfigurationWindowTests
|
||||
{
|
||||
[AvaloniaFact]
|
||||
public void Parse_and_Generate_roundtrip_via_UI_bindings()
|
||||
{
|
||||
// Arrange: initialize Avalonia headless app once for the test
|
||||
var vm = new EditServerConfigurationViewModel(new ServerConfiguration());
|
||||
var window = new EditServerConfigurationWindow(vm);
|
||||
|
||||
// Act: set an URL input, auto mode, then parse
|
||||
vm.InputConnectionString = "postgresql://user:pass@localhost:5433/mydb?sslmode=require";
|
||||
vm.ForcedFormat = EditServerConfigurationViewModel.ForcedFormatOption.Auto;
|
||||
vm.ParseConnectionStringCommand.Execute().Subscribe();
|
||||
|
||||
// Assert fields updated
|
||||
Assert.Equal("localhost", vm.Configuration.Host);
|
||||
Assert.Equal((ushort)5433, vm.Configuration.Port);
|
||||
Assert.Equal("mydb", vm.Configuration.InitialDatabase);
|
||||
Assert.Equal("user", vm.Configuration.User.Name);
|
||||
Assert.Equal("pass", vm.Configuration.User.Password);
|
||||
Assert.Equal(Npgsql.SslMode.Require, vm.Configuration.DefaultSslMode);
|
||||
|
||||
// Generate back as libpq and validate
|
||||
vm.OutputFormat = pgLabII.PgUtils.ConnectionStrings.ConnStringFormat.Libpq;
|
||||
vm.GenerateConnectionStringCommand.Execute().Subscribe();
|
||||
var outStr = vm.OutputConnectionString;
|
||||
Assert.Contains("host=localhost", outStr);
|
||||
Assert.Contains("port=5433", outStr);
|
||||
Assert.Contains("dbname=mydb", outStr);
|
||||
Assert.Contains("user=user", outStr);
|
||||
Assert.Contains("password=pass", outStr);
|
||||
Assert.Contains("sslmode=require", outStr);
|
||||
|
||||
window.Close();
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Forced_format_overrides_auto_detection()
|
||||
{
|
||||
var vm = new EditServerConfigurationViewModel(new ServerConfiguration());
|
||||
|
||||
// A semicolon-separated string that could be auto-detected as Npgsql
|
||||
vm.InputConnectionString = "Host=server;Username=bob;Password=secret;Database=db1;SSL Mode=Require";
|
||||
|
||||
// Force interpret as libpq should fail to parse (libpq uses spaces) and keep defaults
|
||||
vm.ForcedFormat = EditServerConfigurationViewModel.ForcedFormatOption.Libpq;
|
||||
vm.ParseConnectionStringCommand.Execute().Subscribe();
|
||||
|
||||
// Since forced libpq parse would fail, configuration should remain default (Host empty)
|
||||
Assert.True(string.IsNullOrEmpty(vm.Configuration.Host));
|
||||
|
||||
// Now set to Auto and parse again -> should detect Npgsql and parse
|
||||
vm.ForcedFormat = EditServerConfigurationViewModel.ForcedFormatOption.Auto;
|
||||
vm.ParseConnectionStringCommand.Execute().Subscribe();
|
||||
Assert.Equal("server", vm.Configuration.Host);
|
||||
Assert.Equal("db1", vm.Configuration.InitialDatabase);
|
||||
Assert.Equal("bob", vm.Configuration.User.Name);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Parse_Npgsql_with_inline_host_port_updates_all_fields()
|
||||
{
|
||||
var vm = new EditServerConfigurationViewModel(new ServerConfiguration());
|
||||
vm.InputConnectionString = "Host=host.docker.internal:5432;Database=kms_quartz;Username=postgres;Password=admin;Trust Server Certificate=true";
|
||||
vm.ForcedFormat = EditServerConfigurationViewModel.ForcedFormatOption.Auto;
|
||||
vm.ParseConnectionStringCommand.Execute().Subscribe();
|
||||
|
||||
Assert.Equal("host.docker.internal", vm.Configuration.Host);
|
||||
Assert.Equal((ushort)5432, vm.Configuration.Port);
|
||||
Assert.Equal("kms_quartz", vm.Configuration.InitialDatabase);
|
||||
Assert.Equal("postgres", vm.Configuration.User.Name);
|
||||
Assert.Equal("admin", vm.Configuration.User.Password);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Avalonia.Headless" />
|
||||
<PackageReference Include="Avalonia.Headless.XUnit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ public class LocalDb : DbContext
|
|||
// The following configures EF to create a Sqlite database file in the
|
||||
// special "local" folder for your platform.
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||
=> options.UseSqlite($"Data Source={DbPath}");
|
||||
{
|
||||
options.UseSqlite($"Data Source={DbPath}");
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@ using Npgsql;
|
|||
using pgLabII.ViewModels;
|
||||
using pgLabII.Views;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace pgLabII.Model;
|
||||
|
||||
public class ServerConfiguration : ReactiveObject
|
||||
public partial class ServerConfiguration : ReactiveObject
|
||||
{
|
||||
private Color _color;
|
||||
private bool _colorEnabled = true;
|
||||
private string initialDatabase = "";
|
||||
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[Reactive] private Guid _id = Guid.NewGuid();
|
||||
/// <summary>
|
||||
/// For the user to help him identify the item
|
||||
/// </summary>
|
||||
public string Name { get; set; } = "";
|
||||
[Reactive] private string _name = "";
|
||||
|
||||
public Color Color
|
||||
{
|
||||
|
|
@ -50,21 +50,19 @@ public class ServerConfiguration : ReactiveObject
|
|||
}
|
||||
}
|
||||
|
||||
public string Host { get; set; } = "";
|
||||
public ushort Port { get; set; } = 5432;
|
||||
public string InitialDatabase
|
||||
[Reactive] private string _host = "";
|
||||
[Reactive] private ushort _port = 5432;
|
||||
[Reactive] private string _initialDatabase = "";
|
||||
|
||||
[Reactive] private SslMode _sslMode = SslMode.Prefer;
|
||||
|
||||
// Explicit property wrapper to make compiled XAML binding findable
|
||||
public SslMode DefaultSslMode
|
||||
{
|
||||
get => initialDatabase;
|
||||
set
|
||||
{
|
||||
if (initialDatabase != value)
|
||||
{
|
||||
initialDatabase = value;
|
||||
this.RaisePropertyChanged();
|
||||
get => _sslMode;
|
||||
set => this.RaiseAndSetIfChanged(ref _sslMode, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public SslMode DefaultSslMode { get; set; } = SslMode.Prefer;
|
||||
|
||||
public IBrush? BackgroundBrush => ColorEnabled ? new SolidColorBrush(Color) : null;
|
||||
|
||||
public ServerUser User { get; set; } = new();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
using System.Reactive;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Npgsql;
|
||||
using pgLabII.Model;
|
||||
using pgLabII.PgUtils.ConnectionStrings;
|
||||
using pgLabII.Services;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace pgLabII.ViewModels;
|
||||
|
|
@ -8,19 +14,86 @@ public class EditServerConfigurationViewModel : ViewModelBase
|
|||
{
|
||||
public ServerConfiguration Configuration { get; set; }
|
||||
|
||||
// Connection string IO
|
||||
private string _inputConnectionString = string.Empty;
|
||||
public string InputConnectionString
|
||||
{
|
||||
get => _inputConnectionString;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _inputConnectionString, value);
|
||||
// auto-detect when input changes and we are in Auto mode
|
||||
if (ForcedFormat == ForcedFormatOption.Auto)
|
||||
{
|
||||
DetectFormat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ForcedFormatOption
|
||||
{
|
||||
Auto,
|
||||
Libpq,
|
||||
Npgsql,
|
||||
Url
|
||||
}
|
||||
|
||||
private ForcedFormatOption _forcedFormat = ForcedFormatOption.Auto;
|
||||
public ForcedFormatOption ForcedFormat
|
||||
{
|
||||
get => _forcedFormat;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _forcedFormat, value);
|
||||
// When forcing off Auto, clear detected label; when switching to Auto, re-detect
|
||||
if (value == ForcedFormatOption.Auto)
|
||||
DetectFormat();
|
||||
else
|
||||
DetectedFormat = null;
|
||||
}
|
||||
}
|
||||
|
||||
private ConnStringFormat? _detectedFormat;
|
||||
public ConnStringFormat? DetectedFormat
|
||||
{
|
||||
get => _detectedFormat;
|
||||
private set => this.RaiseAndSetIfChanged(ref _detectedFormat, value);
|
||||
}
|
||||
|
||||
private ConnStringFormat _outputFormat = ConnStringFormat.Url;
|
||||
public ConnStringFormat OutputFormat
|
||||
{
|
||||
get => _outputFormat;
|
||||
set => this.RaiseAndSetIfChanged(ref _outputFormat, value);
|
||||
}
|
||||
|
||||
private string _outputConnectionString = string.Empty;
|
||||
public string OutputConnectionString
|
||||
{
|
||||
get => _outputConnectionString;
|
||||
set => this.RaiseAndSetIfChanged(ref _outputConnectionString, value);
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> ParseConnectionStringCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> GenerateConnectionStringCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> CopyOutputConnectionStringCommand { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> SaveAndCloseCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> CloseCommand { get; }
|
||||
|
||||
private readonly IConnectionStringService _service;
|
||||
|
||||
public EditServerConfigurationViewModel()
|
||||
{
|
||||
Configuration = new();
|
||||
_service = ConnectionStringService.CreateDefault();
|
||||
|
||||
SaveAndCloseCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
});
|
||||
CloseCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
});
|
||||
ParseConnectionStringCommand = ReactiveCommand.Create(ParseConnectionString);
|
||||
GenerateConnectionStringCommand = ReactiveCommand.Create(GenerateConnectionString);
|
||||
CopyOutputConnectionStringCommand = ReactiveCommand.Create(() => { /* no-op placeholder */ });
|
||||
|
||||
SaveAndCloseCommand = ReactiveCommand.Create(() => { });
|
||||
CloseCommand = ReactiveCommand.Create(() => { });
|
||||
}
|
||||
|
||||
public EditServerConfigurationViewModel(ServerConfiguration configuration)
|
||||
|
|
@ -29,5 +102,69 @@ public class EditServerConfigurationViewModel : ViewModelBase
|
|||
Configuration = configuration;
|
||||
}
|
||||
|
||||
|
||||
private void DetectFormat()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(InputConnectionString))
|
||||
{
|
||||
DetectedFormat = null;
|
||||
return;
|
||||
}
|
||||
var res = _service.DetectFormat(InputConnectionString);
|
||||
DetectedFormat = res.IsSuccess ? res.Value : null;
|
||||
}
|
||||
|
||||
private void ParseConnectionString()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(InputConnectionString)) return;
|
||||
|
||||
var forced = ForcedFormat;
|
||||
ConnectionDescriptor? descriptor = null;
|
||||
if (forced != ForcedFormatOption.Auto)
|
||||
{
|
||||
IConnectionStringCodec codec = forced switch
|
||||
{
|
||||
ForcedFormatOption.Libpq => new LibpqCodec(),
|
||||
ForcedFormatOption.Npgsql => new NpgsqlCodec(),
|
||||
ForcedFormatOption.Url => new UrlCodec(),
|
||||
_ => new UrlCodec()
|
||||
};
|
||||
var r = codec.TryParse(InputConnectionString);
|
||||
if (r.IsSuccess) descriptor = r.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var r = _service.ParseToDescriptor(InputConnectionString);
|
||||
if (r.IsSuccess) descriptor = r.Value;
|
||||
}
|
||||
|
||||
if (descriptor != null)
|
||||
{
|
||||
// Map into our configuration (update existing)
|
||||
ServerConfigurationMapping.FromDescriptor(descriptor, Configuration);
|
||||
// Also set sensible default OutputFormat to the detected/forced one
|
||||
if (forced == ForcedFormatOption.Auto)
|
||||
{
|
||||
if (DetectedFormat.HasValue) OutputFormat = DetectedFormat.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputFormat = forced switch
|
||||
{
|
||||
ForcedFormatOption.Libpq => ConnStringFormat.Libpq,
|
||||
ForcedFormatOption.Npgsql => ConnStringFormat.Npgsql,
|
||||
ForcedFormatOption.Url => ConnStringFormat.Url,
|
||||
_ => ConnStringFormat.Url
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateConnectionString()
|
||||
{
|
||||
// Build descriptor from current configuration
|
||||
var descriptor = ServerConfigurationMapping.ToDescriptor(Configuration);
|
||||
var r = _service.FormatFromDescriptor(descriptor, OutputFormat);
|
||||
if (r.IsSuccess)
|
||||
OutputConnectionString = r.Value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class ServerListViewModel : ViewModelBase
|
|||
ColorEnabled = true,
|
||||
Host = "localhost",
|
||||
Port = 5434,
|
||||
InitialDatabase = "dbname",
|
||||
User = new ()
|
||||
{
|
||||
Name = "postgres",
|
||||
|
|
@ -46,8 +47,8 @@ public class ServerListViewModel : ViewModelBase
|
|||
|
||||
AddServerCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
ServerConfiguration sc = new();
|
||||
EditServerConfigurationWindow window = new() { DataContext = sc, New = true };
|
||||
EditServerConfigurationViewModel vm = new();
|
||||
EditServerConfigurationWindow window = new() { DataContext = vm, New = true };
|
||||
window.Show();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,37 +2,89 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="650"
|
||||
xmlns:vm="clr-namespace:pgLabII.ViewModels"
|
||||
x:DataType="vm:EditServerConfigurationViewModel"
|
||||
x:Class="pgLabII.Views.EditServerConfigurationWindow"
|
||||
Title="Edit Server Configuration"
|
||||
SizeToContent="WidthAndHeight">
|
||||
<Design.DataContext>
|
||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
||||
<vm:EditServerConfigurationViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<StackPanel >
|
||||
<TextBlock>Name:</TextBlock>
|
||||
<TextBox Text="{Binding Configuration.Name}"/>
|
||||
<Grid Margin="12" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="*">
|
||||
<!-- Basic Details -->
|
||||
<StackPanel Grid.Row="0" Spacing="6">
|
||||
<TextBlock FontWeight="Bold" Text="Details" Margin="0,0,0,4"/>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Name"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Configuration.Name}"/>
|
||||
|
||||
<TextBlock>Color:</TextBlock>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Color"/>
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
||||
<CheckBox IsChecked="{Binding Configuration.ColorEnabled}"/>
|
||||
<ColorPicker Color="{Binding Configuration.Color}"
|
||||
/>
|
||||
<ColorPicker Color="{Binding Configuration.Color}"/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock>Host:</TextBlock>
|
||||
<TextBox Text="{Binding Configuration.Host}"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Database"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Configuration.InitialDatabase}"/>
|
||||
|
||||
<TextBlock>Port:</TextBlock>
|
||||
<NumericUpDown Value="{Binding Configuration.Port}" Minimum="0" Maximum="65535" Increment="1"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="SSL Mode"/>
|
||||
<ComboBox Grid.Row="3" Grid.Column="1" SelectedIndex="{Binding Configuration.DefaultSslMode}">
|
||||
<!-- Order must match Npgsql.SslMode enum: Disable(0), Allow(1), Prefer(2), Require(3), VerifyCA(4), VerifyFull(5) -->
|
||||
<ComboBoxItem>Disable</ComboBoxItem>
|
||||
<ComboBoxItem>Allow</ComboBoxItem>
|
||||
<ComboBoxItem>Prefer</ComboBoxItem>
|
||||
<ComboBoxItem>Require</ComboBoxItem>
|
||||
<ComboBoxItem>VerifyCA</ComboBoxItem>
|
||||
<ComboBoxItem>VerifyFull</ComboBoxItem>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock>Database:</TextBlock>
|
||||
<TextBox Text="{Binding Configuration.InitialDatabase}"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" VerticalAlignment="Center" Margin="16,0,8,0" Text="Host"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding Configuration.Host}"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" Margin="16,0,8,0" Text="Port"/>
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="3" Value="{Binding Configuration.Port}" Minimum="0" Maximum="65535" Increment="1"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Input Connection String -->
|
||||
<StackPanel Grid.Row="1" Margin="0,12,0,0" Spacing="6">
|
||||
<TextBlock FontWeight="Bold" Text="Connection string input"/>
|
||||
<TextBlock Text="Paste an existing connection string (libpq, Npgsql, or URL)."/>
|
||||
<TextBox TextWrapping="Wrap" AcceptsReturn="True" MinHeight="60" Text="{Binding InputConnectionString}"/>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Interpret as"/>
|
||||
<!-- Bind SelectedIndex to enum (Auto=0, Libpq=1, Npgsql=2, Url=3) -->
|
||||
<ComboBox Grid.Column="1" SelectedIndex="{Binding ForcedFormat}">
|
||||
<ComboBoxItem>Auto</ComboBoxItem>
|
||||
<ComboBoxItem>Libpq</ComboBoxItem>
|
||||
<ComboBoxItem>Npgsql</ComboBoxItem>
|
||||
<ComboBoxItem>URL</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<TextBlock Grid.Column="2" Margin="12,0,8,0" VerticalAlignment="Center" Text="Detected:"/>
|
||||
<TextBlock Grid.Column="3" VerticalAlignment="Center" Text="{Binding DetectedFormat}"/>
|
||||
<Button Grid.Column="4" Margin="12,0,0,0" Content="Parse into fields" Command="{Binding ParseConnectionStringCommand}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Output Connection String -->
|
||||
<StackPanel Grid.Row="2" Margin="0,12,0,0" Spacing="6">
|
||||
<TextBlock FontWeight="Bold" Text="Connection string output"/>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0" Text="Format"/>
|
||||
<!-- ConnStringFormat: Libpq=0, Npgsql=1, Url=2 -->
|
||||
<ComboBox Grid.Column="1" SelectedIndex="{Binding OutputFormat}">
|
||||
<ComboBoxItem>Libpq</ComboBoxItem>
|
||||
<ComboBoxItem>Npgsql</ComboBoxItem>
|
||||
<ComboBoxItem>URL</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<Button Grid.Column="2" Margin="12,0,0,0" Content="Generate" Command="{Binding GenerateConnectionStringCommand}"/>
|
||||
<Button Grid.Column="3" Margin="6,0,0,0" Content="Copy" Command="{Binding CopyOutputConnectionStringCommand}"/>
|
||||
</Grid>
|
||||
<TextBox TextWrapping="Wrap" AcceptsReturn="True" MinHeight="60" IsReadOnly="True" Text="{Binding OutputConnectionString}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Spacer / Future buttons row could go here -->
|
||||
</Grid>
|
||||
</Window>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue