refactor(configmanager): rename UI project and split test projects
Rename ConfigManager to ConfigManager.Ui to match the Core/CLI/UI project structure, and split the monolithic test project into Core.Tests, Cli.Tests, and Ui.Tests to align with the source project organization.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:JdeScoping.ConfigManager.Ui.Converters"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.App"
|
||||
RequestedThemeVariant="Dark">
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
||||
</Application.Styles>
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<!-- Converters -->
|
||||
<converters:StringToBoolConverter x:Key="StringToBool" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,74 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using JdeScoping.ConfigManager.Core.DependencyInjection;
|
||||
using JdeScoping.ConfigManager.Ui.Services;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui;
|
||||
|
||||
public partial class App : Avalonia.Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the dependency injection service provider for the application.
|
||||
/// </summary>
|
||||
public static IServiceProvider Services { get; private set; } = null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
ConfigureServices(services);
|
||||
Services = services.BuildServiceProvider();
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new Views.MainWindow
|
||||
{
|
||||
DataContext = Services.GetRequiredService<MainWindowViewModel>()
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Logging
|
||||
services.AddLogging(builder => builder
|
||||
.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Debug));
|
||||
|
||||
// Add all ConfigManager.Core services
|
||||
services.AddConfigManagerCore();
|
||||
|
||||
// Platform Services (Avalonia-specific)
|
||||
services.AddSingleton<IDialogService>(sp =>
|
||||
new AvaloniaDialogService(GetMainWindow));
|
||||
services.AddSingleton<IClipboardService>(sp =>
|
||||
new AvaloniaClipboardService(GetClipboard));
|
||||
|
||||
// ViewModels
|
||||
services.AddTransient<MainWindowViewModel>();
|
||||
}
|
||||
|
||||
private Window? GetMainWindow()
|
||||
{
|
||||
return (ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
|
||||
}
|
||||
|
||||
private IClipboard? GetClipboard()
|
||||
{
|
||||
return GetMainWindow()?.Clipboard;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.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>
|
||||
/// <param name="value">The ConnectionProvider value to convert.</param>
|
||||
/// <param name="targetType">The target type (bool).</param>
|
||||
/// <param name="parameter">Unused converter parameter.</param>
|
||||
/// <param name="culture">The culture for conversion.</param>
|
||||
/// <returns>True if the value matches the target provider; otherwise, false.</returns>
|
||||
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>
|
||||
/// <param name="value">The value to convert back.</param>
|
||||
/// <param name="targetType">The target type.</param>
|
||||
/// <param name="parameter">Unused converter parameter.</param>
|
||||
/// <param name="culture">The culture for conversion.</param>
|
||||
/// <exception cref="NotImplementedException">This converter does not support reverse conversion.</exception>
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to bool (empty/null = false, not empty = true).
|
||||
/// Used for visibility bindings based on validation error messages.
|
||||
/// </summary>
|
||||
public class StringToBoolConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a string to a boolean based on whether it's empty or null.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to check.</param>
|
||||
/// <param name="targetType">The target type (ignored).</param>
|
||||
/// <param name="parameter">An optional parameter (ignored).</param>
|
||||
/// <param name="culture">The culture information (ignored).</param>
|
||||
/// <returns>True if string is not null or whitespace, false otherwise.</returns>
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string str)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(str);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a value back (not implemented for string checks).
|
||||
/// </summary>
|
||||
/// <param name="value">The value to convert back (ignored).</param>
|
||||
/// <param name="targetType">The target type (ignored).</param>
|
||||
/// <param name="parameter">An optional parameter (ignored).</param>
|
||||
/// <param name="culture">The culture information (ignored).</param>
|
||||
/// <returns>Not implemented.</returns>
|
||||
/// <exception cref="NotImplementedException">Always thrown.</exception>
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AssemblyName>JdeScoping.ConfigManager.Ui</AssemblyName>
|
||||
<RootNamespace>JdeScoping.ConfigManager.Ui</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.2.*" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.*" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.*" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.*" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.*" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.*" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="MessageBox.Avalonia" Version="3.1.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.*" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.*" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\JdeScoping.ConfigManager.Core\JdeScoping.ConfigManager.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
using Avalonia;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui;
|
||||
|
||||
class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry point of the application.
|
||||
/// </summary>
|
||||
/// <param name="args">Command-line arguments.</param>
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the Avalonia application builder with platform and font configuration.
|
||||
/// </summary>
|
||||
/// <returns>A configured AppBuilder ready for application startup.</returns>
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Avalonia.Input.Platform;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia implementation of IClipboardService.
|
||||
/// </summary>
|
||||
public class AvaloniaClipboardService : IClipboardService
|
||||
{
|
||||
private readonly Func<IClipboard?> _getClipboard;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of AvaloniaClipboardService.
|
||||
/// </summary>
|
||||
/// <param name="getClipboard">Factory function to get the clipboard instance.</param>
|
||||
public AvaloniaClipboardService(Func<IClipboard?> getClipboard)
|
||||
{
|
||||
_getClipboard = getClipboard ?? throw new ArgumentNullException(nameof(getClipboard));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the clipboard text asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to set on the clipboard.</param>
|
||||
public async Task SetTextAsync(string text)
|
||||
{
|
||||
var clipboard = _getClipboard();
|
||||
if (clipboard != null)
|
||||
{
|
||||
await clipboard.SetTextAsync(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Text;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Avalonia implementation of IDialogService using MsBox.Avalonia and platform storage.
|
||||
/// </summary>
|
||||
public class AvaloniaDialogService : IDialogService
|
||||
{
|
||||
private readonly Func<Window?> _getMainWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of AvaloniaDialogService.
|
||||
/// </summary>
|
||||
/// <param name="getMainWindow">Factory function to get the main window for dialogs.</param>
|
||||
public AvaloniaDialogService(Func<Window?> getMainWindow)
|
||||
{
|
||||
_getMainWindow = getMainWindow ?? throw new ArgumentNullException(nameof(getMainWindow));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> ShowFolderPickerAsync(string? title = null)
|
||||
{
|
||||
var window = _getMainWindow();
|
||||
if (window == null)
|
||||
return null;
|
||||
|
||||
var folders = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = title ?? "Select Folder",
|
||||
AllowMultiple = false
|
||||
});
|
||||
|
||||
return folders.Count > 0 ? folders[0].Path.LocalPath : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> ShowFilePickerAsync(string? title = null)
|
||||
{
|
||||
var window = _getMainWindow();
|
||||
if (window == null)
|
||||
return null;
|
||||
|
||||
var files = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = title ?? "Select File",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new[]
|
||||
{
|
||||
new FilePickerFileType("JSON Files")
|
||||
{
|
||||
Patterns = new[] { "*.json" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return files.Count > 0 ? files[0].Path.LocalPath : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ShowMessageAsync(string title, string message)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, Icon.Info);
|
||||
var window = _getMainWindow();
|
||||
if (window != null)
|
||||
{
|
||||
await box.ShowWindowDialogAsync(window);
|
||||
}
|
||||
else
|
||||
{
|
||||
await box.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> ShowConfirmationAsync(string title, string message)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.YesNo, Icon.Question);
|
||||
var window = _getMainWindow();
|
||||
ButtonResult result;
|
||||
if (window != null)
|
||||
{
|
||||
result = await box.ShowWindowDialogAsync(window);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await box.ShowAsync();
|
||||
}
|
||||
return result == ButtonResult.Yes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> ShowDiffPreviewAsync(string title, DiffResult diff)
|
||||
{
|
||||
// Basic implementation - full diff preview dialog will be implemented in Task 22
|
||||
if (!diff.HasChanges)
|
||||
{
|
||||
await ShowMessageAsync(title, "No changes detected.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var summary = new StringBuilder();
|
||||
summary.AppendLine($"Changes detected: {diff.Insertions} insertion(s), {diff.Deletions} deletion(s)");
|
||||
summary.AppendLine();
|
||||
summary.AppendLine("Do you want to apply these changes?");
|
||||
|
||||
return await ShowConfirmationAsync(title, summary.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ShowValidationResultsAsync(ValidationResult appSettingsResult, ValidationResult pipelinesResult)
|
||||
{
|
||||
// Basic implementation - full validation results dialog will be implemented in Task 23
|
||||
var message = new StringBuilder();
|
||||
|
||||
message.AppendLine("=== AppSettings Validation ===");
|
||||
if (appSettingsResult.IsValid)
|
||||
{
|
||||
message.AppendLine("Valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var error in appSettingsResult.Errors)
|
||||
{
|
||||
message.AppendLine($"Error: {error}");
|
||||
}
|
||||
}
|
||||
foreach (var warning in appSettingsResult.Warnings)
|
||||
{
|
||||
message.AppendLine($"Warning: {warning}");
|
||||
}
|
||||
|
||||
message.AppendLine();
|
||||
message.AppendLine("=== Pipelines Validation ===");
|
||||
if (pipelinesResult.IsValid)
|
||||
{
|
||||
message.AppendLine("Valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var error in pipelinesResult.Errors)
|
||||
{
|
||||
message.AppendLine($"Error: {error}");
|
||||
}
|
||||
}
|
||||
foreach (var warning in pipelinesResult.Warnings)
|
||||
{
|
||||
message.AppendLine($"Warning: {warning}");
|
||||
}
|
||||
|
||||
var title = appSettingsResult.IsValid && pipelinesResult.IsValid
|
||||
? "Validation Passed"
|
||||
: "Validation Issues Found";
|
||||
|
||||
var icon = appSettingsResult.IsValid && pipelinesResult.IsValid ? Icon.Success : Icon.Warning;
|
||||
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message.ToString(), ButtonEnum.Ok, icon);
|
||||
var window = _getMainWindow();
|
||||
if (window != null)
|
||||
{
|
||||
await box.ShowWindowDialogAsync(window);
|
||||
}
|
||||
else
|
||||
{
|
||||
await box.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> ShowInputDialogAsync(string title, string prompt, string? defaultValue = null)
|
||||
{
|
||||
var window = _getMainWindow();
|
||||
if (window == null)
|
||||
return null;
|
||||
|
||||
var dialog = new InputDialog(title, prompt, defaultValue);
|
||||
var result = await dialog.ShowDialog<bool?>(window);
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
return dialog.InputText;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace JdeScoping.ConfigManager.Ui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for platform-specific clipboard operations.
|
||||
/// Enables unit testing of view models that need clipboard access.
|
||||
/// </summary>
|
||||
public interface IClipboardService
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies text to the system clipboard.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to copy.</param>
|
||||
Task SetTextAsync(string text);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for platform-specific dialog operations.
|
||||
/// Enables unit testing of view models that need to show dialogs.
|
||||
/// </summary>
|
||||
public interface IDialogService
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows a folder picker dialog.
|
||||
/// </summary>
|
||||
/// <param name="title">Optional title for the dialog.</param>
|
||||
/// <returns>The selected folder path, or null if cancelled.</returns>
|
||||
Task<string?> ShowFolderPickerAsync(string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a file picker dialog filtered for JSON files.
|
||||
/// </summary>
|
||||
/// <param name="title">Optional title for the dialog.</param>
|
||||
/// <returns>The selected file path, or null if cancelled.</returns>
|
||||
Task<string?> ShowFilePickerAsync(string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a message dialog.
|
||||
/// </summary>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
/// <param name="message">The message to display.</param>
|
||||
Task ShowMessageAsync(string title, string message);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a confirmation dialog with Yes/No options.
|
||||
/// </summary>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
/// <param name="message">The confirmation message to display.</param>
|
||||
/// <returns>True if user clicked Yes, false otherwise.</returns>
|
||||
Task<bool> ShowConfirmationAsync(string title, string message);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a diff preview dialog allowing the user to review changes.
|
||||
/// </summary>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
/// <param name="diff">The diff result to display.</param>
|
||||
/// <returns>True if user confirms the changes, false otherwise.</returns>
|
||||
Task<bool> ShowDiffPreviewAsync(string title, DiffResult diff);
|
||||
|
||||
/// <summary>
|
||||
/// Shows validation results for configuration files.
|
||||
/// </summary>
|
||||
/// <param name="appSettingsResult">Validation result for appsettings.json.</param>
|
||||
/// <param name="pipelinesResult">Validation result for pipelines.json.</param>
|
||||
Task ShowValidationResultsAsync(ValidationResult appSettingsResult, ValidationResult pipelinesResult);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an input dialog to collect text from the user.
|
||||
/// </summary>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
/// <param name="prompt">The prompt message to display.</param>
|
||||
/// <param name="defaultValue">Optional default value for the input field.</param>
|
||||
/// <returns>The text entered by the user, or null if cancelled.</returns>
|
||||
Task<string?> ShowInputDialogAsync(string title, string prompt, string? defaultValue = null);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// An async command implementation that properly handles async operations.
|
||||
/// </summary>
|
||||
public class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isExecuting;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the result of <see cref="CanExecute"/> has changed.
|
||||
/// </summary>
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
remove => _canExecuteChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="execute">The async action to execute when the command is invoked.</param>
|
||||
/// <param name="canExecute">An optional predicate to determine if the command can execute.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="execute"/> is null.</exception>
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the command can execute in its current state.
|
||||
/// </summary>
|
||||
/// <param name="parameter">Unused parameter required by <see cref="ICommand"/> interface.</param>
|
||||
/// <returns>False if the command is currently executing; otherwise returns the result of the canExecute predicate.</returns>
|
||||
public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the async command, preventing concurrent execution.
|
||||
/// </summary>
|
||||
/// <param name="parameter">Unused parameter required by <see cref="ICommand"/> interface.</param>
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter)) return;
|
||||
|
||||
_isExecuting = true;
|
||||
RaiseCanExecuteChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await _execute();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="CanExecuteChanged"/> event to notify command bindings of state changes.
|
||||
/// </summary>
|
||||
public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the diff preview dialog.
|
||||
/// </summary>
|
||||
public class DiffPreviewDialogViewModel : ViewModelBase
|
||||
{
|
||||
private bool _result;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DiffPreviewDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="diff">The diff result containing the changes to preview.</param>
|
||||
public DiffPreviewDialogViewModel(DiffResult diff)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(diff);
|
||||
|
||||
Lines = new ObservableCollection<DiffLineViewModel>(
|
||||
diff.Lines.Select(l => new DiffLineViewModel(l)));
|
||||
Insertions = diff.Insertions;
|
||||
Deletions = diff.Deletions;
|
||||
HasChanges = diff.HasChanges;
|
||||
|
||||
SaveCommand = new RelayCommand(() => { Result = true; RequestClose?.Invoke(); });
|
||||
CancelCommand = new RelayCommand(() => { Result = false; RequestClose?.Invoke(); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of diff lines to display in the preview.
|
||||
/// </summary>
|
||||
public ObservableCollection<DiffLineViewModel> Lines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines inserted in the diff.
|
||||
/// </summary>
|
||||
public int Insertions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines deleted in the diff.
|
||||
/// </summary>
|
||||
public int Deletions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the diff contains any changes.
|
||||
/// </summary>
|
||||
public bool HasChanges { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dialog result (true if saved, false if canceled).
|
||||
/// </summary>
|
||||
public bool Result
|
||||
{
|
||||
get => _result;
|
||||
private set => SetProperty(ref _result, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to save the diff changes and close the dialog.
|
||||
/// </summary>
|
||||
public ICommand SaveCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to cancel and close the dialog without saving.
|
||||
/// </summary>
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action to request dialog closure.
|
||||
/// </summary>
|
||||
public Action? RequestClose { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for a single diff line.
|
||||
/// </summary>
|
||||
public class DiffLineViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DiffLineViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="line">The diff line data to represent.</param>
|
||||
public DiffLineViewModel(DiffLine line)
|
||||
{
|
||||
OldLineNumber = line.OldLineNumber?.ToString() ?? "";
|
||||
NewLineNumber = line.NewLineNumber?.ToString() ?? "";
|
||||
Text = line.Text;
|
||||
Type = line.Type;
|
||||
Background = line.Type switch
|
||||
{
|
||||
DiffLineType.Added => "#1A3DD68C",
|
||||
DiffLineType.Removed => "#1AFF6B6B",
|
||||
_ => "Transparent"
|
||||
};
|
||||
BorderColor = line.Type switch
|
||||
{
|
||||
DiffLineType.Added => "#3DD68C",
|
||||
DiffLineType.Removed => "#FF6B6B",
|
||||
_ => "Transparent"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original line number (empty string if not applicable).
|
||||
/// </summary>
|
||||
public string OldLineNumber { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new line number (empty string if not applicable).
|
||||
/// </summary>
|
||||
public string NewLineNumber { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text content of the diff line.
|
||||
/// </summary>
|
||||
public string Text { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of diff line (added, removed, or context).
|
||||
/// </summary>
|
||||
public DiffLineType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the background color for the diff line visualization.
|
||||
/// </summary>
|
||||
public string Background { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the border color for the diff line visualization.
|
||||
/// </summary>
|
||||
public string BorderColor { get; }
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.ConfigManager.Core.Constants;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// View model for creating a new secure store.
|
||||
/// </summary>
|
||||
public class NewStoreDialogViewModel : ViewModelBase
|
||||
{
|
||||
private string _storePath = string.Empty;
|
||||
private string _keyFilePath = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NewStoreDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
public NewStoreDialogViewModel()
|
||||
{
|
||||
BrowseStorePathCommand = new RelayCommand(BrowseStorePath);
|
||||
BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath);
|
||||
GenerateKeyFileCommand = new AsyncRelayCommand(GenerateKeyFileAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the store file to create.
|
||||
/// </summary>
|
||||
public string StorePath
|
||||
{
|
||||
get => _storePath;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _storePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the key file for encryption.
|
||||
/// </summary>
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _keyFilePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to browse for store path location.
|
||||
/// </summary>
|
||||
public ICommand BrowseStorePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to browse for key file path location.
|
||||
/// </summary>
|
||||
public ICommand BrowseKeyFilePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to generate a new key file.
|
||||
/// </summary>
|
||||
public ICommand GenerateKeyFileCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(StorePath) && !string.IsNullOrWhiteSpace(KeyFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(StorePath))
|
||||
return SecureStoreStrings.StorePathRequired;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeyFilePath))
|
||||
return SecureStoreStrings.KeyFilePathRequired;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to request save file dialog for store path.
|
||||
/// Parameters: title, fileTypeName, pattern, defaultExtension
|
||||
/// Returns: selected file path or null
|
||||
/// </summary>
|
||||
public event Func<string, string, string, string, Task<string?>>? OnShowSaveFileDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to request key file generation.
|
||||
/// Parameters: title, fileTypeName, pattern, defaultExtension
|
||||
/// Returns: generated key file path or null
|
||||
/// </summary>
|
||||
public event Func<string, string, string, string, Task<string?>>? OnGenerateKeyFile;
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
private async void BrowseStorePath()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowSaveFileDialog(
|
||||
SecureStoreStrings.ChooseStoreLocation,
|
||||
SecureStoreFileExtensions.StoreTypeName,
|
||||
SecureStoreFileExtensions.StorePattern,
|
||||
SecureStoreFileExtensions.StoreExtension);
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
StorePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
private async void BrowseKeyFilePath()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowSaveFileDialog(
|
||||
SecureStoreStrings.ChooseKeyFileLocation,
|
||||
SecureStoreFileExtensions.KeyTypeName,
|
||||
SecureStoreFileExtensions.KeyPattern,
|
||||
SecureStoreFileExtensions.KeyExtension);
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
KeyFilePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GenerateKeyFileAsync()
|
||||
{
|
||||
if (OnGenerateKeyFile == null)
|
||||
return;
|
||||
|
||||
var path = await OnGenerateKeyFile(
|
||||
SecureStoreStrings.GenerateKeyFileTitle,
|
||||
SecureStoreFileExtensions.KeyTypeName,
|
||||
SecureStoreFileExtensions.KeyPattern,
|
||||
SecureStoreFileExtensions.KeyExtension);
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
KeyFilePath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
using JdeScoping.ConfigManager.Core.Constants;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// View model for adding or editing a secret in the secure store.
|
||||
/// </summary>
|
||||
public class SecretEditDialogViewModel : ViewModelBase
|
||||
{
|
||||
private string _key = string.Empty;
|
||||
private string _value = string.Empty;
|
||||
private bool _isNewSecret = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialogViewModel"/> class for adding a new secret.
|
||||
/// </summary>
|
||||
public SecretEditDialogViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialogViewModel"/> class for editing an existing secret.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key (read-only when editing).</param>
|
||||
/// <param name="value">The current secret value.</param>
|
||||
public SecretEditDialogViewModel(string key, string value)
|
||||
{
|
||||
_key = key;
|
||||
_value = value;
|
||||
_isNewSecret = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key.
|
||||
/// </summary>
|
||||
public string Key
|
||||
{
|
||||
get => _key;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _key, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret value.
|
||||
/// </summary>
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this is a new secret being added.
|
||||
/// </summary>
|
||||
public bool IsNewSecret
|
||||
{
|
||||
get => _isNewSecret;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isNewSecret, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsKeyEditable));
|
||||
OnPropertyChanged(nameof(DialogTitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the key field is editable.
|
||||
/// The key is only editable when adding a new secret.
|
||||
/// </summary>
|
||||
public bool IsKeyEditable => _isNewSecret;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dialog title based on whether this is a new secret or edit.
|
||||
/// </summary>
|
||||
public string DialogTitle => _isNewSecret ? "Add Secret" : "Edit Secret";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(Key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Key))
|
||||
return SecureStoreStrings.KeyRequired;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.ConfigManager.Core.Constants;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// View model for unlocking an existing secure store.
|
||||
/// </summary>
|
||||
public class UnlockStoreDialogViewModel : ViewModelBase
|
||||
{
|
||||
private readonly string _storePath;
|
||||
private string _keyFilePath = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnlockStoreDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path to the store file to unlock.</param>
|
||||
public UnlockStoreDialogViewModel(string storePath)
|
||||
{
|
||||
_storePath = storePath ?? throw new ArgumentNullException(nameof(storePath));
|
||||
BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the store file to unlock (read-only).
|
||||
/// </summary>
|
||||
public string StorePath => _storePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the key file for decryption.
|
||||
/// </summary>
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _keyFilePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to browse for key file path location.
|
||||
/// </summary>
|
||||
public ICommand BrowseKeyFilePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(KeyFilePath))
|
||||
return false;
|
||||
|
||||
return System.IO.File.Exists(KeyFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(KeyFilePath))
|
||||
return SecureStoreStrings.KeyFilePathRequired;
|
||||
|
||||
if (!System.IO.File.Exists(KeyFilePath))
|
||||
return SecureStoreStrings.KeyFileNotFound;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to request open file dialog for key file path.
|
||||
/// Parameters: title, fileTypeName, pattern
|
||||
/// Returns: selected file path or null
|
||||
/// </summary>
|
||||
public event Func<string, string, string, Task<string?>>? OnShowOpenFileDialog;
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
private async void BrowseKeyFilePath()
|
||||
{
|
||||
if (OnShowOpenFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowOpenFileDialog(
|
||||
SecureStoreStrings.SelectKeyFile,
|
||||
SecureStoreFileExtensions.KeyTypeName,
|
||||
SecureStoreFileExtensions.KeyPattern);
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
KeyFilePath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the validation results dialog.
|
||||
/// </summary>
|
||||
public class ValidationResultsDialogViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValidationResultsDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="appSettingsResult">The validation result for appsettings.json.</param>
|
||||
/// <param name="pipelinesResult">The validation result for pipelines.json.</param>
|
||||
public ValidationResultsDialogViewModel(ValidationResult appSettingsResult, ValidationResult pipelinesResult)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(appSettingsResult);
|
||||
ArgumentNullException.ThrowIfNull(pipelinesResult);
|
||||
|
||||
var items = new List<ValidationItemViewModel>();
|
||||
|
||||
foreach (var error in appSettingsResult.Errors)
|
||||
items.Add(new ValidationItemViewModel(error, "appsettings.json", ValidationItemType.Error));
|
||||
foreach (var warning in appSettingsResult.Warnings)
|
||||
items.Add(new ValidationItemViewModel(warning, "appsettings.json", ValidationItemType.Warning));
|
||||
foreach (var error in pipelinesResult.Errors)
|
||||
items.Add(new ValidationItemViewModel(error, "pipelines.json", ValidationItemType.Error));
|
||||
foreach (var warning in pipelinesResult.Warnings)
|
||||
items.Add(new ValidationItemViewModel(warning, "pipelines.json", ValidationItemType.Warning));
|
||||
|
||||
Items = new ObservableCollection<ValidationItemViewModel>(items);
|
||||
ErrorCount = appSettingsResult.Errors.Count + pipelinesResult.Errors.Count;
|
||||
WarningCount = appSettingsResult.Warnings.Count + pipelinesResult.Warnings.Count;
|
||||
IsValid = ErrorCount == 0 && WarningCount == 0;
|
||||
|
||||
CloseCommand = new RelayCommand(() => RequestClose?.Invoke());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of validation items to display.
|
||||
/// </summary>
|
||||
public ObservableCollection<ValidationItemViewModel> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total count of validation errors.
|
||||
/// </summary>
|
||||
public int ErrorCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total count of validation warnings.
|
||||
/// </summary>
|
||||
public int WarningCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether all validation passed (no errors or warnings).
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to close the dialog.
|
||||
/// </summary>
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action to request closing the dialog.
|
||||
/// </summary>
|
||||
public Action? RequestClose { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of validation item.
|
||||
/// </summary>
|
||||
public enum ValidationItemType
|
||||
{
|
||||
/// <summary>
|
||||
/// A validation error that must be fixed.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// A validation warning that should be reviewed.
|
||||
/// </summary>
|
||||
Warning
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for a single validation item.
|
||||
/// </summary>
|
||||
public class ValidationItemViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValidationItemViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The validation message.</param>
|
||||
/// <param name="source">The source file name.</param>
|
||||
/// <param name="type">The type of validation item.</param>
|
||||
public ValidationItemViewModel(string message, string source, ValidationItemType type)
|
||||
{
|
||||
Message = message;
|
||||
Source = source;
|
||||
Type = type;
|
||||
Icon = type == ValidationItemType.Error ? "\u2717" : "\u26A0";
|
||||
IconColor = type == ValidationItemType.Error ? "#FF6B6B" : "#FFB84D";
|
||||
Background = type == ValidationItemType.Error ? "#1AFF6B6B" : "#1AFFB84D";
|
||||
BorderColor = type == ValidationItemType.Error ? "#FF6B6B" : "#FFB84D";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation message.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source file name.
|
||||
/// </summary>
|
||||
public string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of validation item.
|
||||
/// </summary>
|
||||
public ValidationItemType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon character for the validation type.
|
||||
/// </summary>
|
||||
public string Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon color as a hex string.
|
||||
/// </summary>
|
||||
public string IconColor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the background color as a hex string with alpha.
|
||||
/// </summary>
|
||||
public string Background { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the border color as a hex string.
|
||||
/// </summary>
|
||||
public string BorderColor { get; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for editing Auth configuration section.
|
||||
/// </summary>
|
||||
public class AuthFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly AuthSection _model;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The auth section model to edit.</param>
|
||||
/// <param name="onChanged">The callback to invoke when configuration changes.</param>
|
||||
public AuthFormViewModel(AuthSection model, Action onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the authentication cookie.
|
||||
/// </summary>
|
||||
public string CookieName
|
||||
{
|
||||
get => _model.CookieName;
|
||||
set
|
||||
{
|
||||
if (_model.CookieName != value)
|
||||
{
|
||||
_model.CookieName = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cookie expiration time in minutes.
|
||||
/// </summary>
|
||||
public int CookieExpirationMinutes
|
||||
{
|
||||
get => _model.CookieExpirationMinutes;
|
||||
set
|
||||
{
|
||||
if (_model.CookieExpirationMinutes != value)
|
||||
{
|
||||
_model.CookieExpirationMinutes = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.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 SQL Server port number.
|
||||
/// </summary>
|
||||
public int? SqlServerPort
|
||||
{
|
||||
get => _model.SqlServerPort;
|
||||
set
|
||||
{
|
||||
if (_model.SqlServerPort != value)
|
||||
{
|
||||
_model.SqlServerPort = 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,
|
||||
_ => "-"
|
||||
};
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.ConfigManager.Core.Services.SecureStore;
|
||||
using JdeScoping.ConfigManager.Ui.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for editing connection strings stored in SecureStore.
|
||||
/// Connection string names come from configuration, values come from SecureStore.
|
||||
/// </summary>
|
||||
public class ConnectionStringsFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ConnectionStringsSection _model;
|
||||
private readonly ISecureStoreManager _secureStoreManager;
|
||||
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 (provides names).</param>
|
||||
/// <param name="secureStoreManager">The SecureStore manager for reading/writing values.</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,
|
||||
ISecureStoreManager secureStoreManager,
|
||||
Action onChanged,
|
||||
IDialogService dialogService,
|
||||
IConnectionTestService connectionTestService)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_secureStoreManager = secureStoreManager ?? throw new ArgumentNullException(nameof(secureStoreManager));
|
||||
_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, loading values from SecureStore
|
||||
foreach (var entry in _model.Entries)
|
||||
{
|
||||
// Load the actual connection string value from SecureStore
|
||||
var secureStoreValue = _secureStoreManager.IsStoreOpen && !string.IsNullOrEmpty(entry.Name)
|
||||
? TryGetSecret(entry.Name)
|
||||
: null;
|
||||
|
||||
// Update entry's RawConnectionString with SecureStore value if available
|
||||
if (!string.IsNullOrEmpty(secureStoreValue))
|
||||
{
|
||||
ConnectionStringsSectionConverter.ApplyConnectionString(entry, secureStoreValue);
|
||||
}
|
||||
|
||||
Connections.Add(new ConnectionStringEntryViewModel(entry, OnEntryChanged));
|
||||
}
|
||||
|
||||
// Initialize commands
|
||||
AddConnectionCommand = new RelayCommand(AddConnection);
|
||||
DeleteConnectionCommand = new AsyncRelayCommand(DeleteConnectionAsync, () => HasSelection);
|
||||
ValidateConnectionCommand = new AsyncRelayCommand(ValidateConnectionAsync, () => HasSelection);
|
||||
TestConnectionCommand = new AsyncRelayCommand(TestConnectionAsync, () => HasSelection && !IsTesting);
|
||||
}
|
||||
|
||||
private string? TryGetSecret(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _secureStoreManager.GetSecret(key);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEntryChanged()
|
||||
{
|
||||
// When an entry changes, save its value to SecureStore
|
||||
if (SelectedConnection != null && !string.IsNullOrEmpty(SelectedConnection.Name))
|
||||
{
|
||||
var connectionString = SelectedConnection.GeneratedConnectionString;
|
||||
if (_secureStoreManager.IsStoreOpen)
|
||||
{
|
||||
_secureStoreManager.SetSecret(SelectedConnection.Name, connectionString);
|
||||
}
|
||||
}
|
||||
_onChanged();
|
||||
}
|
||||
|
||||
/// <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",
|
||||
Provider = ConnectionProvider.Generic
|
||||
};
|
||||
|
||||
_model.Entries.Add(entry);
|
||||
|
||||
// Create empty entry in SecureStore
|
||||
if (_secureStoreManager.IsStoreOpen)
|
||||
{
|
||||
_secureStoreManager.SetSecret(entry.Name, string.Empty);
|
||||
}
|
||||
|
||||
var viewModel = new ConnectionStringEntryViewModel(entry, OnEntryChanged);
|
||||
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}'? This will also remove the SecureStore entry.");
|
||||
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
// Remove from SecureStore
|
||||
if (_secureStoreManager.IsStoreOpen && !string.IsNullOrEmpty(name))
|
||||
{
|
||||
try
|
||||
{
|
||||
_secureStoreManager.RemoveSecret(name);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
// Entry didn't exist in SecureStore, that's OK
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for editing DataAccess configuration section.
|
||||
/// </summary>
|
||||
public class DataAccessFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly DataAccessSection _model;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataAccessFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The data access section model to edit.</param>
|
||||
/// <param name="onChanged">The callback to invoke when configuration changes.</param>
|
||||
public DataAccessFormViewModel(DataAccessSection model, Action onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default query timeout in seconds.
|
||||
/// </summary>
|
||||
public int DefaultTimeoutSeconds
|
||||
{
|
||||
get => _model.DefaultTimeoutSeconds;
|
||||
set
|
||||
{
|
||||
if (_model.DefaultTimeoutSeconds != value)
|
||||
{
|
||||
_model.DefaultTimeoutSeconds = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lot usage query timeout in seconds.
|
||||
/// </summary>
|
||||
public int LotUsageTimeoutSeconds
|
||||
{
|
||||
get => _model.LotUsageTimeoutSeconds;
|
||||
set
|
||||
{
|
||||
if (_model.LotUsageTimeoutSeconds != value)
|
||||
{
|
||||
_model.LotUsageTimeoutSeconds = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIS data query timeout in seconds.
|
||||
/// </summary>
|
||||
public int MisDataTimeoutSeconds
|
||||
{
|
||||
get => _model.MisDataTimeoutSeconds;
|
||||
set
|
||||
{
|
||||
if (_model.MisDataTimeoutSeconds != value)
|
||||
{
|
||||
_model.MisDataTimeoutSeconds = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the production schema name.
|
||||
/// </summary>
|
||||
public string ProductionSchema
|
||||
{
|
||||
get => _model.ProductionSchema;
|
||||
set
|
||||
{
|
||||
if (_model.ProductionSchema != value)
|
||||
{
|
||||
_model.ProductionSchema = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the archive schema name.
|
||||
/// </summary>
|
||||
public string ArchiveSchema
|
||||
{
|
||||
get => _model.ArchiveSchema;
|
||||
set
|
||||
{
|
||||
if (_model.ArchiveSchema != value)
|
||||
{
|
||||
_model.ArchiveSchema = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stage schema name.
|
||||
/// </summary>
|
||||
public string StageSchema
|
||||
{
|
||||
get => _model.StageSchema;
|
||||
set
|
||||
{
|
||||
if (_model.StageSchema != value)
|
||||
{
|
||||
_model.StageSchema = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether detailed logging is enabled.
|
||||
/// </summary>
|
||||
public bool EnableDetailedLogging
|
||||
{
|
||||
get => _model.EnableDetailedLogging;
|
||||
set
|
||||
{
|
||||
if (_model.EnableDetailedLogging != value)
|
||||
{
|
||||
_model.EnableDetailedLogging = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for editing DataSync configuration section.
|
||||
/// </summary>
|
||||
public class DataSyncFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly DataSyncSection _model;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataSyncFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The data sync section model to edit.</param>
|
||||
/// <param name="onChanged">The callback to invoke when configuration changes.</param>
|
||||
public DataSyncFormViewModel(DataSyncSection model, Action onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether data synchronization is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled
|
||||
{
|
||||
get => _model.Enabled;
|
||||
set
|
||||
{
|
||||
if (_model.Enabled != value)
|
||||
{
|
||||
_model.Enabled = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the check interval in minutes.
|
||||
/// </summary>
|
||||
public int CheckIntervalMinutes
|
||||
{
|
||||
get => (int)_model.CheckInterval.TotalMinutes;
|
||||
set
|
||||
{
|
||||
var newValue = TimeSpan.FromMinutes(value);
|
||||
if (_model.CheckInterval != newValue)
|
||||
{
|
||||
_model.CheckInterval = newValue;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum degree of parallelism.
|
||||
/// </summary>
|
||||
public int MaxDegreeOfParallelism
|
||||
{
|
||||
get => _model.MaxDegreeOfParallelism;
|
||||
set
|
||||
{
|
||||
if (_model.MaxDegreeOfParallelism != value)
|
||||
{
|
||||
_model.MaxDegreeOfParallelism = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the batch size for sync operations.
|
||||
/// </summary>
|
||||
public int BatchSize
|
||||
{
|
||||
get => _model.BatchSize;
|
||||
set
|
||||
{
|
||||
if (_model.BatchSize != value)
|
||||
{
|
||||
_model.BatchSize = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bulk copy batch size.
|
||||
/// </summary>
|
||||
public int BulkCopyBatchSize
|
||||
{
|
||||
get => _model.BulkCopyBatchSize;
|
||||
set
|
||||
{
|
||||
if (_model.BulkCopyBatchSize != value)
|
||||
{
|
||||
_model.BulkCopyBatchSize = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lookback multiplier.
|
||||
/// </summary>
|
||||
public double LookbackMultiplier
|
||||
{
|
||||
get => _model.LookbackMultiplier;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_model.LookbackMultiplier - value) > 0.001)
|
||||
{
|
||||
_model.LookbackMultiplier = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the purge retention days.
|
||||
/// </summary>
|
||||
public int PurgeRetentionDays
|
||||
{
|
||||
get => _model.PurgeRetentionDays;
|
||||
set
|
||||
{
|
||||
if (_model.PurgeRetentionDays != value)
|
||||
{
|
||||
_model.PurgeRetentionDays = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sync timeout in seconds.
|
||||
/// </summary>
|
||||
public int SyncTimeoutSeconds
|
||||
{
|
||||
get => _model.SyncTimeoutSeconds;
|
||||
set
|
||||
{
|
||||
if (_model.SyncTimeoutSeconds != value)
|
||||
{
|
||||
_model.SyncTimeoutSeconds = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for editing ExcelExport configuration section.
|
||||
/// </summary>
|
||||
public class ExcelExportFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ExcelExportSection _model;
|
||||
private readonly Action _onChanged;
|
||||
private string _selectedTimezone;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available system timezones (IANA format for cross-platform compatibility).
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> AvailableTimezones { get; } = TimeZoneInfo
|
||||
.GetSystemTimeZones()
|
||||
.Select(tz => tz.Id)
|
||||
.OrderBy(id => id)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExcelExportFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The excel export section model to edit.</param>
|
||||
/// <param name="onChanged">The callback to invoke when configuration changes.</param>
|
||||
public ExcelExportFormViewModel(ExcelExportSection model, Action onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
|
||||
// Initialize selected timezone from model (model defaults to "America/Chicago")
|
||||
_selectedTimezone = _model.TimezoneId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of rows per Excel worksheet.
|
||||
/// </summary>
|
||||
public int MaxRowsPerSheet
|
||||
{
|
||||
get => _model.MaxRowsPerSheet;
|
||||
set
|
||||
{
|
||||
if (_model.MaxRowsPerSheet != value)
|
||||
{
|
||||
_model.MaxRowsPerSheet = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default date format for Excel exports.
|
||||
/// </summary>
|
||||
public string DefaultDateFormat
|
||||
{
|
||||
get => _model.DefaultDateFormat;
|
||||
set
|
||||
{
|
||||
if (_model.DefaultDateFormat != value)
|
||||
{
|
||||
_model.DefaultDateFormat = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to write debug output to files.
|
||||
/// </summary>
|
||||
public bool DebugWriteToFile
|
||||
{
|
||||
get => _model.DebugWriteToFile;
|
||||
set
|
||||
{
|
||||
if (_model.DebugWriteToFile != value)
|
||||
{
|
||||
_model.DebugWriteToFile = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory path for debug output files.
|
||||
/// </summary>
|
||||
public string DebugOutputDirectory
|
||||
{
|
||||
get => _model.DebugOutputDirectory;
|
||||
set
|
||||
{
|
||||
if (_model.DebugOutputDirectory != value)
|
||||
{
|
||||
_model.DebugOutputDirectory = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected timezone from the dropdown.
|
||||
/// </summary>
|
||||
public string SelectedTimezone
|
||||
{
|
||||
get => _selectedTimezone;
|
||||
set
|
||||
{
|
||||
if (_selectedTimezone != value)
|
||||
{
|
||||
_selectedTimezone = value;
|
||||
_model.TimezoneId = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for editing LDAP configuration section.
|
||||
/// </summary>
|
||||
public class LdapFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly LdapSection _model;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The LDAP section model to edit.</param>
|
||||
/// <param name="onChanged">The callback to invoke when configuration changes.</param>
|
||||
public LdapFormViewModel(LdapSection model, Action onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server URLs as newline-separated text.
|
||||
/// </summary>
|
||||
public string ServerUrlsText
|
||||
{
|
||||
get => string.Join("\n", _model.ServerUrls);
|
||||
set
|
||||
{
|
||||
var urls = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (!_model.ServerUrls.SequenceEqual(urls))
|
||||
{
|
||||
_model.ServerUrls = urls;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the group distinguished name.
|
||||
/// </summary>
|
||||
public string GroupDn
|
||||
{
|
||||
get => _model.GroupDn;
|
||||
set
|
||||
{
|
||||
if (_model.GroupDn != value)
|
||||
{
|
||||
_model.GroupDn = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the search base distinguished name.
|
||||
/// </summary>
|
||||
public string SearchBase
|
||||
{
|
||||
get => _model.SearchBase;
|
||||
set
|
||||
{
|
||||
if (_model.SearchBase != value)
|
||||
{
|
||||
_model.SearchBase = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection timeout in seconds.
|
||||
/// </summary>
|
||||
public int ConnectionTimeoutSeconds
|
||||
{
|
||||
get => _model.ConnectionTimeoutSeconds;
|
||||
set
|
||||
{
|
||||
if (_model.ConnectionTimeoutSeconds != value)
|
||||
{
|
||||
_model.ConnectionTimeoutSeconds = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use fake authentication.
|
||||
/// </summary>
|
||||
public bool UseFakeAuth
|
||||
{
|
||||
get => _model.UseFakeAuth;
|
||||
set
|
||||
{
|
||||
if (_model.UseFakeAuth != value)
|
||||
{
|
||||
_model.UseFakeAuth = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the admin bypass users as newline-separated text.
|
||||
/// </summary>
|
||||
public string AdminBypassUsersText
|
||||
{
|
||||
get => string.Join("\n", _model.AdminBypassUsers);
|
||||
set
|
||||
{
|
||||
var users = value.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (!_model.AdminBypassUsers.SequenceEqual(users))
|
||||
{
|
||||
_model.AdminBypassUsers = users;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
using JdeScoping.ConfigManager.Ui.Services;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the visual pipeline editor with flow diagram.
|
||||
/// </summary>
|
||||
public class PipelineEditorViewModel : ViewModelBase
|
||||
{
|
||||
private readonly EtlPipelineConfig _model;
|
||||
private readonly Action _onChanged;
|
||||
private readonly IDialogService _dialogService;
|
||||
private PipelineStepViewModelBase? _selectedStep;
|
||||
private object? _selectedStepEditor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipelineEditorViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The pipeline name.</param>
|
||||
/// <param name="model">The pipeline configuration model.</param>
|
||||
/// <param name="availableConnections">List of available connection names from configuration.</param>
|
||||
/// <param name="dialogService">Service for showing dialogs.</param>
|
||||
/// <param name="onChanged">Callback invoked when the pipeline configuration changes.</param>
|
||||
public PipelineEditorViewModel(string name, EtlPipelineConfig model, IReadOnlyList<string> availableConnections, IDialogService dialogService, Action onChanged)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
AvailableConnections = availableConnections ?? [];
|
||||
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
|
||||
// Initialize collections
|
||||
PreScripts = [];
|
||||
Transformers = [];
|
||||
PostScripts = [];
|
||||
|
||||
// Build the pipeline steps from the model
|
||||
BuildPipelineSteps();
|
||||
|
||||
// Initialize commands
|
||||
AddPreScriptCommand = new RelayCommand(AddPreScript);
|
||||
AddTransformerCommand = new RelayCommand(AddTransformer);
|
||||
AddPostScriptCommand = new RelayCommand(AddPostScript);
|
||||
RemoveStepCommand = new RelayCommand<PipelineStepViewModelBase>(RemoveStep);
|
||||
DeleteSelectedStepCommand = new AsyncRelayCommand(DeleteSelectedStepAsync, () => CanDeleteSelectedStep);
|
||||
MoveStepUpCommand = new RelayCommand<PipelineStepViewModelBase>(MoveStepUp, CanMoveStepUp);
|
||||
MoveStepDownCommand = new RelayCommand<PipelineStepViewModelBase>(MoveStepDown, CanMoveStepDown);
|
||||
MoveSelectedStepUpCommand = new RelayCommand(MoveSelectedStepUp, () => CanMoveSelectedStepUp);
|
||||
MoveSelectedStepDownCommand = new RelayCommand(MoveSelectedStepDown, () => CanMoveSelectedStepDown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pipeline name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the available connection names from configuration.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AvailableConnections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pre-script steps.
|
||||
/// </summary>
|
||||
public ObservableCollection<PreScriptStepViewModel> PreScripts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source step.
|
||||
/// </summary>
|
||||
public SourceStepViewModel Source { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transformer steps.
|
||||
/// </summary>
|
||||
public ObservableCollection<TransformerStepViewModelBase> Transformers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the destination step.
|
||||
/// </summary>
|
||||
public DestinationStepViewModel Destination { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the post-script steps.
|
||||
/// </summary>
|
||||
public ObservableCollection<PostScriptStepViewModel> PostScripts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all pipeline steps in flow order for display.
|
||||
/// </summary>
|
||||
public IEnumerable<PipelineStepViewModelBase> AllSteps
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var step in PreScripts)
|
||||
yield return step;
|
||||
yield return Source;
|
||||
foreach (var step in Transformers)
|
||||
yield return step;
|
||||
yield return Destination;
|
||||
foreach (var step in PostScripts)
|
||||
yield return step;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently selected pipeline step.
|
||||
/// </summary>
|
||||
public PipelineStepViewModelBase? SelectedStep
|
||||
{
|
||||
get => _selectedStep;
|
||||
set
|
||||
{
|
||||
if (_selectedStep != value)
|
||||
{
|
||||
// Deselect previous step
|
||||
if (_selectedStep != null)
|
||||
_selectedStep.IsSelected = false;
|
||||
|
||||
_selectedStep = value;
|
||||
|
||||
// Select new step
|
||||
if (_selectedStep != null)
|
||||
_selectedStep.IsSelected = true;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(CanDeleteSelectedStep));
|
||||
OnPropertyChanged(nameof(CanMoveSelectedStepUp));
|
||||
OnPropertyChanged(nameof(CanMoveSelectedStepDown));
|
||||
(DeleteSelectedStepCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(MoveSelectedStepUpCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(MoveSelectedStepDownCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
UpdateSelectedStepEditor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the selected step can be deleted.
|
||||
/// Source and Destination steps cannot be deleted.
|
||||
/// </summary>
|
||||
public bool CanDeleteSelectedStep => _selectedStep is PreScriptStepViewModel
|
||||
or TransformerStepViewModelBase
|
||||
or PostScriptStepViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the selected step can be moved up.
|
||||
/// </summary>
|
||||
public bool CanMoveSelectedStepUp => CanMoveStepUp(_selectedStep);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the selected step can be moved down.
|
||||
/// </summary>
|
||||
public bool CanMoveSelectedStepDown => CanMoveStepDown(_selectedStep);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the editor view model for the currently selected step.
|
||||
/// </summary>
|
||||
public object? SelectedStepEditor
|
||||
{
|
||||
get => _selectedStepEditor;
|
||||
private set => SetProperty(ref _selectedStepEditor, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the pipeline is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _model.IsEnabled;
|
||||
set
|
||||
{
|
||||
if (_model.IsEnabled != value)
|
||||
{
|
||||
_model.IsEnabled = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the pipeline is manual-only.
|
||||
/// </summary>
|
||||
public bool IsManualOnly
|
||||
{
|
||||
get => _model.IsManualOnly;
|
||||
set
|
||||
{
|
||||
if (_model.IsManualOnly != value)
|
||||
{
|
||||
_model.IsManualOnly = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether mass sync is enabled.
|
||||
/// </summary>
|
||||
public bool MassSyncEnabled
|
||||
{
|
||||
get => _model.MassSyncIntervalMinutes.HasValue;
|
||||
set
|
||||
{
|
||||
if (value && !_model.MassSyncIntervalMinutes.HasValue)
|
||||
{
|
||||
_model.MassSyncIntervalMinutes = 10080; // 1 week
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(MassSyncIntervalMinutes));
|
||||
_onChanged();
|
||||
}
|
||||
else if (!value && _model.MassSyncIntervalMinutes.HasValue)
|
||||
{
|
||||
_model.MassSyncIntervalMinutes = null;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(MassSyncIntervalMinutes));
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mass sync interval in minutes.
|
||||
/// </summary>
|
||||
public int MassSyncIntervalMinutes
|
||||
{
|
||||
get => _model.MassSyncIntervalMinutes ?? 10080;
|
||||
set
|
||||
{
|
||||
if (_model.MassSyncIntervalMinutes != value)
|
||||
{
|
||||
_model.MassSyncIntervalMinutes = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether daily sync is enabled.
|
||||
/// </summary>
|
||||
public bool DailySyncEnabled
|
||||
{
|
||||
get => _model.DailySyncIntervalMinutes.HasValue;
|
||||
set
|
||||
{
|
||||
if (value && !_model.DailySyncIntervalMinutes.HasValue)
|
||||
{
|
||||
_model.DailySyncIntervalMinutes = 1440; // 1 day
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(DailySyncIntervalMinutes));
|
||||
_onChanged();
|
||||
}
|
||||
else if (!value && _model.DailySyncIntervalMinutes.HasValue)
|
||||
{
|
||||
_model.DailySyncIntervalMinutes = null;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(DailySyncIntervalMinutes));
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the daily sync interval in minutes.
|
||||
/// </summary>
|
||||
public int DailySyncIntervalMinutes
|
||||
{
|
||||
get => _model.DailySyncIntervalMinutes ?? 1440;
|
||||
set
|
||||
{
|
||||
if (_model.DailySyncIntervalMinutes != value)
|
||||
{
|
||||
_model.DailySyncIntervalMinutes = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether hourly sync is enabled.
|
||||
/// </summary>
|
||||
public bool HourlySyncEnabled
|
||||
{
|
||||
get => _model.HourlySyncIntervalMinutes.HasValue;
|
||||
set
|
||||
{
|
||||
if (value && !_model.HourlySyncIntervalMinutes.HasValue)
|
||||
{
|
||||
_model.HourlySyncIntervalMinutes = 60; // 1 hour
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HourlySyncIntervalMinutes));
|
||||
_onChanged();
|
||||
}
|
||||
else if (!value && _model.HourlySyncIntervalMinutes.HasValue)
|
||||
{
|
||||
_model.HourlySyncIntervalMinutes = null;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HourlySyncIntervalMinutes));
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hourly sync interval in minutes.
|
||||
/// </summary>
|
||||
public int HourlySyncIntervalMinutes
|
||||
{
|
||||
get => _model.HourlySyncIntervalMinutes ?? 60;
|
||||
set
|
||||
{
|
||||
if (_model.HourlySyncIntervalMinutes != value)
|
||||
{
|
||||
_model.HourlySyncIntervalMinutes = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commands
|
||||
/// <summary>
|
||||
/// Gets the command to add a pre-script step.
|
||||
/// </summary>
|
||||
public ICommand AddPreScriptCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to add a transformer step.
|
||||
/// </summary>
|
||||
public ICommand AddTransformerCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to add a post-script step.
|
||||
/// </summary>
|
||||
public ICommand AddPostScriptCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to remove a pipeline step.
|
||||
/// </summary>
|
||||
public ICommand RemoveStepCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to delete the currently selected step.
|
||||
/// </summary>
|
||||
public ICommand DeleteSelectedStepCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to move a step up in its collection.
|
||||
/// </summary>
|
||||
public ICommand MoveStepUpCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to move a step down in its collection.
|
||||
/// </summary>
|
||||
public ICommand MoveStepDownCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to move the selected step up.
|
||||
/// </summary>
|
||||
public ICommand MoveSelectedStepUpCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to move the selected step down.
|
||||
/// </summary>
|
||||
public ICommand MoveSelectedStepDownCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available transformer types for the add dialog.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AvailableTransformerTypes => TransformerFactory.AvailableTypes;
|
||||
|
||||
private string? _selectedTransformerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected transformer type for adding a new transformer.
|
||||
/// </summary>
|
||||
public string? SelectedTransformerType
|
||||
{
|
||||
get => _selectedTransformerType;
|
||||
set => SetProperty(ref _selectedTransformerType, value);
|
||||
}
|
||||
|
||||
private void BuildPipelineSteps()
|
||||
{
|
||||
// Pre-scripts
|
||||
PreScripts.Clear();
|
||||
foreach (var script in _model.PreScripts)
|
||||
{
|
||||
PreScripts.Add(new PreScriptStepViewModel(script, () =>
|
||||
{
|
||||
SyncPreScriptsToModel();
|
||||
_onChanged();
|
||||
}));
|
||||
}
|
||||
|
||||
// Source
|
||||
Source = new SourceStepViewModel(_model.Source, AvailableConnections, () =>
|
||||
{
|
||||
_onChanged();
|
||||
});
|
||||
|
||||
// Transformers
|
||||
Transformers.Clear();
|
||||
foreach (var transform in _model.Transforms)
|
||||
{
|
||||
var vm = TransformerFactory.Create(transform, () =>
|
||||
{
|
||||
SyncTransformersToModel();
|
||||
_onChanged();
|
||||
});
|
||||
if (vm != null)
|
||||
Transformers.Add(vm);
|
||||
}
|
||||
|
||||
// Destination
|
||||
Destination = new DestinationStepViewModel(_model.Destination, () =>
|
||||
{
|
||||
_onChanged();
|
||||
});
|
||||
|
||||
// Post-scripts
|
||||
PostScripts.Clear();
|
||||
foreach (var script in _model.PostScripts)
|
||||
{
|
||||
PostScripts.Add(new PostScriptStepViewModel(script, () =>
|
||||
{
|
||||
SyncPostScriptsToModel();
|
||||
_onChanged();
|
||||
}));
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
}
|
||||
|
||||
private void UpdateSelectedStepEditor()
|
||||
{
|
||||
// The selected step IS the editor - we use the step VM directly
|
||||
// The view will use DataTemplates to show the appropriate editor
|
||||
SelectedStepEditor = _selectedStep;
|
||||
}
|
||||
|
||||
private void AddPreScript()
|
||||
{
|
||||
var step = new PreScriptStepViewModel(() =>
|
||||
{
|
||||
SyncPreScriptsToModel();
|
||||
_onChanged();
|
||||
});
|
||||
PreScripts.Add(step);
|
||||
SyncPreScriptsToModel();
|
||||
_onChanged();
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
SelectedStep = step;
|
||||
}
|
||||
|
||||
private void AddTransformer()
|
||||
{
|
||||
// Default to ColumnDrop if nothing selected
|
||||
var type = SelectedTransformerType ?? "ColumnDrop";
|
||||
var vm = TransformerFactory.CreateNew(type, () =>
|
||||
{
|
||||
SyncTransformersToModel();
|
||||
_onChanged();
|
||||
});
|
||||
if (vm != null)
|
||||
{
|
||||
Transformers.Add(vm);
|
||||
SyncTransformersToModel();
|
||||
_onChanged();
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
SelectedStep = vm;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specific transformer type.
|
||||
/// </summary>
|
||||
/// <param name="typeName">The name of the transformer type to add.</param>
|
||||
public void AddTransformerOfType(string typeName)
|
||||
{
|
||||
var vm = TransformerFactory.CreateNew(typeName, () =>
|
||||
{
|
||||
SyncTransformersToModel();
|
||||
_onChanged();
|
||||
});
|
||||
if (vm != null)
|
||||
{
|
||||
Transformers.Add(vm);
|
||||
SyncTransformersToModel();
|
||||
_onChanged();
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
SelectedStep = vm;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPostScript()
|
||||
{
|
||||
var step = new PostScriptStepViewModel(() =>
|
||||
{
|
||||
SyncPostScriptsToModel();
|
||||
_onChanged();
|
||||
});
|
||||
PostScripts.Add(step);
|
||||
SyncPostScriptsToModel();
|
||||
_onChanged();
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
SelectedStep = step;
|
||||
}
|
||||
|
||||
private void RemoveStep(PipelineStepViewModelBase? step)
|
||||
{
|
||||
if (step == null) return;
|
||||
|
||||
switch (step)
|
||||
{
|
||||
case PreScriptStepViewModel preScript:
|
||||
PreScripts.Remove(preScript);
|
||||
SyncPreScriptsToModel();
|
||||
break;
|
||||
case TransformerStepViewModelBase transformer:
|
||||
Transformers.Remove(transformer);
|
||||
SyncTransformersToModel();
|
||||
break;
|
||||
case PostScriptStepViewModel postScript:
|
||||
PostScripts.Remove(postScript);
|
||||
SyncPostScriptsToModel();
|
||||
break;
|
||||
// Source and Destination cannot be removed
|
||||
}
|
||||
|
||||
_onChanged();
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
|
||||
if (SelectedStep == step)
|
||||
SelectedStep = null;
|
||||
}
|
||||
|
||||
private bool CanMoveStepUp(PipelineStepViewModelBase? step)
|
||||
{
|
||||
if (step == null) return false;
|
||||
|
||||
return step switch
|
||||
{
|
||||
PreScriptStepViewModel preScript => PreScripts.IndexOf(preScript) > 0,
|
||||
TransformerStepViewModelBase transformer => Transformers.IndexOf(transformer) > 0,
|
||||
PostScriptStepViewModel postScript => PostScripts.IndexOf(postScript) > 0,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private void MoveStepUp(PipelineStepViewModelBase? step)
|
||||
{
|
||||
if (step == null || !CanMoveStepUp(step)) return;
|
||||
|
||||
switch (step)
|
||||
{
|
||||
case PreScriptStepViewModel preScript:
|
||||
MoveInCollection(PreScripts, preScript, -1);
|
||||
SyncPreScriptsToModel();
|
||||
break;
|
||||
case TransformerStepViewModelBase transformer:
|
||||
MoveInCollection(Transformers, transformer, -1);
|
||||
SyncTransformersToModel();
|
||||
break;
|
||||
case PostScriptStepViewModel postScript:
|
||||
MoveInCollection(PostScripts, postScript, -1);
|
||||
SyncPostScriptsToModel();
|
||||
break;
|
||||
}
|
||||
|
||||
_onChanged();
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
}
|
||||
|
||||
private bool CanMoveStepDown(PipelineStepViewModelBase? step)
|
||||
{
|
||||
if (step == null) return false;
|
||||
|
||||
return step switch
|
||||
{
|
||||
PreScriptStepViewModel preScript => PreScripts.IndexOf(preScript) < PreScripts.Count - 1,
|
||||
TransformerStepViewModelBase transformer => Transformers.IndexOf(transformer) < Transformers.Count - 1,
|
||||
PostScriptStepViewModel postScript => PostScripts.IndexOf(postScript) < PostScripts.Count - 1,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private void MoveStepDown(PipelineStepViewModelBase? step)
|
||||
{
|
||||
if (step == null || !CanMoveStepDown(step)) return;
|
||||
|
||||
switch (step)
|
||||
{
|
||||
case PreScriptStepViewModel preScript:
|
||||
MoveInCollection(PreScripts, preScript, 1);
|
||||
SyncPreScriptsToModel();
|
||||
break;
|
||||
case TransformerStepViewModelBase transformer:
|
||||
MoveInCollection(Transformers, transformer, 1);
|
||||
SyncTransformersToModel();
|
||||
break;
|
||||
case PostScriptStepViewModel postScript:
|
||||
MoveInCollection(PostScripts, postScript, 1);
|
||||
SyncPostScriptsToModel();
|
||||
break;
|
||||
}
|
||||
|
||||
_onChanged();
|
||||
OnPropertyChanged(nameof(AllSteps));
|
||||
}
|
||||
|
||||
private void MoveSelectedStepUp()
|
||||
{
|
||||
if (_selectedStep == null) return;
|
||||
MoveStepUp(_selectedStep);
|
||||
RaiseMoveCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void MoveSelectedStepDown()
|
||||
{
|
||||
if (_selectedStep == null) return;
|
||||
MoveStepDown(_selectedStep);
|
||||
RaiseMoveCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void RaiseMoveCanExecuteChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(CanMoveSelectedStepUp));
|
||||
OnPropertyChanged(nameof(CanMoveSelectedStepDown));
|
||||
(MoveSelectedStepUpCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(MoveSelectedStepDownCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private static void MoveInCollection<T>(ObservableCollection<T> collection, T item, int offset)
|
||||
{
|
||||
var index = collection.IndexOf(item);
|
||||
if (index < 0) return;
|
||||
var newIndex = index + offset;
|
||||
if (newIndex < 0 || newIndex >= collection.Count) return;
|
||||
collection.Move(index, newIndex);
|
||||
}
|
||||
|
||||
private void SyncPreScriptsToModel()
|
||||
{
|
||||
_model.PreScripts.Clear();
|
||||
foreach (var script in PreScripts)
|
||||
{
|
||||
_model.PreScripts.Add(script.ToModel());
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncTransformersToModel()
|
||||
{
|
||||
_model.Transforms.Clear();
|
||||
foreach (var transformer in Transformers)
|
||||
{
|
||||
_model.Transforms.Add(transformer.ToModel());
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncPostScriptsToModel()
|
||||
{
|
||||
_model.PostScripts.Clear();
|
||||
foreach (var script in PostScripts)
|
||||
{
|
||||
_model.PostScripts.Add(script.ToModel());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the currently selected step after user confirmation.
|
||||
/// Source and Destination steps cannot be deleted.
|
||||
/// </summary>
|
||||
private async Task DeleteSelectedStepAsync()
|
||||
{
|
||||
if (_selectedStep == null || !CanDeleteSelectedStep)
|
||||
return;
|
||||
|
||||
var stepTypeName = _selectedStep switch
|
||||
{
|
||||
PreScriptStepViewModel => "Pre-Script",
|
||||
TransformerStepViewModelBase t => $"Transformer ({t.TransformerType})",
|
||||
PostScriptStepViewModel => "Post-Script",
|
||||
_ => "Step"
|
||||
};
|
||||
|
||||
var confirmed = await _dialogService.ShowConfirmationAsync(
|
||||
"Delete Step",
|
||||
$"Are you sure you want to delete this {stepTypeName}?\n\nThis action cannot be undone.");
|
||||
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
RemoveStep(_selectedStep);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for editing Search configuration section.
|
||||
/// </summary>
|
||||
public class SearchFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly SearchSection _model;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SearchFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The search section model to edit.</param>
|
||||
/// <param name="onChanged">The callback to invoke when configuration changes.</param>
|
||||
public SearchFormViewModel(SearchSection model, Action onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of result rows returned by a search.
|
||||
/// </summary>
|
||||
public int MaxResultRows
|
||||
{
|
||||
get => _model.MaxResultRows;
|
||||
set
|
||||
{
|
||||
if (_model.MaxResultRows != value)
|
||||
{
|
||||
_model.MaxResultRows = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in seconds for search operations.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds
|
||||
{
|
||||
get => _model.TimeoutSeconds;
|
||||
set
|
||||
{
|
||||
if (_model.TimeoutSeconds != value)
|
||||
{
|
||||
_model.TimeoutSeconds = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of concurrent search operations allowed.
|
||||
/// </summary>
|
||||
public int MaxConcurrentSearches
|
||||
{
|
||||
get => _model.MaxConcurrentSearches;
|
||||
set
|
||||
{
|
||||
if (_model.MaxConcurrentSearches != value)
|
||||
{
|
||||
_model.MaxConcurrentSearches = value;
|
||||
OnPropertyChanged();
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.ConfigManager.Ui.Services;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for displaying and editing a secret.
|
||||
/// </summary>
|
||||
public class SecretFormViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly Action<string> _onValueChanged;
|
||||
private readonly Action _onDeleteRequested;
|
||||
private string _value;
|
||||
private bool _isValueVisible;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key (read-only).</param>
|
||||
/// <param name="value">The initial secret value.</param>
|
||||
/// <param name="clipboardService">The clipboard service for copying values.</param>
|
||||
/// <param name="onValueChanged">The action to invoke when the value changes.</param>
|
||||
/// <param name="onDeleteRequested">The action to invoke when deletion is requested.</param>
|
||||
public SecretFormViewModel(
|
||||
string key,
|
||||
string value,
|
||||
IClipboardService clipboardService,
|
||||
Action<string> onValueChanged,
|
||||
Action onDeleteRequested)
|
||||
{
|
||||
Key = key ?? throw new ArgumentNullException(nameof(key));
|
||||
_value = value ?? string.Empty;
|
||||
_clipboardService = clipboardService ?? throw new ArgumentNullException(nameof(clipboardService));
|
||||
_onValueChanged = onValueChanged ?? throw new ArgumentNullException(nameof(onValueChanged));
|
||||
_onDeleteRequested = onDeleteRequested ?? throw new ArgumentNullException(nameof(onDeleteRequested));
|
||||
|
||||
ToggleVisibilityCommand = new RelayCommand(() => IsValueVisible = !IsValueVisible);
|
||||
CopyToClipboardCommand = new AsyncRelayCommand(CopyToClipboardAsync);
|
||||
DeleteCommand = new RelayCommand(_onDeleteRequested);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the secret key (read-only).
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret value.
|
||||
/// </summary>
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _value, value))
|
||||
{
|
||||
_onValueChanged(value);
|
||||
OnPropertyChanged(nameof(DisplayValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the value is visible (unmasked).
|
||||
/// </summary>
|
||||
public bool IsValueVisible
|
||||
{
|
||||
get => _isValueVisible;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isValueVisible, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayValue));
|
||||
OnPropertyChanged(nameof(VisibilityButtonText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display value (masked or unmasked based on visibility).
|
||||
/// </summary>
|
||||
public string DisplayValue => IsValueVisible ? Value : new string('\u2022', Math.Min(Value?.Length ?? 0, 20));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text for the visibility toggle button.
|
||||
/// </summary>
|
||||
public string VisibilityButtonText => IsValueVisible ? "Hide" : "Show";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to toggle value visibility.
|
||||
/// </summary>
|
||||
public ICommand ToggleVisibilityCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to copy the value to clipboard.
|
||||
/// </summary>
|
||||
public ICommand CopyToClipboardCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to delete this secret.
|
||||
/// </summary>
|
||||
public ICommand DeleteCommand { get; }
|
||||
|
||||
private async Task CopyToClipboardAsync()
|
||||
{
|
||||
await _clipboardService.SetTextAsync(Value);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// View model for the SecureStore info panel shown when the store node is selected.
|
||||
/// </summary>
|
||||
public class SecureStoreInfoFormViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the instruction text to display.
|
||||
/// </summary>
|
||||
public string InstructionText => "Select a secret from the tree to view or edit its value.";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the store path for display.
|
||||
/// </summary>
|
||||
public string StorePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key file path for display.
|
||||
/// </summary>
|
||||
public string KeyFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of secrets in the store.
|
||||
/// </summary>
|
||||
public int SecretCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecureStoreInfoFormViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path to the secure store file.</param>
|
||||
/// <param name="keyFilePath">The path to the key file.</param>
|
||||
/// <param name="secretCount">The number of secrets in the store.</param>
|
||||
public SecureStoreInfoFormViewModel(string storePath, string keyFilePath, int secretCount)
|
||||
{
|
||||
StorePath = storePath;
|
||||
KeyFilePath = keyFilePath;
|
||||
SecretCount = secretCount;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+87
@@ -0,0 +1,87 @@
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
|
||||
|
||||
/// <summary>
|
||||
/// View model for the destination step in a pipeline.
|
||||
/// </summary>
|
||||
public class DestinationStepViewModel : PipelineStepViewModelBase
|
||||
{
|
||||
private readonly DestinationElement _model;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DestinationStepViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The destination configuration model.</param>
|
||||
/// <param name="onChanged">Callback invoked when the destination configuration changes.</param>
|
||||
public DestinationStepViewModel(DestinationElement model, Action onChanged) : base(onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PipelineStepType StepType => PipelineStepType.Destination;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "Destination";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Icon => ""; // mdi-database
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Summary => !string.IsNullOrEmpty(Table) ? $"→ {Table}" : "(no table)";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the destination table name.
|
||||
/// </summary>
|
||||
public string Table
|
||||
{
|
||||
get => _model.Table;
|
||||
set
|
||||
{
|
||||
if (_model.Table != value)
|
||||
{
|
||||
_model.Table = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the match columns as newline-separated text.
|
||||
/// </summary>
|
||||
public string MatchColumnsText
|
||||
{
|
||||
get => string.Join("\n", _model.MatchColumns);
|
||||
set
|
||||
{
|
||||
var columns = (value ?? string.Empty).Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
if (!_model.MatchColumns.SequenceEqual(columns))
|
||||
{
|
||||
_model.MatchColumns = columns;
|
||||
OnPropertyChanged();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the columns to exclude from updates as newline-separated text.
|
||||
/// </summary>
|
||||
public string ExcludeFromUpdateText
|
||||
{
|
||||
get => string.Join("\n", _model.ExcludeFromUpdate);
|
||||
set
|
||||
{
|
||||
var columns = (value ?? string.Empty).Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
if (!_model.ExcludeFromUpdate.SequenceEqual(columns))
|
||||
{
|
||||
_model.ExcludeFromUpdate = columns;
|
||||
OnPropertyChanged();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
|
||||
|
||||
/// <summary>
|
||||
/// Type of pipeline step in the flow diagram.
|
||||
/// </summary>
|
||||
public enum PipelineStepType
|
||||
{
|
||||
PreScript,
|
||||
Source,
|
||||
Transformer,
|
||||
Destination,
|
||||
PostScript
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all pipeline step view models in the visual flow diagram.
|
||||
/// </summary>
|
||||
public abstract class PipelineStepViewModelBase : ViewModelBase
|
||||
{
|
||||
private bool _isSelected;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipelineStepViewModelBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the step changes.</param>
|
||||
protected PipelineStepViewModelBase(Action onChanged)
|
||||
{
|
||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of this pipeline step.
|
||||
/// </summary>
|
||||
public abstract PipelineStepType StepType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name for this step.
|
||||
/// </summary>
|
||||
public abstract string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon character for this step (using Material Design Icons).
|
||||
/// </summary>
|
||||
public abstract string Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a short description for this step shown in the flow diagram.
|
||||
/// </summary>
|
||||
public abstract string Summary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this step is currently selected.
|
||||
/// </summary>
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies that the step has changed.
|
||||
/// </summary>
|
||||
protected void NotifyChanged()
|
||||
{
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for a pre-script step.
|
||||
/// </summary>
|
||||
public class PreScriptStepViewModel : PipelineStepViewModelBase
|
||||
{
|
||||
private readonly ScriptElement _model;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PreScriptStepViewModel"/> class with an existing script.
|
||||
/// </summary>
|
||||
/// <param name="model">The script element to wrap.</param>
|
||||
/// <param name="onChanged">Callback invoked when the script changes.</param>
|
||||
public PreScriptStepViewModel(ScriptElement model, Action onChanged) : base(onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new pre-script with default values.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the script changes.</param>
|
||||
public PreScriptStepViewModel(Action onChanged) : base(onChanged)
|
||||
{
|
||||
_model = new ScriptElement { Connection = "lotfinder", Script = string.Empty };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PipelineStepType StepType => PipelineStepType.PreScript;
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "Pre-Script";
|
||||
/// <inheritdoc />
|
||||
public override string Icon => ""; // mdi-script-text
|
||||
/// <inheritdoc />
|
||||
public override string Summary => TruncateScript(_model.Script);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection for script execution.
|
||||
/// </summary>
|
||||
public string Connection
|
||||
{
|
||||
get => _model.Connection;
|
||||
set
|
||||
{
|
||||
if (_model.Connection != value)
|
||||
{
|
||||
_model.Connection = value ?? "lotfinder";
|
||||
OnPropertyChanged();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL script content.
|
||||
/// </summary>
|
||||
public string Script
|
||||
{
|
||||
get => _model.Script;
|
||||
set
|
||||
{
|
||||
if (_model.Script != value)
|
||||
{
|
||||
_model.Script = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying model.
|
||||
/// </summary>
|
||||
public ScriptElement ToModel() => _model;
|
||||
|
||||
private static string TruncateScript(string script)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(script)) return "(empty)";
|
||||
var firstLine = script.Split('\n')[0].Trim();
|
||||
return firstLine.Length > 30 ? firstLine[..27] + "..." : firstLine;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for a post-script step.
|
||||
/// </summary>
|
||||
public class PostScriptStepViewModel : PipelineStepViewModelBase
|
||||
{
|
||||
private readonly ScriptElement _model;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostScriptStepViewModel"/> class with an existing script.
|
||||
/// </summary>
|
||||
/// <param name="model">The script element to wrap.</param>
|
||||
/// <param name="onChanged">Callback invoked when the script changes.</param>
|
||||
public PostScriptStepViewModel(ScriptElement model, Action onChanged) : base(onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new post-script with default values.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the script changes.</param>
|
||||
public PostScriptStepViewModel(Action onChanged) : base(onChanged)
|
||||
{
|
||||
_model = new ScriptElement { Connection = "lotfinder", Script = string.Empty };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PipelineStepType StepType => PipelineStepType.PostScript;
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "Post-Script";
|
||||
/// <inheritdoc />
|
||||
public override string Icon => ""; // mdi-script-text
|
||||
/// <inheritdoc />
|
||||
public override string Summary => TruncateScript(_model.Script);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection for script execution.
|
||||
/// </summary>
|
||||
public string Connection
|
||||
{
|
||||
get => _model.Connection;
|
||||
set
|
||||
{
|
||||
if (_model.Connection != value)
|
||||
{
|
||||
_model.Connection = value ?? "lotfinder";
|
||||
OnPropertyChanged();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL script content.
|
||||
/// </summary>
|
||||
public string Script
|
||||
{
|
||||
get => _model.Script;
|
||||
set
|
||||
{
|
||||
if (_model.Script != value)
|
||||
{
|
||||
_model.Script = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying model.
|
||||
/// </summary>
|
||||
public ScriptElement ToModel() => _model;
|
||||
|
||||
private static string TruncateScript(string script)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(script)) return "(empty)";
|
||||
var firstLine = script.Split('\n')[0].Trim();
|
||||
return firstLine.Length > 30 ? firstLine[..27] + "..." : firstLine;
|
||||
}
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
|
||||
|
||||
/// <summary>
|
||||
/// View model for the source step in a pipeline.
|
||||
/// </summary>
|
||||
public class SourceStepViewModel : PipelineStepViewModelBase
|
||||
{
|
||||
private readonly SourceElement _model;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SourceStepViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The source configuration model.</param>
|
||||
/// <param name="availableConnections">List of available connection names from configuration.</param>
|
||||
/// <param name="onChanged">Callback invoked when the source configuration changes.</param>
|
||||
public SourceStepViewModel(SourceElement model, IReadOnlyList<string> availableConnections, Action onChanged) : base(onChanged)
|
||||
{
|
||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
AvailableConnections = availableConnections ?? [];
|
||||
|
||||
// Initialize parameters collection
|
||||
Parameters = new ObservableCollection<ParameterViewModel>(
|
||||
_model.Parameters.Select(kvp => new ParameterViewModel(kvp.Key, kvp.Value, () =>
|
||||
{
|
||||
SyncParametersToModel();
|
||||
NotifyChanged();
|
||||
})));
|
||||
|
||||
// Initialize commands
|
||||
AddParameterCommand = new RelayCommand(AddParameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available connection string names from configuration.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AvailableConnections { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PipelineStepType StepType => PipelineStepType.Source;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "Source";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Icon => ""; // mdi-database
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Summary => $"{Connection}: {TruncateQuery(Query)}";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source database connection name.
|
||||
/// </summary>
|
||||
public string Connection
|
||||
{
|
||||
get => _model.Connection;
|
||||
set
|
||||
{
|
||||
if (_model.Connection != value)
|
||||
{
|
||||
_model.Connection = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query to extract data from the source.
|
||||
/// </summary>
|
||||
public string Query
|
||||
{
|
||||
get => _model.Query;
|
||||
set
|
||||
{
|
||||
if (_model.Query != value)
|
||||
{
|
||||
_model.Query = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional mass query for full data extraction.
|
||||
/// </summary>
|
||||
public string? MassQuery
|
||||
{
|
||||
get => _model.MassQuery;
|
||||
set
|
||||
{
|
||||
if (_model.MassQuery != value)
|
||||
{
|
||||
_model.MassQuery = value;
|
||||
OnPropertyChanged();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of query parameters.
|
||||
/// </summary>
|
||||
public ObservableCollection<ParameterViewModel> Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to add a new parameter.
|
||||
/// </summary>
|
||||
public ICommand AddParameterCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new parameter.
|
||||
/// </summary>
|
||||
public void AddParameter()
|
||||
{
|
||||
var key = $"param{Parameters.Count + 1}";
|
||||
var param = new ParameterElement { Name = key, Source = "offset" };
|
||||
var vm = new ParameterViewModel(key, param, () =>
|
||||
{
|
||||
SyncParametersToModel();
|
||||
NotifyChanged();
|
||||
});
|
||||
Parameters.Add(vm);
|
||||
SyncParametersToModel();
|
||||
NotifyChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter view model to remove.</param>
|
||||
public void RemoveParameter(ParameterViewModel parameter)
|
||||
{
|
||||
if (Parameters.Remove(parameter))
|
||||
{
|
||||
SyncParametersToModel();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncParametersToModel()
|
||||
{
|
||||
_model.Parameters.Clear();
|
||||
foreach (var p in Parameters)
|
||||
{
|
||||
_model.Parameters[p.Key] = p.ToModel();
|
||||
}
|
||||
}
|
||||
|
||||
private static string TruncateQuery(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return "(no query)";
|
||||
var trimmed = query.Trim().Replace("\n", " ").Replace("\r", "");
|
||||
return trimmed.Length > 25 ? trimmed[..22] + "..." : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for a query parameter.
|
||||
/// </summary>
|
||||
public class ParameterViewModel : ViewModelBase
|
||||
{
|
||||
private string _key;
|
||||
private string _name;
|
||||
private string? _format;
|
||||
private string _source;
|
||||
private string? _value;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ParameterViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The parameter key used in the dictionary.</param>
|
||||
/// <param name="model">The parameter configuration model.</param>
|
||||
/// <param name="onChanged">Callback invoked when the parameter changes.</param>
|
||||
public ParameterViewModel(string key, ParameterElement model, Action onChanged)
|
||||
{
|
||||
_key = key;
|
||||
_name = model.Name;
|
||||
_format = model.Format;
|
||||
_source = model.Source;
|
||||
_value = model.Value;
|
||||
_onChanged = onChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter key (used in the dictionary).
|
||||
/// </summary>
|
||||
public string Key
|
||||
{
|
||||
get => _key;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _key, value ?? string.Empty))
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter name.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _name, value ?? string.Empty))
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter format (e.g., jdeJulian, jdeTime).
|
||||
/// </summary>
|
||||
public string? Format
|
||||
{
|
||||
get => _format;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _format, value))
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter source (e.g., offset, static).
|
||||
/// </summary>
|
||||
public string Source
|
||||
{
|
||||
get => _source;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _source, value ?? "offset"))
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the static value (if source is not offset).
|
||||
/// </summary>
|
||||
public string? Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _value, value))
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this view model back to a model.
|
||||
/// </summary>
|
||||
public ParameterElement ToModel() => new()
|
||||
{
|
||||
Name = _name,
|
||||
Format = _format,
|
||||
Source = _source,
|
||||
Value = _value
|
||||
};
|
||||
}
|
||||
+786
@@ -0,0 +1,786 @@
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for transformer step view models.
|
||||
/// </summary>
|
||||
public abstract class TransformerStepViewModelBase : PipelineStepViewModelBase
|
||||
{
|
||||
protected static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransformerStepViewModelBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the transformer changes.</param>
|
||||
protected TransformerStepViewModelBase(Action onChanged) : base(onChanged)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PipelineStepType StepType => PipelineStepType.Transformer;
|
||||
/// <inheritdoc />
|
||||
public override string Icon => ""; // mdi-cog-transfer
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transformer type name.
|
||||
/// </summary>
|
||||
public abstract string TransformerType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts this view model back to a TransformElement.
|
||||
/// </summary>
|
||||
public abstract TransformElement ToModel();
|
||||
|
||||
/// <summary>
|
||||
/// Helper to create a JsonElement from an object.
|
||||
/// </summary>
|
||||
/// <param name="config">The object to serialize into a JsonElement.</param>
|
||||
/// <returns>A JsonElement containing the serialized configuration.</returns>
|
||||
protected static JsonElement CreateConfigElement(object config)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(config, JsonOptions);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for ColumnDrop transformer.
|
||||
/// </summary>
|
||||
public class ColumnDropTransformerViewModel : TransformerStepViewModelBase
|
||||
{
|
||||
private string _columnsText;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnDropTransformerViewModel"/> class with an existing configuration.
|
||||
/// </summary>
|
||||
/// <param name="element">The transform element containing the column configuration.</param>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public ColumnDropTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
|
||||
{
|
||||
_columnsText = string.Empty;
|
||||
if (element.Config.HasValue)
|
||||
{
|
||||
if (element.Config.Value.TryGetProperty("columns", out var columnsProp) &&
|
||||
columnsProp.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var columns = columnsProp.EnumerateArray().Select(c => c.GetString() ?? "").Where(c => !string.IsNullOrEmpty(c));
|
||||
_columnsText = string.Join("\n", columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnDropTransformerViewModel"/> class with default values.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public ColumnDropTransformerViewModel(Action onChanged) : base(onChanged)
|
||||
{
|
||||
_columnsText = string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string TransformerType => "ColumnDrop";
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "Column Drop";
|
||||
/// <inheritdoc />
|
||||
public override string Summary => GetColumnCount() > 0 ? $"Drop {GetColumnCount()} columns" : "No columns";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the columns to drop as newline-separated text.
|
||||
/// </summary>
|
||||
public string ColumnsText
|
||||
{
|
||||
get => _columnsText;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _columnsText, value ?? string.Empty))
|
||||
{
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the columns as a list.
|
||||
/// </summary>
|
||||
public List<string> GetColumns()
|
||||
{
|
||||
return _columnsText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
}
|
||||
|
||||
private int GetColumnCount()
|
||||
{
|
||||
return _columnsText.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Length;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override TransformElement ToModel() => new()
|
||||
{
|
||||
TransformType = TransformerType,
|
||||
Config = CreateConfigElement(new { columns = GetColumns() })
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for ColumnRename transformer.
|
||||
/// </summary>
|
||||
public class ColumnRenameTransformerViewModel : TransformerStepViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnRenameTransformerViewModel"/> class with an existing configuration.
|
||||
/// </summary>
|
||||
/// <param name="element">The transform element containing the mapping configuration.</param>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public ColumnRenameTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
|
||||
{
|
||||
Mappings = [];
|
||||
if (element.Config.HasValue)
|
||||
{
|
||||
if (element.Config.Value.TryGetProperty("mappings", out var mappingsProp) &&
|
||||
mappingsProp.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var prop in mappingsProp.EnumerateObject())
|
||||
{
|
||||
var newName = prop.Value.GetString() ?? "";
|
||||
Mappings.Add(new ColumnMappingViewModel(prop.Name, newName, () =>
|
||||
{
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
AddMappingCommand = new RelayCommand(AddMapping);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnRenameTransformerViewModel"/> class with default values.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public ColumnRenameTransformerViewModel(Action onChanged) : base(onChanged)
|
||||
{
|
||||
Mappings = [];
|
||||
AddMappingCommand = new RelayCommand(AddMapping);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string TransformerType => "ColumnRename";
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "Column Rename";
|
||||
/// <inheritdoc />
|
||||
public override string Summary => Mappings.Count > 0 ? $"Rename {Mappings.Count} columns" : "No mappings";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of column mappings (old name -> new name).
|
||||
/// </summary>
|
||||
public ObservableCollection<ColumnMappingViewModel> Mappings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to add a new mapping.
|
||||
/// </summary>
|
||||
public ICommand AddMappingCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new mapping.
|
||||
/// </summary>
|
||||
public void AddMapping()
|
||||
{
|
||||
Mappings.Add(new ColumnMappingViewModel("", "", () =>
|
||||
{
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}));
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a mapping.
|
||||
/// </summary>
|
||||
/// <param name="mapping">The column mapping to remove.</param>
|
||||
public void RemoveMapping(ColumnMappingViewModel mapping)
|
||||
{
|
||||
if (Mappings.Remove(mapping))
|
||||
{
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override TransformElement ToModel() => new()
|
||||
{
|
||||
TransformType = TransformerType,
|
||||
Config = CreateConfigElement(new { mappings = Mappings.ToDictionary(m => m.OldName, m => m.NewName) })
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for a column rename mapping.
|
||||
/// </summary>
|
||||
public class ColumnMappingViewModel : ViewModelBase
|
||||
{
|
||||
private string _oldName;
|
||||
private string _newName;
|
||||
private readonly Action _onChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnMappingViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="oldName">The original column name.</param>
|
||||
/// <param name="newName">The new column name.</param>
|
||||
/// <param name="onChanged">Callback invoked when the mapping changes.</param>
|
||||
public ColumnMappingViewModel(string oldName, string newName, Action onChanged)
|
||||
{
|
||||
_oldName = oldName;
|
||||
_newName = newName;
|
||||
_onChanged = onChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the original column name.
|
||||
/// </summary>
|
||||
public string OldName
|
||||
{
|
||||
get => _oldName;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _oldName, value ?? string.Empty))
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the new column name.
|
||||
/// </summary>
|
||||
public string NewName
|
||||
{
|
||||
get => _newName;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _newName, value ?? string.Empty))
|
||||
_onChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for JdeDate transformer (converts JDE Julian date/time to DateTime).
|
||||
/// </summary>
|
||||
public class JdeDateTransformerViewModel : TransformerStepViewModelBase
|
||||
{
|
||||
private string? _dateColumn;
|
||||
private string? _timeColumn;
|
||||
private string? _outputColumn;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JdeDateTransformerViewModel"/> class from a TransformElement.
|
||||
/// </summary>
|
||||
/// <param name="element">The transform element containing date/time column configuration.</param>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public JdeDateTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
|
||||
{
|
||||
_dateColumn = null;
|
||||
_timeColumn = null;
|
||||
_outputColumn = null;
|
||||
|
||||
if (element.Config.HasValue)
|
||||
{
|
||||
if (element.Config.Value.TryGetProperty("dateColumn", out var dateProp))
|
||||
_dateColumn = dateProp.GetString();
|
||||
if (element.Config.Value.TryGetProperty("timeColumn", out var timeProp))
|
||||
_timeColumn = timeProp.GetString();
|
||||
if (element.Config.Value.TryGetProperty("outputColumn", out var outputProp))
|
||||
_outputColumn = outputProp.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JdeDateTransformerViewModel"/> class with default values.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public JdeDateTransformerViewModel(Action onChanged) : base(onChanged)
|
||||
{
|
||||
_dateColumn = null;
|
||||
_timeColumn = null;
|
||||
_outputColumn = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string TransformerType => "JdeDate";
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "JDE Date Convert";
|
||||
/// <inheritdoc />
|
||||
public override string Icon => ""; // mdi-calendar
|
||||
/// <inheritdoc />
|
||||
public override string Summary => !string.IsNullOrEmpty(_outputColumn) ? $"→ {_outputColumn}" : "Configure...";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date column name.
|
||||
/// </summary>
|
||||
public string? DateColumn
|
||||
{
|
||||
get => _dateColumn;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _dateColumn, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time column name.
|
||||
/// </summary>
|
||||
public string? TimeColumn
|
||||
{
|
||||
get => _timeColumn;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _timeColumn, value))
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output column name.
|
||||
/// </summary>
|
||||
public string? OutputColumn
|
||||
{
|
||||
get => _outputColumn;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _outputColumn, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override TransformElement ToModel() => new()
|
||||
{
|
||||
TransformType = TransformerType,
|
||||
Config = CreateConfigElement(new { dateColumn = _dateColumn, timeColumn = _timeColumn, outputColumn = _outputColumn })
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies behavior when a regex pattern does not match the input value.
|
||||
/// </summary>
|
||||
public enum NonMatchBehavior
|
||||
{
|
||||
/// <summary>Keep the original value unchanged.</summary>
|
||||
KeepOriginal,
|
||||
/// <summary>Return null/DBNull.</summary>
|
||||
ReturnNull,
|
||||
/// <summary>Return an empty string.</summary>
|
||||
ReturnEmpty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for Regex transformer.
|
||||
/// </summary>
|
||||
public class RegexTransformerViewModel : TransformerStepViewModelBase
|
||||
{
|
||||
private string _columnName = string.Empty;
|
||||
private string _pattern = string.Empty;
|
||||
private string? _replacement = string.Empty;
|
||||
private bool _isFindReplaceMode = true;
|
||||
private bool _ignoreCase;
|
||||
private NonMatchBehavior _nonMatchBehavior = NonMatchBehavior.KeepOriginal;
|
||||
|
||||
// Test feature fields
|
||||
private string _testInput = string.Empty;
|
||||
private string _testResultValue = string.Empty;
|
||||
private string _testResultLabel = string.Empty;
|
||||
private string _testResultIcon = string.Empty;
|
||||
private string _testResultBackground = string.Empty;
|
||||
private bool _hasTestResult;
|
||||
private bool _hasTestError;
|
||||
private string _testErrorMessage = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegexTransformerViewModel"/> class from a TransformElement.
|
||||
/// </summary>
|
||||
/// <param name="element">The transform element containing regex configuration.</param>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public RegexTransformerViewModel(TransformElement element, Action onChanged) : base(onChanged)
|
||||
{
|
||||
_columnName = string.Empty;
|
||||
_pattern = string.Empty;
|
||||
_replacement = null;
|
||||
_isFindReplaceMode = true;
|
||||
_ignoreCase = false;
|
||||
_nonMatchBehavior = NonMatchBehavior.KeepOriginal;
|
||||
|
||||
if (element.Config.HasValue)
|
||||
{
|
||||
if (element.Config.Value.TryGetProperty("columnName", out var colProp))
|
||||
_columnName = colProp.GetString() ?? string.Empty;
|
||||
if (element.Config.Value.TryGetProperty("pattern", out var patternProp))
|
||||
_pattern = patternProp.GetString() ?? string.Empty;
|
||||
if (element.Config.Value.TryGetProperty("replacement", out var replaceProp))
|
||||
{
|
||||
_replacement = replaceProp.ValueKind == JsonValueKind.Null ? null : replaceProp.GetString();
|
||||
_isFindReplaceMode = _replacement != null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No replacement property means Match & Extract mode
|
||||
_replacement = null;
|
||||
_isFindReplaceMode = false;
|
||||
}
|
||||
if (element.Config.Value.TryGetProperty("ignoreCase", out var ignoreProp))
|
||||
_ignoreCase = ignoreProp.GetBoolean();
|
||||
if (element.Config.Value.TryGetProperty("nonMatchBehavior", out var behaviorProp))
|
||||
{
|
||||
var behaviorStr = behaviorProp.GetString();
|
||||
if (Enum.TryParse<NonMatchBehavior>(behaviorStr, true, out var behavior))
|
||||
_nonMatchBehavior = behavior;
|
||||
}
|
||||
}
|
||||
TestPatternCommand = new RelayCommand(ExecuteTestPattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegexTransformerViewModel"/> class with default values.
|
||||
/// </summary>
|
||||
/// <param name="onChanged">Callback invoked when the configuration changes.</param>
|
||||
public RegexTransformerViewModel(Action onChanged) : base(onChanged)
|
||||
{
|
||||
TestPatternCommand = new RelayCommand(ExecuteTestPattern);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string TransformerType => "Regex";
|
||||
/// <inheritdoc />
|
||||
public override string DisplayName => "Regex Transform";
|
||||
/// <inheritdoc />
|
||||
public override string Icon => ""; // mdi-regex
|
||||
/// <inheritdoc />
|
||||
public override string Summary => !string.IsNullOrEmpty(_columnName)
|
||||
? $"{_columnName}: {(_isFindReplaceMode ? "Replace" : "Extract")}"
|
||||
: "Configure...";
|
||||
|
||||
/// <summary>Gets or sets the column name to transform.</summary>
|
||||
public string ColumnName
|
||||
{
|
||||
get => _columnName;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _columnName, value ?? string.Empty))
|
||||
{
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the regex pattern.</summary>
|
||||
public string Pattern
|
||||
{
|
||||
get => _pattern;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _pattern, value ?? string.Empty))
|
||||
{
|
||||
ClearTestResult();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the replacement string (Find & Replace mode).</summary>
|
||||
public string? Replacement
|
||||
{
|
||||
get => _replacement;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _replacement, value))
|
||||
{
|
||||
ClearTestResult();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets whether Find & Replace mode is active.</summary>
|
||||
public bool IsFindReplaceMode
|
||||
{
|
||||
get => _isFindReplaceMode;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isFindReplaceMode, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsMatchExtractMode));
|
||||
OnPropertyChanged(nameof(PatternHelpText));
|
||||
OnPropertyChanged(nameof(Summary));
|
||||
ClearTestResult();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets whether Match & Extract mode is active.</summary>
|
||||
public bool IsMatchExtractMode
|
||||
{
|
||||
get => !_isFindReplaceMode;
|
||||
set => IsFindReplaceMode = !value;
|
||||
}
|
||||
|
||||
/// <summary>Gets the help text for the pattern field based on current mode.</summary>
|
||||
public string PatternHelpText => _isFindReplaceMode
|
||||
? "Pattern to search for in the column value"
|
||||
: "Pattern with capture group - first group (parentheses) will be extracted";
|
||||
|
||||
/// <summary>Gets or sets whether matching is case-insensitive.</summary>
|
||||
public bool IgnoreCase
|
||||
{
|
||||
get => _ignoreCase;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _ignoreCase, value))
|
||||
{
|
||||
ClearTestResult();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the behavior when pattern doesn't match.</summary>
|
||||
public NonMatchBehavior NonMatchBehavior
|
||||
{
|
||||
get => _nonMatchBehavior;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _nonMatchBehavior, value))
|
||||
{
|
||||
ClearTestResult();
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the available non-match behavior options for binding.</summary>
|
||||
public static IReadOnlyList<NonMatchBehavior> NonMatchBehaviorOptions { get; } =
|
||||
[NonMatchBehavior.KeepOriginal, NonMatchBehavior.ReturnNull, NonMatchBehavior.ReturnEmpty];
|
||||
|
||||
// Test feature properties
|
||||
/// <summary>
|
||||
/// Gets or sets the test input value for pattern testing.
|
||||
/// </summary>
|
||||
public string TestInput
|
||||
{
|
||||
get => _testInput;
|
||||
set => SetProperty(ref _testInput, value ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the result value from pattern testing.
|
||||
/// </summary>
|
||||
public string TestResultValue
|
||||
{
|
||||
get => _testResultValue;
|
||||
set => SetProperty(ref _testResultValue, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the label describing the test result (e.g., "Output" or "No Match").
|
||||
/// </summary>
|
||||
public string TestResultLabel
|
||||
{
|
||||
get => _testResultLabel;
|
||||
set => SetProperty(ref _testResultLabel, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the icon for the test result.
|
||||
/// </summary>
|
||||
public string TestResultIcon
|
||||
{
|
||||
get => _testResultIcon;
|
||||
set => SetProperty(ref _testResultIcon, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the background color for the test result display.
|
||||
/// </summary>
|
||||
public string TestResultBackground
|
||||
{
|
||||
get => _testResultBackground;
|
||||
set => SetProperty(ref _testResultBackground, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a test result is available.
|
||||
/// </summary>
|
||||
public bool HasTestResult
|
||||
{
|
||||
get => _hasTestResult;
|
||||
set => SetProperty(ref _hasTestResult, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a test error occurred.
|
||||
/// </summary>
|
||||
public bool HasTestError
|
||||
{
|
||||
get => _hasTestError;
|
||||
set => SetProperty(ref _hasTestError, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error message from test execution.
|
||||
/// </summary>
|
||||
public string TestErrorMessage
|
||||
{
|
||||
get => _testErrorMessage;
|
||||
set => SetProperty(ref _testErrorMessage, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to execute pattern testing.
|
||||
/// </summary>
|
||||
public ICommand TestPatternCommand { get; }
|
||||
|
||||
private void ExecuteTestPattern()
|
||||
{
|
||||
ClearTestResult();
|
||||
|
||||
if (string.IsNullOrEmpty(_pattern))
|
||||
{
|
||||
HasTestError = true;
|
||||
TestErrorMessage = "Pattern is required";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = _ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
|
||||
var regex = new Regex(_pattern, options);
|
||||
|
||||
string result;
|
||||
bool matched;
|
||||
|
||||
if (_isFindReplaceMode)
|
||||
{
|
||||
result = regex.Replace(_testInput, _replacement ?? string.Empty);
|
||||
matched = regex.IsMatch(_testInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
var match = regex.Match(_testInput);
|
||||
if (match.Success && match.Groups.Count > 1)
|
||||
{
|
||||
result = match.Groups[1].Value;
|
||||
matched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
matched = false;
|
||||
result = _nonMatchBehavior switch
|
||||
{
|
||||
NonMatchBehavior.ReturnNull => "(null)",
|
||||
NonMatchBehavior.ReturnEmpty => "(empty)",
|
||||
_ => _testInput
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
HasTestResult = true;
|
||||
TestResultValue = result;
|
||||
TestResultLabel = matched ? "Output" : "No Match";
|
||||
TestResultIcon = matched ? "✓" : "—";
|
||||
TestResultBackground = matched ? "#22C55E" : "#F59E0B";
|
||||
}
|
||||
catch (RegexParseException ex)
|
||||
{
|
||||
HasTestError = true;
|
||||
TestErrorMessage = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTestResult()
|
||||
{
|
||||
HasTestResult = false;
|
||||
HasTestError = false;
|
||||
TestResultValue = string.Empty;
|
||||
TestResultLabel = string.Empty;
|
||||
TestErrorMessage = string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override TransformElement ToModel() => new()
|
||||
{
|
||||
TransformType = TransformerType,
|
||||
Config = CreateConfigElement(new
|
||||
{
|
||||
columnName = _columnName,
|
||||
pattern = _pattern,
|
||||
replacement = _isFindReplaceMode ? _replacement : null,
|
||||
ignoreCase = _ignoreCase,
|
||||
nonMatchBehavior = _nonMatchBehavior.ToString()
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory methods for creating transformer view models.
|
||||
/// </summary>
|
||||
public static class TransformerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a transformer view model from a TransformElement.
|
||||
/// </summary>
|
||||
/// <param name="element">The transform element containing configuration.</param>
|
||||
/// <param name="onChanged">Callback invoked when the view model changes.</param>
|
||||
/// <returns>A transformer view model of the appropriate type, or null if the transformer type is unknown.</returns>
|
||||
public static TransformerStepViewModelBase? Create(TransformElement element, Action onChanged)
|
||||
{
|
||||
return element.TransformType?.ToLowerInvariant() switch
|
||||
{
|
||||
"columndrop" => new ColumnDropTransformerViewModel(element, onChanged),
|
||||
"columnrename" => new ColumnRenameTransformerViewModel(element, onChanged),
|
||||
"jdedate" => new JdeDateTransformerViewModel(element, onChanged),
|
||||
"regex" => new RegexTransformerViewModel(element, onChanged),
|
||||
_ => null // Unknown transformer type
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new transformer view model by type name.
|
||||
/// </summary>
|
||||
/// <param name="typeName">The type name of the transformer to create.</param>
|
||||
/// <param name="onChanged">Callback invoked when the view model changes.</param>
|
||||
/// <returns>A new transformer view model of the specified type, or null if the type name is unknown.</returns>
|
||||
public static TransformerStepViewModelBase? CreateNew(string typeName, Action onChanged)
|
||||
{
|
||||
return typeName?.ToLowerInvariant() switch
|
||||
{
|
||||
"columndrop" => new ColumnDropTransformerViewModel(onChanged),
|
||||
"columnrename" => new ColumnRenameTransformerViewModel(onChanged),
|
||||
"jdedate" => new JdeDateTransformerViewModel(onChanged),
|
||||
"regex" => new RegexTransformerViewModel(onChanged),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available transformer type names.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> AvailableTypes => ["ColumnDrop", "ColumnRename", "JdeDate", "Regex"];
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// A command implementation that delegates to action methods.
|
||||
/// </summary>
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the result of <see cref="CanExecute"/> has changed.
|
||||
/// </summary>
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
remove => _canExecuteChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RelayCommand"/> class with a parameterized action.
|
||||
/// </summary>
|
||||
/// <param name="execute">The action to execute when the command is invoked.</param>
|
||||
/// <param name="canExecute">An optional predicate to determine if the command can execute.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="execute"/> is null.</exception>
|
||||
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RelayCommand"/> class with a parameterless action.
|
||||
/// </summary>
|
||||
/// <param name="execute">The parameterless action to execute when the command is invoked.</param>
|
||||
/// <param name="canExecute">An optional predicate to determine if the command can execute.</param>
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
: this(_ => execute(), canExecute != null ? _ => canExecute() : null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the command can execute in its current state.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to pass to the canExecute predicate, or null.</param>
|
||||
/// <returns>True if the command can execute; otherwise, false.</returns>
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command with the specified parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to pass to the execute action.</param>
|
||||
public void Execute(object? parameter) => _execute(parameter);
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="CanExecuteChanged"/> event to notify command bindings of state changes.
|
||||
/// </summary>
|
||||
public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed command implementation that delegates to action methods.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the command parameter.</typeparam>
|
||||
public class RelayCommand<T> : ICommand
|
||||
{
|
||||
private readonly Action<T?> _execute;
|
||||
private readonly Predicate<T?>? _canExecute;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the result of <see cref="CanExecute"/> has changed.
|
||||
/// </summary>
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
remove => _canExecuteChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RelayCommand{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="execute">The action to execute when the command is invoked.</param>
|
||||
/// <param name="canExecute">An optional predicate to determine if the command can execute.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="execute"/> is null.</exception>
|
||||
public RelayCommand(Action<T?> execute, Predicate<T?>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the command can execute in its current state.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to pass to the canExecute predicate.</param>
|
||||
/// <returns>True if the command can execute; otherwise, false.</returns>
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
if (parameter is T typedParam)
|
||||
return _canExecute?.Invoke(typedParam) ?? true;
|
||||
if (parameter is null && !typeof(T).IsValueType)
|
||||
return _canExecute?.Invoke(default) ?? true;
|
||||
return _canExecute?.Invoke(default) ?? true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command with the specified parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to pass to the execute action.</param>
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
if (parameter is T typedParam)
|
||||
_execute(typedParam);
|
||||
else if (parameter is null && !typeof(T).IsValueType)
|
||||
_execute(default);
|
||||
else
|
||||
_execute(default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="CanExecuteChanged"/> event to notify command bindings of state changes.
|
||||
/// </summary>
|
||||
public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels;
|
||||
|
||||
public enum TreeNodeType
|
||||
{
|
||||
Folder,
|
||||
SettingsSection,
|
||||
Pipeline,
|
||||
SecureStore, // The single secure store node
|
||||
Secret // Individual secrets within a store
|
||||
}
|
||||
|
||||
public enum ValidationState
|
||||
{
|
||||
Valid,
|
||||
Warning,
|
||||
Error,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for a tree node in the configuration tree.
|
||||
/// </summary>
|
||||
public class TreeNodeViewModel : ViewModelBase
|
||||
{
|
||||
private bool _isModified;
|
||||
private bool _isExpanded;
|
||||
private bool _isSelected;
|
||||
private ValidationState _validationState = ValidationState.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the tree node.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon identifier for the tree node.
|
||||
/// </summary>
|
||||
public string Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of tree node (folder, settings section, or pipeline).
|
||||
/// </summary>
|
||||
public TreeNodeType NodeType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration section key associated with this node.
|
||||
/// </summary>
|
||||
public string? SectionKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full path to the secure store file.
|
||||
/// Only applicable for SecureStore node types.
|
||||
/// </summary>
|
||||
public string? StorePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full path to the key file for the secure store.
|
||||
/// Only applicable for SecureStore node types.
|
||||
/// </summary>
|
||||
public string? KeyFilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key name.
|
||||
/// Only applicable for Secret node types.
|
||||
/// </summary>
|
||||
public string? SecretKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of child nodes.
|
||||
/// </summary>
|
||||
public ObservableCollection<TreeNodeViewModel> Children { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this node has been modified.
|
||||
/// </summary>
|
||||
public bool IsModified
|
||||
{
|
||||
get => _isModified;
|
||||
set => SetProperty(ref _isModified, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this node is expanded in the tree view.
|
||||
/// </summary>
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => SetProperty(ref _isExpanded, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this node is selected in the tree view.
|
||||
/// </summary>
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the validation state of this node.
|
||||
/// </summary>
|
||||
public ValidationState ValidationState
|
||||
{
|
||||
get => _validationState;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _validationState, value))
|
||||
OnPropertyChanged(nameof(StatusIcon));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon character representing the validation state.
|
||||
/// </summary>
|
||||
public string StatusIcon => ValidationState switch
|
||||
{
|
||||
ValidationState.Valid => "✓",
|
||||
ValidationState.Warning => "⚠",
|
||||
ValidationState.Error => "✗",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TreeNodeViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the node.</param>
|
||||
/// <param name="icon">The icon identifier for the node.</param>
|
||||
/// <param name="nodeType">The type of the node.</param>
|
||||
public TreeNodeViewModel(string name, string icon, TreeNodeType nodeType)
|
||||
{
|
||||
Name = name;
|
||||
Icon = icon;
|
||||
NodeType = nodeType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all view models providing INotifyPropertyChanged implementation.
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when a property value changes.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the PropertyChanged event with the specified property name.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property that changed. Automatically captured from the calling member.</param>
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a field value and raises PropertyChanged if the value actually changed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property value.</typeparam>
|
||||
/// <param name="field">The backing field to update by reference.</param>
|
||||
/// <param name="value">The new value to assign.</param>
|
||||
/// <param name="propertyName">The name of the property being changed. Automatically captured from the calling member.</param>
|
||||
/// <returns>True if the value was changed; otherwise, false.</returns>
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Controls.FlowArrow"
|
||||
Width="200"
|
||||
Height="24">
|
||||
|
||||
<Canvas HorizontalAlignment="Center" Width="20" Height="24">
|
||||
<!-- Vertical line -->
|
||||
<Line StartPoint="10,0" EndPoint="10,16"
|
||||
Stroke="#3D4550" StrokeThickness="2"/>
|
||||
|
||||
<!-- Arrow head (pointing down) -->
|
||||
<Polygon Points="10,24 4,16 16,16"
|
||||
Fill="#3D4550"/>
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Controls;
|
||||
|
||||
public partial class FlowArrow : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FlowArrow"/> class.
|
||||
/// </summary>
|
||||
public FlowArrow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Controls.PipelineStepCard"
|
||||
x:DataType="steps:PipelineStepViewModelBase"
|
||||
Width="200">
|
||||
|
||||
<UserControl.Styles>
|
||||
<!-- Selected state style -->
|
||||
<Style Selector="Border.card">
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
<Setter Property="BorderBrush" Value="#3D4550"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
</Style>
|
||||
<Style Selector="Border.card:pointerover">
|
||||
<Setter Property="BorderBrush" Value="#5C6A7A"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Border Classes="card"
|
||||
Background="#1A1F26"
|
||||
CornerRadius="8"
|
||||
Padding="12"
|
||||
MinHeight="60"
|
||||
Name="CardBorder">
|
||||
<Grid ColumnDefinitions="Auto,12,*">
|
||||
<!-- Icon with colored background -->
|
||||
<Border Grid.Column="0"
|
||||
Width="36" Height="36"
|
||||
CornerRadius="6"
|
||||
Name="IconBackground"
|
||||
Background="#3B82F6">
|
||||
<TextBlock Text="{Binding Icon}"
|
||||
FontSize="18"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="White"/>
|
||||
</Border>
|
||||
|
||||
<!-- Step info -->
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13"/>
|
||||
<TextBlock Text="{Binding Summary}"
|
||||
Foreground="#5C6A7A"
|
||||
FontSize="11"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,109 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Controls;
|
||||
|
||||
public partial class PipelineStepCard : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<string> StepColorProperty =
|
||||
AvaloniaProperty.Register<PipelineStepCard, string>(nameof(StepColor), "#3B82F6");
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipelineStepCard"/> class.
|
||||
/// </summary>
|
||||
public PipelineStepCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
PointerPressed += OnPointerPressed;
|
||||
PropertyChanged += OnPropertyChangedHandler;
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the step color for the icon background.
|
||||
/// </summary>
|
||||
public string StepColor
|
||||
{
|
||||
get => GetValue(StepColorProperty);
|
||||
set => SetValue(StepColorProperty, value);
|
||||
}
|
||||
|
||||
private void OnPropertyChangedHandler(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property == StepColorProperty)
|
||||
{
|
||||
UpdateIconColor();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateIconColor();
|
||||
UpdateSelectionState();
|
||||
|
||||
if (DataContext is PipelineStepViewModelBase vm)
|
||||
{
|
||||
vm.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(PipelineStepViewModelBase.IsSelected))
|
||||
{
|
||||
UpdateSelectionState();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateIconColor()
|
||||
{
|
||||
var iconBg = this.FindControl<Border>("IconBackground");
|
||||
if (iconBg != null && !string.IsNullOrEmpty(StepColor))
|
||||
{
|
||||
if (Color.TryParse(StepColor, out var color))
|
||||
{
|
||||
iconBg.Background = new SolidColorBrush(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelectionState()
|
||||
{
|
||||
var cardBorder = this.FindControl<Border>("CardBorder");
|
||||
if (cardBorder != null && DataContext is PipelineStepViewModelBase vm)
|
||||
{
|
||||
if (vm.IsSelected)
|
||||
{
|
||||
cardBorder.BorderBrush = new SolidColorBrush(Color.Parse("#3B82F6"));
|
||||
cardBorder.Background = new SolidColorBrush(Color.Parse("#1E2A3A"));
|
||||
}
|
||||
else
|
||||
{
|
||||
cardBorder.BorderBrush = new SolidColorBrush(Color.Parse("#3D4550"));
|
||||
cardBorder.Background = new SolidColorBrush(Color.Parse("#1A1F26"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
// Find the PipelineEditorViewModel from the visual tree
|
||||
var parent = this.Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is UserControl uc && uc.DataContext is PipelineEditorViewModel editorVm)
|
||||
{
|
||||
if (DataContext is PipelineStepViewModelBase stepVm)
|
||||
{
|
||||
editorVm.SelectedStep = stepVm;
|
||||
}
|
||||
break;
|
||||
}
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Dialogs"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Dialogs.DiffPreviewDialog"
|
||||
x:DataType="vm:DiffPreviewDialogViewModel"
|
||||
Title="Preview Changes"
|
||||
Width="800" Height="600"
|
||||
MinWidth="600" MinHeight="400"
|
||||
Background="#151920"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Header -->
|
||||
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="Preview Changes"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<TextBlock Foreground="#9BA8B8" FontFamily="JetBrains Mono" FontSize="12">
|
||||
<Run Text="{Binding Insertions}"/> insertions, <Run Text="{Binding Deletions}"/> deletions
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Content="Cancel" Command="{Binding CancelCommand}"
|
||||
Background="Transparent" BorderBrush="#3D4550"
|
||||
Foreground="#9BA8B8" Padding="16,8"/>
|
||||
<Button Content="Save Changes" Command="{Binding SaveCommand}"
|
||||
Background="#5C9AFF" Foreground="#0D0F12"
|
||||
Padding="16,8" FontWeight="Medium"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Diff Content -->
|
||||
<ScrollViewer Background="#0D0F12" Padding="0">
|
||||
<ItemsControl ItemsSource="{Binding Lines}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding Background}"
|
||||
BorderBrush="{Binding BorderColor}"
|
||||
BorderThickness="3,0,0,0"
|
||||
Padding="0,2">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50"/>
|
||||
<ColumnDefinition Width="50"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="{Binding OldLineNumber}"
|
||||
Foreground="#5C6A7A" FontFamily="JetBrains Mono" FontSize="12"
|
||||
HorizontalAlignment="Right" Padding="0,0,8,0"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding NewLineNumber}"
|
||||
Foreground="#5C6A7A" FontFamily="JetBrains Mono" FontSize="12"
|
||||
HorizontalAlignment="Right" Padding="0,0,8,0"/>
|
||||
<Border Grid.Column="1" Width="1" Background="#2D3540" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Text}"
|
||||
Foreground="#E6EDF5" FontFamily="JetBrains Mono" FontSize="12"
|
||||
Padding="12,0,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Avalonia.Controls;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
|
||||
public partial class DiffPreviewDialog : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DiffPreviewDialog"/> class.
|
||||
/// </summary>
|
||||
public DiffPreviewDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DiffPreviewDialog"/> class with a view model.
|
||||
/// </summary>
|
||||
/// <param name="viewModel">The view model to bind to the dialog.</param>
|
||||
public DiffPreviewDialog(DiffPreviewDialogViewModel viewModel) : this()
|
||||
{
|
||||
DataContext = viewModel;
|
||||
viewModel.RequestClose = Close;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Dialogs.InputDialog"
|
||||
Title="Input"
|
||||
Width="450" Height="200"
|
||||
MinWidth="350" MinHeight="180"
|
||||
Background="#151920"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Header -->
|
||||
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
|
||||
<TextBlock x:Name="TitleText"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Content="Cancel" Click="CancelButton_Click"
|
||||
Background="Transparent" BorderBrush="#3D4550"
|
||||
Foreground="#9BA8B8" Padding="16,8" MinWidth="80"/>
|
||||
<Button Content="OK" Click="OkButton_Click"
|
||||
Background="#5C9AFF" Foreground="#0D0F12"
|
||||
Padding="16,8" FontWeight="Medium" MinWidth="80"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Content -->
|
||||
<Grid Background="#151920" Margin="24,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Prompt -->
|
||||
<TextBlock Grid.Row="0" x:Name="PromptText"
|
||||
Foreground="#9BA8B8" FontSize="14"
|
||||
Margin="0,0,0,12" TextWrapping="Wrap"/>
|
||||
|
||||
<!-- Input -->
|
||||
<TextBox Grid.Row="1" x:Name="InputTextBox"
|
||||
Background="#0D0F12" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Padding="12"
|
||||
VerticalContentAlignment="Center"
|
||||
FontFamily="JetBrains Mono" FontSize="14"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,59 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// A simple input dialog for collecting text from the user.
|
||||
/// </summary>
|
||||
public partial class InputDialog : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the text entered by the user.
|
||||
/// </summary>
|
||||
public string? InputText => InputTextBox.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Design-time constructor for XAML previewer.
|
||||
/// </summary>
|
||||
public InputDialog() : this("Input", "Enter value:")
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputDialog"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
/// <param name="prompt">The prompt message to display.</param>
|
||||
/// <param name="defaultValue">Optional default value for the input field.</param>
|
||||
public InputDialog(string title, string prompt, string? defaultValue = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
TitleText.Text = title;
|
||||
Title = title;
|
||||
PromptText.Text = prompt;
|
||||
|
||||
if (!string.IsNullOrEmpty(defaultValue))
|
||||
{
|
||||
InputTextBox.Text = defaultValue;
|
||||
}
|
||||
|
||||
// Focus the input when loaded
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
InputTextBox.Focus();
|
||||
InputTextBox.SelectAll();
|
||||
};
|
||||
}
|
||||
|
||||
private void OkButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Dialogs"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Dialogs.NewStoreDialog"
|
||||
x:DataType="vm:NewStoreDialogViewModel"
|
||||
Title="Create New Secure Store"
|
||||
Width="550" Height="350"
|
||||
MinWidth="450" MinHeight="300"
|
||||
Background="#151920"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Header -->
|
||||
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="Create New Secure Store"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Content="Cancel" Click="CancelButton_Click"
|
||||
Background="Transparent" BorderBrush="#3D4550"
|
||||
Foreground="#9BA8B8" Padding="16,8" MinWidth="80"/>
|
||||
<Button Content="Create" Click="CreateButton_Click"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Background="#5C9AFF" Foreground="#0D0F12"
|
||||
Padding="16,8" FontWeight="Medium" MinWidth="80"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Content -->
|
||||
<ScrollViewer Background="#151920" Padding="24,16">
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Store Path -->
|
||||
<Border Background="#1C2128" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="16">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Store Location" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding StorePath, Mode=TwoWay}"
|
||||
Watermark="Path to new secure store file..."
|
||||
Background="#0D0F12" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Padding="8"/>
|
||||
<Button Grid.Column="1" Content="Browse..."
|
||||
Command="{Binding BrowseStorePathCommand}"
|
||||
Background="#2D3540" Foreground="#E6EDF5"
|
||||
Margin="8,0,0,0" Padding="12,8"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Key File -->
|
||||
<Border Background="#1C2128" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="16">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Key File" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
<TextBlock Text="The key file is required to encrypt and decrypt the store."
|
||||
Foreground="#9BA8B8" FontSize="12" TextWrapping="Wrap"/>
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,4,0,0">
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding KeyFilePath, Mode=TwoWay}"
|
||||
Watermark="Path to key file..."
|
||||
Background="#0D0F12" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Padding="8"/>
|
||||
<Button Grid.Column="1" Content="Browse..."
|
||||
Command="{Binding BrowseKeyFilePathCommand}"
|
||||
Background="#2D3540" Foreground="#E6EDF5"
|
||||
Margin="8,0,0,0" Padding="12,8"/>
|
||||
<Button Grid.Column="2" Content="Generate"
|
||||
Command="{Binding GenerateKeyFileCommand}"
|
||||
Background="#3D8C40" Foreground="#E6EDF5"
|
||||
Margin="8,0,0,0" Padding="12,8"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Validation Error -->
|
||||
<TextBlock Text="{Binding ValidationError}"
|
||||
Foreground="#FF6B6B" FontSize="12"
|
||||
IsVisible="{Binding ValidationError, Converter={StaticResource StringToBool}}"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,129 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using JdeScoping.ConfigManager.Core.Constants;
|
||||
using JdeScoping.ConfigManager.Core.Services.SecureStore;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Dialog for creating a new secure store.
|
||||
/// </summary>
|
||||
public partial class NewStoreDialog : Window
|
||||
{
|
||||
private readonly ISecureStoreManager _secureStoreManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the view model for this dialog.
|
||||
/// </summary>
|
||||
public NewStoreDialogViewModel ViewModel => (NewStoreDialogViewModel)DataContext!;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NewStoreDialog"/> class.
|
||||
/// </summary>
|
||||
public NewStoreDialog() : this(new SecureStoreManager())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NewStoreDialog"/> class with the specified secure store manager.
|
||||
/// </summary>
|
||||
/// <param name="secureStoreManager">The secure store manager for key generation.</param>
|
||||
public NewStoreDialog(ISecureStoreManager secureStoreManager)
|
||||
{
|
||||
_secureStoreManager = secureStoreManager ?? throw new ArgumentNullException(nameof(secureStoreManager));
|
||||
InitializeComponent();
|
||||
DataContext = new NewStoreDialogViewModel();
|
||||
Loaded += NewStoreDialog_Loaded;
|
||||
}
|
||||
|
||||
private void NewStoreDialog_Loaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.OnShowSaveFileDialog += ShowSaveFileDialogAsync;
|
||||
ViewModel.OnGenerateKeyFile += GenerateKeyFileAsync;
|
||||
}
|
||||
|
||||
private async Task<string?> ShowSaveFileDialogAsync(string title, string fileTypeName, string pattern, string defaultExtension)
|
||||
{
|
||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = title,
|
||||
DefaultExtension = defaultExtension,
|
||||
FileTypeChoices = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType(SecureStoreFileExtensions.AllFilesTypeName) { Patterns = new[] { SecureStoreFileExtensions.AllFilesPattern } }
|
||||
}
|
||||
});
|
||||
|
||||
return file?.Path.LocalPath;
|
||||
}
|
||||
|
||||
private async Task<string?> GenerateKeyFileAsync(string title, string fileTypeName, string pattern, string defaultExtension)
|
||||
{
|
||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = title,
|
||||
DefaultExtension = defaultExtension,
|
||||
FileTypeChoices = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType(SecureStoreFileExtensions.AllFilesTypeName) { Patterns = new[] { SecureStoreFileExtensions.AllFilesPattern } }
|
||||
}
|
||||
});
|
||||
|
||||
if (file == null)
|
||||
return null;
|
||||
|
||||
var path = file.Path.LocalPath;
|
||||
|
||||
try
|
||||
{
|
||||
// Generate a new key file using the local SecureStoreManager
|
||||
_secureStoreManager.GenerateKeyFile(path);
|
||||
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(
|
||||
SecureStoreStrings.KeyGeneratedTitle,
|
||||
string.Format(SecureStoreStrings.KeyFileGeneratedFormat, path),
|
||||
ButtonEnum.Ok,
|
||||
MsBox.Avalonia.Enums.Icon.Info);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
|
||||
return path;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(
|
||||
SecureStoreStrings.ErrorTitle,
|
||||
string.Format(SecureStoreStrings.FailedToGenerateKeyFormat, ex.Message),
|
||||
ButtonEnum.Ok,
|
||||
MsBox.Avalonia.Enums.Icon.Error);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void CreateButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ViewModel.IsValid)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(
|
||||
SecureStoreStrings.ValidationErrorTitle,
|
||||
ViewModel.ValidationError ?? SecureStoreStrings.DefaultValidationError,
|
||||
ButtonEnum.Ok,
|
||||
MsBox.Avalonia.Enums.Icon.Warning);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Dialogs"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Dialogs.SecretEditDialog"
|
||||
x:DataType="vm:SecretEditDialogViewModel"
|
||||
Title="{Binding DialogTitle}"
|
||||
Width="500" Height="320"
|
||||
MinWidth="400" MinHeight="280"
|
||||
Background="#151920"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Header -->
|
||||
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="{Binding DialogTitle}"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Content="Cancel" Click="CancelButton_Click"
|
||||
Background="Transparent" BorderBrush="#3D4550"
|
||||
Foreground="#9BA8B8" Padding="16,8" MinWidth="80"/>
|
||||
<Button Content="Save" Click="SaveButton_Click"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Background="#5C9AFF" Foreground="#0D0F12"
|
||||
Padding="16,8" FontWeight="Medium" MinWidth="80"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Content -->
|
||||
<Grid Background="#151920" Margin="24,16" RowDefinitions="Auto,*,Auto">
|
||||
<!-- Key -->
|
||||
<Border Grid.Row="0" Background="#1C2128" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="16" Margin="0,0,0,16">
|
||||
<Grid ColumnDefinitions="80,*">
|
||||
<TextBlock Text="Key:" Foreground="#9BA8B8"
|
||||
VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding Key, Mode=TwoWay}"
|
||||
IsEnabled="{Binding IsKeyEditable}"
|
||||
Watermark="Enter secret key..."
|
||||
Background="#0D0F12" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Padding="8"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Value -->
|
||||
<Border Grid.Row="1" Background="#1C2128" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="16">
|
||||
<Grid ColumnDefinitions="80,*">
|
||||
<TextBlock Text="Value:" Foreground="#9BA8B8"
|
||||
VerticalAlignment="Top" Margin="0,8,16,0"/>
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding Value, Mode=TwoWay}"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
Watermark="Enter secret value..."
|
||||
Background="#0D0F12" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Padding="8"
|
||||
MinHeight="80"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Validation Error -->
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding ValidationError}"
|
||||
Foreground="#FF6B6B" FontSize="12"
|
||||
IsVisible="{Binding ValidationError, Converter={StaticResource StringToBool}}"
|
||||
Margin="0,8,0,0"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,60 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using JdeScoping.ConfigManager.Core.Constants;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Dialog for adding or editing a secret in the secure store.
|
||||
/// </summary>
|
||||
public partial class SecretEditDialog : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the view model for this dialog.
|
||||
/// </summary>
|
||||
public SecretEditDialogViewModel ViewModel => (SecretEditDialogViewModel)DataContext!;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialog"/> class for creating a new secret.
|
||||
/// </summary>
|
||||
public SecretEditDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new SecretEditDialogViewModel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialog"/> class for editing an existing secret.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <param name="value">The secret value.</param>
|
||||
public SecretEditDialog(string key, string value)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new SecretEditDialogViewModel(key, value);
|
||||
}
|
||||
|
||||
private async void SaveButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ViewModel.IsValid)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(
|
||||
SecureStoreStrings.ValidationErrorTitle,
|
||||
ViewModel.ValidationError ?? SecureStoreStrings.DefaultValidationError,
|
||||
ButtonEnum.Ok,
|
||||
MsBox.Avalonia.Enums.Icon.Warning);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Dialogs"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Dialogs.UnlockStoreDialog"
|
||||
x:DataType="vm:UnlockStoreDialogViewModel"
|
||||
Title="Unlock Secure Store"
|
||||
Width="500" Height="320"
|
||||
MinWidth="400" MinHeight="280"
|
||||
Background="#151920"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Header -->
|
||||
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="Unlock Secure Store"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Content="Cancel" Click="CancelButton_Click"
|
||||
Background="Transparent" BorderBrush="#3D4550"
|
||||
Foreground="#9BA8B8" Padding="16,8" MinWidth="80"/>
|
||||
<Button Content="Unlock" Click="UnlockButton_Click"
|
||||
IsEnabled="{Binding IsValid}"
|
||||
Background="#5C9AFF" Foreground="#0D0F12"
|
||||
Padding="16,8" FontWeight="Medium" MinWidth="80"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Content -->
|
||||
<ScrollViewer Background="#151920" Padding="24,16">
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Store Path (Read-Only) -->
|
||||
<Border Background="#1C2128" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="16">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Store File" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
<TextBox Text="{Binding StorePath}"
|
||||
IsReadOnly="True"
|
||||
Background="#0D0F12" Foreground="#9BA8B8"
|
||||
BorderBrush="#3D4550" Padding="8" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Key File -->
|
||||
<Border Background="#1C2128" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="16">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Key File" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
<TextBlock Text="Select the key file used to encrypt this store."
|
||||
Foreground="#9BA8B8" FontSize="12" TextWrapping="Wrap"/>
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding KeyFilePath, Mode=TwoWay}"
|
||||
Watermark="Path to key file..."
|
||||
Background="#0D0F12" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Padding="8"/>
|
||||
<Button Grid.Column="1" Content="Browse..."
|
||||
Command="{Binding BrowseKeyFilePathCommand}"
|
||||
Background="#2D3540" Foreground="#E6EDF5"
|
||||
Margin="8,0,0,0" Padding="12,8"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Validation Error -->
|
||||
<TextBlock Text="{Binding ValidationError}"
|
||||
Foreground="#FF6B6B" FontSize="12"
|
||||
IsVisible="{Binding ValidationError, Converter={StaticResource StringToBool}}"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,80 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using JdeScoping.ConfigManager.Core.Constants;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
using MsBox.Avalonia;
|
||||
using MsBox.Avalonia.Enums;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Dialog for unlocking an existing secure store.
|
||||
/// </summary>
|
||||
public partial class UnlockStoreDialog : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the view model for this dialog.
|
||||
/// </summary>
|
||||
public UnlockStoreDialogViewModel ViewModel => (UnlockStoreDialogViewModel)DataContext!;
|
||||
|
||||
/// <summary>
|
||||
/// Design-time constructor. Required for XAML previewer.
|
||||
/// </summary>
|
||||
public UnlockStoreDialog() : this(string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnlockStoreDialog"/> class.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path to the store file to unlock.</param>
|
||||
public UnlockStoreDialog(string storePath)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new UnlockStoreDialogViewModel(storePath);
|
||||
Loaded += UnlockStoreDialog_Loaded;
|
||||
}
|
||||
|
||||
private void UnlockStoreDialog_Loaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.OnShowOpenFileDialog += ShowOpenFileDialogAsync;
|
||||
}
|
||||
|
||||
private async Task<string?> ShowOpenFileDialogAsync(string title, string fileTypeName, string pattern)
|
||||
{
|
||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = title,
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new[]
|
||||
{
|
||||
new FilePickerFileType(fileTypeName) { Patterns = new[] { pattern } },
|
||||
new FilePickerFileType(SecureStoreFileExtensions.AllFilesTypeName) { Patterns = new[] { SecureStoreFileExtensions.AllFilesPattern } }
|
||||
}
|
||||
});
|
||||
|
||||
return files.Count > 0 ? files[0].Path.LocalPath : null;
|
||||
}
|
||||
|
||||
private async void UnlockButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ViewModel.IsValid)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(
|
||||
SecureStoreStrings.ValidationErrorTitle,
|
||||
ViewModel.ValidationError ?? SecureStoreStrings.DefaultValidationError,
|
||||
ButtonEnum.Ok,
|
||||
MsBox.Avalonia.Enums.Icon.Warning);
|
||||
await box.ShowWindowDialogAsync(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Dialogs"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Dialogs.ValidationResultsDialog"
|
||||
x:DataType="vm:ValidationResultsDialogViewModel"
|
||||
Title="Validation Results"
|
||||
Width="600" Height="500"
|
||||
MinWidth="400" MinHeight="300"
|
||||
Background="#151920"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<DockPanel>
|
||||
<!-- Header -->
|
||||
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Validation Results"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0" Spacing="16">
|
||||
<TextBlock Foreground="#FF6B6B" FontSize="12">
|
||||
<Run Text="{Binding ErrorCount}"/> errors
|
||||
</TextBlock>
|
||||
<TextBlock Foreground="#FFB84D" FontSize="12">
|
||||
<Run Text="{Binding WarningCount}"/> warnings
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Footer -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
|
||||
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
|
||||
<Button Content="Close" Command="{Binding CloseCommand}"
|
||||
HorizontalAlignment="Right"
|
||||
Background="#5C9AFF" Foreground="#0D0F12"
|
||||
Padding="16,8" FontWeight="Medium"/>
|
||||
</Border>
|
||||
|
||||
<!-- Content -->
|
||||
<ScrollViewer Background="#0D0F12" Padding="16">
|
||||
<ItemsControl ItemsSource="{Binding Items}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding Background}"
|
||||
BorderBrush="{Binding BorderColor}"
|
||||
BorderThickness="3,0,0,0"
|
||||
Margin="0,0,0,8"
|
||||
Padding="12">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Icon}"
|
||||
Foreground="{Binding IconColor}"
|
||||
FontSize="14"/>
|
||||
<TextBlock Text="{Binding Source}"
|
||||
Foreground="#5C6A7A"
|
||||
FontFamily="JetBrains Mono" FontSize="11"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Message}"
|
||||
Foreground="#E6EDF5"
|
||||
TextWrapping="Wrap"
|
||||
Margin="22,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using Avalonia.Controls;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.Dialogs;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Dialog window for displaying validation results.
|
||||
/// </summary>
|
||||
public partial class ValidationResultsDialog : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValidationResultsDialog"/> class.
|
||||
/// </summary>
|
||||
public ValidationResultsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValidationResultsDialog"/> class with a view model.
|
||||
/// </summary>
|
||||
/// <param name="viewModel">The view model containing validation results.</param>
|
||||
public ValidationResultsDialog(ValidationResultsDialogViewModel viewModel) : this()
|
||||
{
|
||||
DataContext = viewModel;
|
||||
viewModel.RequestClose = Close;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.ColumnDropEditorView"
|
||||
x:DataType="steps:ColumnDropTransformerViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Column Drop Transformer"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Remove columns from the data stream"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Columns to Drop -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Columns to Drop (one per line)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding ColumnsText}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="150"
|
||||
Watermark="TempColumn1
TempColumn2
..."/>
|
||||
<TextBlock Text="Enter column names to remove from the data, one per line"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class ColumnDropEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnDropEditorView"/> class.
|
||||
/// </summary>
|
||||
public ColumnDropEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.ColumnRenameEditorView"
|
||||
x:DataType="steps:ColumnRenameTransformerViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Column Rename Transformer"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Rename columns in the data stream"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Mappings List -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Column Mappings"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Mappings}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="steps:ColumnMappingViewModel">
|
||||
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="8" Margin="0,0,0,4">
|
||||
<Grid ColumnDefinitions="*,Auto,*,8,Auto">
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding OldName}"
|
||||
Background="#232A35" Height="32" FontSize="11"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="Old Name"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="→" Foreground="#5C6A7A"
|
||||
FontSize="14" Margin="8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Column="2"
|
||||
Text="{Binding NewName}"
|
||||
Background="#232A35" Height="32" FontSize="11"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="New Name"/>
|
||||
<Button Grid.Column="4" Content="X"
|
||||
Background="Transparent" Foreground="#FF6B6B"
|
||||
BorderThickness="0" FontSize="11" Width="28"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Button Content="+ Add Mapping"
|
||||
Background="#232A35" Foreground="#9BA8B8"
|
||||
BorderBrush="#3D4550" Height="32"
|
||||
HorizontalAlignment="Left" Padding="12,0"
|
||||
Command="{Binding AddMappingCommand}"/>
|
||||
|
||||
<TextBlock Text="Map original column names to new names"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class ColumnRenameEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColumnRenameEditorView"/> class.
|
||||
/// </summary>
|
||||
public ColumnRenameEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.DestinationEditorView"
|
||||
x:DataType="steps:DestinationStepViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Destination Configuration"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Configure how data is loaded into the destination table"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Destination Table -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Destination Table"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding Table}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="dbo.WorkOrder_Curr"/>
|
||||
<TextBlock Text="Target table in SQL Server (include schema prefix)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Match Columns -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Match Columns (one per line)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding MatchColumnsText}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="80"
|
||||
Watermark="OrderNumber
OrderType"/>
|
||||
<TextBlock Text="Columns to match source rows with existing destination rows (for upsert)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Exclude From Update -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Exclude From Update (one per line)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding ExcludeFromUpdateText}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="60"
|
||||
Watermark="CreatedDate
CreatedBy"/>
|
||||
<TextBlock Text="Columns to skip when updating existing rows"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class DestinationEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DestinationEditorView"/> class.
|
||||
/// </summary>
|
||||
public DestinationEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.JdeDateEditorView"
|
||||
x:DataType="steps:JdeDateTransformerViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="JDE Date Transformer"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Convert JDE Julian date/time columns to DateTime"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Date Column -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Date Column"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding DateColumn}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="TRDJ"/>
|
||||
<TextBlock Text="Column containing JDE Julian date (e.g., TRDJ)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Time Column -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Time Column (Optional)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding TimeColumn}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="TRTM"/>
|
||||
<TextBlock Text="Column containing JDE time value (e.g., TRTM)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Output Column -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Output Column"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding OutputColumn}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="TransactionDateTime"/>
|
||||
<TextBlock Text="Name for the new DateTime column"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Info Box -->
|
||||
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="12" Margin="0,8,0,0">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="JDE Date Format" Foreground="#9BA8B8" FontSize="11" FontWeight="Medium"/>
|
||||
<TextBlock Text="JDE Julian dates are in CYYDDD format where:" Foreground="#5C6A7A" FontSize="10"/>
|
||||
<TextBlock Text=" C = Century (1=20th, 2=21st)" Foreground="#5C6A7A" FontSize="10"/>
|
||||
<TextBlock Text=" YY = Year" Foreground="#5C6A7A" FontSize="10"/>
|
||||
<TextBlock Text=" DDD = Day of year (001-366)" Foreground="#5C6A7A" FontSize="10"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class JdeDateEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JdeDateEditorView"/> class.
|
||||
/// </summary>
|
||||
public JdeDateEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.PostScriptEditorView"
|
||||
x:DataType="steps:PostScriptStepViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header (dynamic based on script type) -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding DisplayName, StringFormat='{}{0} Configuration'}"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="SQL script to execute"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Script Content -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Script"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding Script}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="200"
|
||||
Watermark="-- SQL Script
EXEC dbo.MyProcedure @Param1"/>
|
||||
<TextBlock Text="SQL script or stored procedure call to execute"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Info Box -->
|
||||
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="12" Margin="0,8,0,0">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Script Execution" Foreground="#9BA8B8" FontSize="11" FontWeight="Medium"/>
|
||||
<TextBlock Foreground="#5C6A7A" FontSize="10">
|
||||
<Run Text="Pre-scripts run before data extraction."/>
|
||||
</TextBlock>
|
||||
<TextBlock Foreground="#5C6A7A" FontSize="10">
|
||||
<Run Text="Post-scripts run after data loading completes."/>
|
||||
</TextBlock>
|
||||
<TextBlock Foreground="#5C6A7A" FontSize="10">
|
||||
<Run Text="Use for index management, statistics updates, or cleanup tasks."/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class PostScriptEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostScriptEditorView"/> class.
|
||||
/// </summary>
|
||||
public PostScriptEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.RegexEditorView"
|
||||
x:DataType="steps:RegexTransformerViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Regex Transformer"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Transform column values using regular expressions"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Column Name -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Column"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding ColumnName}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="BatchID"/>
|
||||
<TextBlock Text="Column to apply the regex transformation"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Mode" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<RadioButton GroupName="RegexMode"
|
||||
IsChecked="{Binding IsFindReplaceMode}"
|
||||
Foreground="#E6EDF5">
|
||||
<TextBlock Text="Find & Replace" FontSize="12"/>
|
||||
</RadioButton>
|
||||
<RadioButton GroupName="RegexMode"
|
||||
IsChecked="{Binding IsMatchExtractMode}"
|
||||
Foreground="#E6EDF5">
|
||||
<TextBlock Text="Match & Extract" FontSize="12"/>
|
||||
</RadioButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Pattern -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Pattern"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding Pattern}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="^IIS_"/>
|
||||
<TextBlock Text="{Binding PatternHelpText}"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Replacement (only visible in Find & Replace mode) -->
|
||||
<StackPanel Spacing="4" IsVisible="{Binding IsFindReplaceMode}">
|
||||
<TextBlock Text="Replacement"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding Replacement}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="(empty to remove)"/>
|
||||
<TextBlock Text="Text to replace matches with (use $1, $2 for capture groups)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Options Row -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Options" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto">
|
||||
<!-- Case Insensitive Toggle -->
|
||||
<CheckBox Grid.Column="0"
|
||||
IsChecked="{Binding IgnoreCase}"
|
||||
Foreground="#E6EDF5" FontSize="12">
|
||||
<TextBlock Text="Case Insensitive" FontSize="12"/>
|
||||
</CheckBox>
|
||||
|
||||
<!-- Non-Match Behavior -->
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="If No Match"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
<ComboBox SelectedItem="{Binding NonMatchBehavior}"
|
||||
ItemsSource="{Binding NonMatchBehaviorOptions}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="32"
|
||||
FontSize="11" MinWidth="140"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Test/Preview Section -->
|
||||
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="12" Margin="0,8,0,0">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Test Pattern"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
|
||||
<!-- Test Input Row -->
|
||||
<Grid ColumnDefinitions="*,8,Auto">
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding TestInput}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="IIS_12345"/>
|
||||
<Button Grid.Column="2"
|
||||
Content="Test"
|
||||
Background="#3B82F6" Foreground="#FFFFFF"
|
||||
BorderThickness="0" Height="36"
|
||||
Padding="16,0"
|
||||
Command="{Binding TestPatternCommand}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Result Display -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="10" MinHeight="44"
|
||||
IsVisible="{Binding HasTestResult}">
|
||||
<Grid ColumnDefinitions="Auto,8,*">
|
||||
<!-- Status Icon -->
|
||||
<Border Grid.Column="0"
|
||||
Width="24" Height="24" CornerRadius="12"
|
||||
Background="{Binding TestResultBackground}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding TestResultIcon}"
|
||||
Foreground="#FFFFFF"
|
||||
FontSize="12" FontWeight="Bold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- Result Text -->
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding TestResultLabel}"
|
||||
Foreground="#5C6A7A" FontSize="10"/>
|
||||
<TextBlock Text="{Binding TestResultValue}"
|
||||
Foreground="#E6EDF5" FontSize="12"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Error Display -->
|
||||
<Border Background="#2D1F1F" BorderBrush="#5C2626" BorderThickness="1"
|
||||
CornerRadius="4" Padding="10"
|
||||
IsVisible="{Binding HasTestError}">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="Invalid Pattern"
|
||||
Foreground="#FF6B6B" FontSize="11" FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding TestErrorMessage}"
|
||||
Foreground="#CC8888" FontSize="11"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Help Info -->
|
||||
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="12" Margin="0,4,0,0">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Pattern Examples" Foreground="#9BA8B8" FontSize="11" FontWeight="Medium"/>
|
||||
<TextBlock Text=" ^IIS_ Remove 'IIS_' prefix" Foreground="#5C6A7A" FontSize="10" FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text=" _SUFFIX$ Remove '_SUFFIX' at end" Foreground="#5C6A7A" FontSize="10" FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text=" (\d+) Extract first number" Foreground="#5C6A7A" FontSize="10" FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class RegexEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegexEditorView"/> class.
|
||||
/// </summary>
|
||||
public RegexEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.ScriptEditorView"
|
||||
x:DataType="steps:PreScriptStepViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header (dynamic based on script type) -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding DisplayName, StringFormat='{}{0} Configuration'}"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="SQL script to execute"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Script Content -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Script"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding Script}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="200"
|
||||
Watermark="-- SQL Script
EXEC dbo.MyProcedure @Param1"/>
|
||||
<TextBlock Text="SQL script or stored procedure call to execute"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Info Box -->
|
||||
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="12" Margin="0,8,0,0">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Script Execution" Foreground="#9BA8B8" FontSize="11" FontWeight="Medium"/>
|
||||
<TextBlock Foreground="#5C6A7A" FontSize="10">
|
||||
<Run Text="Pre-scripts run before data extraction."/>
|
||||
</TextBlock>
|
||||
<TextBlock Foreground="#5C6A7A" FontSize="10">
|
||||
<Run Text="Post-scripts run after data loading completes."/>
|
||||
</TextBlock>
|
||||
<TextBlock Foreground="#5C6A7A" FontSize="10">
|
||||
<Run Text="Use for index management, statistics updates, or cleanup tasks."/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class ScriptEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScriptEditorView"/> class.
|
||||
/// </summary>
|
||||
public ScriptEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Editors.SourceEditorView"
|
||||
x:DataType="steps:SourceStepViewModel">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Source Configuration"
|
||||
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Configure the data source for this pipeline"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Connection -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Connection"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<ComboBox ItemsSource="{Binding AvailableConnections}"
|
||||
SelectedItem="{Binding Connection}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
HorizontalAlignment="Stretch"
|
||||
PlaceholderText="Select connection..."/>
|
||||
<TextBlock Text="Connection string name from Settings > ConnectionStrings"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Query -->
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<TextBlock Text="Query"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding Query}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="100"
|
||||
Watermark="SELECT ... FROM ... WHERE ..."/>
|
||||
<TextBlock Text="SQL query for incremental updates (use @LastSync parameter)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Mass Query -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Mass Query (Optional)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding MassQuery}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="80"
|
||||
Watermark="SELECT ... FROM ... (no date filter)"/>
|
||||
<TextBlock Text="Query for full table reload during mass sync"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Parameters Section -->
|
||||
<Expander IsExpanded="False">
|
||||
<Expander.Header>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Parameters" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding Parameters.Count, StringFormat='({0})'}"
|
||||
Foreground="#5C6A7A" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Expander.Header>
|
||||
<StackPanel Spacing="8" Margin="0,8,0,0">
|
||||
<ItemsControl ItemsSource="{Binding Parameters}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="steps:ParameterViewModel">
|
||||
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="4" Padding="8" Margin="0,0,0,4">
|
||||
<Grid ColumnDefinitions="*,8,*,8,*,8,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="Key" Foreground="#5C6A7A" FontSize="10"/>
|
||||
<TextBox Text="{Binding Key}"
|
||||
Background="#232A35" Height="28" FontSize="11"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock Text="Format" Foreground="#5C6A7A" FontSize="10"/>
|
||||
<TextBox Text="{Binding Format}"
|
||||
Background="#232A35" Height="28" FontSize="11"
|
||||
Watermark="jdeJulian"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="4" Spacing="2">
|
||||
<TextBlock Text="Source" Foreground="#5C6A7A" FontSize="10"/>
|
||||
<TextBox Text="{Binding Source}"
|
||||
Background="#232A35" Height="28" FontSize="11"
|
||||
Watermark="offset"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="6" Content="X"
|
||||
Background="Transparent" Foreground="#FF6B6B"
|
||||
BorderThickness="0" FontSize="11" Width="24"
|
||||
VerticalAlignment="Bottom"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Content="+ Add Parameter"
|
||||
Background="#232A35" Foreground="#9BA8B8"
|
||||
BorderBrush="#3D4550" Height="32"
|
||||
HorizontalAlignment="Left" Padding="12,0"
|
||||
Command="{Binding AddParameterCommand}"/>
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Editors;
|
||||
|
||||
public partial class SourceEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SourceEditorView"/> class.
|
||||
/// </summary>
|
||||
public SourceEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.AuthFormView"
|
||||
x:DataType="vm:AuthFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Authentication Settings"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Cookie Settings Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Cookie Settings" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Cookie Name -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Cookie Name"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding CookieName}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark=".JdeScoping.Auth"/>
|
||||
<TextBlock Text="Name of the authentication cookie stored in the browser"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Cookie Expiration -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Cookie Expiration (minutes)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding CookieExpirationMinutes}"
|
||||
Minimum="1" Maximum="525600"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="How long the authentication cookie remains valid (e.g., 480 = 8 hours, 525600 = 1 year)"
|
||||
Foreground="#5C6A7A" FontSize="11" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class AuthFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthFormView"/> class.
|
||||
/// </summary>
|
||||
public AuthFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
xmlns:models="using:JdeScoping.ConfigManager.Ui.Models"
|
||||
xmlns:local="using:JdeScoping.ConfigManager.Ui.Converters"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.ConnectionStringsFormView"
|
||||
x:DataType="vm:ConnectionStringsFormViewModel">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- SqlServer Provider Template -->
|
||||
<DataTemplate x:Key="SqlServerTemplate" x:DataType="vm:ConnectionStringEntryViewModel">
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Server & Port Row -->
|
||||
<Grid ColumnDefinitions="*,16,120">
|
||||
<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="Port" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding SqlServerPort}"
|
||||
Minimum="1" Maximum="65535"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Default: 1433"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Database Row -->
|
||||
<StackPanel 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Connection List -->
|
||||
<Border BorderBrush="#2D3540" BorderThickness="1" CornerRadius="4">
|
||||
<DataGrid ItemsSource="{Binding Connections}"
|
||||
SelectedItem="{Binding SelectedConnection}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Single"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column"
|
||||
Background="#0D0F12"
|
||||
RowBackground="#0D0F12"
|
||||
MaxHeight="220"
|
||||
MinHeight="100">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="Auto"/>
|
||||
<DataGridTextColumn Header="Provider" Binding="{Binding ProviderDisplay}" Width="Auto"/>
|
||||
<DataGridTextColumn Header="Server" Binding="{Binding ServerDisplay}" Width="*"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Border>
|
||||
|
||||
<!-- 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>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class ConnectionStringsFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionStringsFormView"/> class.
|
||||
/// </summary>
|
||||
public ConnectionStringsFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.DataAccessFormView"
|
||||
x:DataType="vm:DataAccessFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Data Access Settings"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Query Timeouts Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Query Timeouts" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Default Timeout -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Default Timeout (seconds)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding DefaultTimeoutSeconds}"
|
||||
Minimum="5" Maximum="3600"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Default timeout for all database queries"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,16,*">
|
||||
<!-- Lot Usage Timeout -->
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Lot Usage Timeout (s)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding LotUsageTimeoutSeconds}"
|
||||
Minimum="5" Maximum="7200"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- MIS Data Timeout -->
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="MIS Data Timeout (s)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding MisDataTimeoutSeconds}"
|
||||
Minimum="5" Maximum="7200"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Schema Names Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Schema Names" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Production Schema -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Production Schema"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding ProductionSchema}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="dbo"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Archive Schema -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Archive Schema"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding ArchiveSchema}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="archive"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Stage Schema -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Stage Schema"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding StageSchema}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="stage"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Debug Options Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Debug Options" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<CheckBox IsChecked="{Binding EnableDetailedLogging}" Foreground="#E6EDF5">
|
||||
<TextBlock Text="Enable Detailed Logging" Foreground="#E6EDF5"/>
|
||||
</CheckBox>
|
||||
<TextBlock Text="Enables verbose logging for troubleshooting database operations"
|
||||
Foreground="#5C6A7A" FontSize="11" Margin="24,0,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class DataAccessFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataAccessFormView"/> class.
|
||||
/// </summary>
|
||||
public DataAccessFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.DataSyncFormView"
|
||||
x:DataType="vm:DataSyncFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Data Sync Settings"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Enable/Disable -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<CheckBox IsChecked="{Binding Enabled}" Foreground="#E6EDF5">
|
||||
<TextBlock Text="Enable Data Synchronization" Foreground="#E6EDF5"/>
|
||||
</CheckBox>
|
||||
</Border>
|
||||
|
||||
<!-- Sync Intervals Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Sync Intervals" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Check Interval -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Check Interval (minutes)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding CheckIntervalMinutes}"
|
||||
Minimum="1" Maximum="1440"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="How often to check for pending sync operations"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Sync Timeout -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Sync Timeout (seconds)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding SyncTimeoutSeconds}"
|
||||
Minimum="30" Maximum="3600"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Maximum time allowed for a single sync operation"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Performance Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Performance" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<Grid ColumnDefinitions="*,16,*">
|
||||
<!-- Max Degree of Parallelism -->
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Max Parallelism"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding MaxDegreeOfParallelism}"
|
||||
Minimum="1" Maximum="16"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Batch Size -->
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Batch Size"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding BatchSize}"
|
||||
Minimum="100" Maximum="100000"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Bulk Copy Batch Size -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Bulk Copy Batch Size"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding BulkCopyBatchSize}"
|
||||
Minimum="100" Maximum="100000"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Number of rows per bulk copy batch"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Data Retention Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Data Retention" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<Grid ColumnDefinitions="*,16,*">
|
||||
<!-- Lookback Multiplier -->
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Lookback Multiplier"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding LookbackMultiplier}"
|
||||
Minimum="1.0" Maximum="10.0"
|
||||
Increment="0.1" FormatString="F1"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Purge Retention Days -->
|
||||
<StackPanel Grid.Column="2" Spacing="4">
|
||||
<TextBlock Text="Purge Retention (days)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding PurgeRetentionDays}"
|
||||
Minimum="1" Maximum="365"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBlock Text="Lookback multiplier extends the sync window; retention days controls when old data is purged"
|
||||
Foreground="#5C6A7A" FontSize="11" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class DataSyncFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataSyncFormView"/> class.
|
||||
/// </summary>
|
||||
public DataSyncFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.ExcelExportFormView"
|
||||
x:DataType="vm:ExcelExportFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Excel Export Settings"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Output Format Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Output Format" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Max Rows Per Sheet -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Max Rows Per Sheet"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding MaxRowsPerSheet}"
|
||||
Minimum="1000" Maximum="1048576"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Excel limit is 1,048,576 rows per sheet"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Default Date Format -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Default Date Format"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding DefaultDateFormat}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="MM/dd/yyyy HH:mm:ss"/>
|
||||
<TextBlock Text="Format string for date/time values in Excel"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Timezone Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Timezone" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Timezone"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<ComboBox ItemsSource="{Binding AvailableTimezones}"
|
||||
SelectedItem="{Binding SelectedTimezone}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Text="Used for converting UTC timestamps to local time in exports"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Debug Options Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Debug Options" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<CheckBox IsChecked="{Binding DebugWriteToFile}" Foreground="#E6EDF5">
|
||||
<TextBlock Text="Write Debug Output to File" Foreground="#E6EDF5"/>
|
||||
</CheckBox>
|
||||
|
||||
<!-- Debug Output Directory -->
|
||||
<StackPanel Spacing="4" Margin="24,0,0,0">
|
||||
<TextBlock Text="Debug Output Directory"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding DebugOutputDirectory}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="C:\Temp\ExcelDebug"
|
||||
IsEnabled="{Binding DebugWriteToFile}"/>
|
||||
<TextBlock Text="Directory where debug Excel files will be written"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class ExcelExportFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExcelExportFormView"/> class.
|
||||
/// </summary>
|
||||
public ExcelExportFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.LdapFormView"
|
||||
x:DataType="vm:LdapFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="LDAP Settings"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Server Configuration Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Server Configuration" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Server URLs -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Server URLs (one per line)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding ServerUrlsText}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="80"
|
||||
Watermark="ldap://ldap.example.com:389"/>
|
||||
<TextBlock Text="LDAP server URLs in order of preference for failover"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Connection Timeout -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Connection Timeout (seconds)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding ConnectionTimeoutSeconds}"
|
||||
Minimum="1" Maximum="300"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Directory Structure Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Directory Structure" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Group DN -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Group Distinguished Name (DN)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding GroupDn}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="CN=AppUsers,OU=Groups,DC=example,DC=com"/>
|
||||
<TextBlock Text="The group users must belong to for application access"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Search Base -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Search Base DN"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding SearchBase}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
Watermark="OU=Users,DC=example,DC=com"/>
|
||||
<TextBlock Text="Base DN for user searches"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Development Options Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Development Options" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<CheckBox IsChecked="{Binding UseFakeAuth}" Foreground="#E6EDF5">
|
||||
<TextBlock Text="Use Fake Authentication (Development Only)" Foreground="#FFB84D"/>
|
||||
</CheckBox>
|
||||
<TextBlock Text="WARNING: Bypasses LDAP authentication entirely. Never enable in production!"
|
||||
Foreground="#FF6B6B" FontSize="11" Margin="24,0,0,0" TextWrapping="Wrap"/>
|
||||
|
||||
<!-- Admin Bypass Users -->
|
||||
<StackPanel Spacing="4" Margin="0,8,0,0">
|
||||
<TextBlock Text="Admin Bypass Users (one per line)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding AdminBypassUsersText}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550"
|
||||
FontFamily="JetBrains Mono"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="NoWrap"
|
||||
MinHeight="60"
|
||||
Watermark="admin"/>
|
||||
<TextBlock Text="Users who bypass group membership requirements"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class LdapFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LdapFormView"/> class.
|
||||
/// </summary>
|
||||
public LdapFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
xmlns:controls="using:JdeScoping.ConfigManager.Ui.Views.Controls"
|
||||
xmlns:editors="using:JdeScoping.ConfigManager.Ui.Views.Editors"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.PipelineEditorView"
|
||||
x:DataType="vm:PipelineEditorViewModel">
|
||||
|
||||
<UserControl.Resources>
|
||||
<!-- Step card colors by type -->
|
||||
<SolidColorBrush x:Key="PreScriptBrush" Color="#8B5CF6"/>
|
||||
<SolidColorBrush x:Key="SourceBrush" Color="#3B82F6"/>
|
||||
<SolidColorBrush x:Key="TransformerBrush" Color="#F59E0B"/>
|
||||
<SolidColorBrush x:Key="DestinationBrush" Color="#10B981"/>
|
||||
<SolidColorBrush x:Key="PostScriptBrush" Color="#EC4899"/>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" MinWidth="180"/>
|
||||
<ColumnDefinition Width="1"/>
|
||||
<ColumnDefinition Width="*" MinWidth="300"/>
|
||||
<ColumnDefinition Width="1"/>
|
||||
<ColumnDefinition Width="350" MinWidth="280"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left Panel: Toolbar and Add Buttons -->
|
||||
<Border Grid.Column="0" Background="#0D0F12" Padding="12">
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Header -->
|
||||
<TextBlock Text="{Binding Name, StringFormat='{}{0}'}"
|
||||
Foreground="#E6EDF5" FontSize="16" FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<Border Height="1" Background="#2D3540"/>
|
||||
|
||||
<!-- Add Steps Section -->
|
||||
<TextBlock Text="ADD STEPS" Foreground="#5C6A7A" FontSize="11" FontWeight="Medium"/>
|
||||
|
||||
<!-- Add Pre-Script Button -->
|
||||
<Button Command="{Binding AddPreScriptCommand}"
|
||||
Background="#151920" Foreground="#E6EDF5"
|
||||
BorderBrush="#2D3540" BorderThickness="1"
|
||||
HorizontalAlignment="Stretch" Height="36"
|
||||
HorizontalContentAlignment="Left" Padding="12,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="+" Foreground="#8B5CF6" FontWeight="Bold" FontSize="14"/>
|
||||
<TextBlock Text="Pre-Script" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- Add Transformer Dropdown -->
|
||||
<StackPanel Spacing="4">
|
||||
<ComboBox ItemsSource="{Binding AvailableTransformerTypes}"
|
||||
SelectedItem="{Binding SelectedTransformerType}"
|
||||
Background="#151920" Foreground="#E6EDF5"
|
||||
BorderBrush="#2D3540" BorderThickness="1"
|
||||
HorizontalAlignment="Stretch" Height="32"
|
||||
PlaceholderText="Select Transformer..."/>
|
||||
<Button Command="{Binding AddTransformerCommand}"
|
||||
Background="#151920" Foreground="#E6EDF5"
|
||||
BorderBrush="#2D3540" BorderThickness="1"
|
||||
HorizontalAlignment="Stretch" Height="36"
|
||||
HorizontalContentAlignment="Left" Padding="12,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="+" Foreground="#F59E0B" FontWeight="Bold" FontSize="14"/>
|
||||
<TextBlock Text="Transformer" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Add Post-Script Button -->
|
||||
<Button Command="{Binding AddPostScriptCommand}"
|
||||
Background="#151920" Foreground="#E6EDF5"
|
||||
BorderBrush="#2D3540" BorderThickness="1"
|
||||
HorizontalAlignment="Stretch" Height="36"
|
||||
HorizontalContentAlignment="Left" Padding="12,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="+" Foreground="#EC4899" FontWeight="Bold" FontSize="14"/>
|
||||
<TextBlock Text="Post-Script" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Border Height="1" Background="#2D3540" Margin="0,8,0,0"/>
|
||||
|
||||
<!-- Schedules Section -->
|
||||
<Expander IsExpanded="False">
|
||||
<Expander.Header>
|
||||
<TextBlock Text="Schedules" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
</Expander.Header>
|
||||
<StackPanel Spacing="8" Margin="0,8,0,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<CheckBox IsChecked="{Binding MassSyncEnabled}"/>
|
||||
<TextBlock Text="Mass" Foreground="#9BA8B8" FontSize="12"/>
|
||||
<NumericUpDown Value="{Binding MassSyncIntervalMinutes}"
|
||||
Minimum="1" Width="80" Height="28"
|
||||
Background="#232A35" FontSize="11"
|
||||
IsEnabled="{Binding MassSyncEnabled}"/>
|
||||
<TextBlock Text="min" Foreground="#5C6A7A" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<CheckBox IsChecked="{Binding DailySyncEnabled}"/>
|
||||
<TextBlock Text="Daily" Foreground="#9BA8B8" FontSize="12"/>
|
||||
<NumericUpDown Value="{Binding DailySyncIntervalMinutes}"
|
||||
Minimum="1" Width="80" Height="28"
|
||||
Background="#232A35" FontSize="11"
|
||||
IsEnabled="{Binding DailySyncEnabled}"/>
|
||||
<TextBlock Text="min" Foreground="#5C6A7A" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<CheckBox IsChecked="{Binding HourlySyncEnabled}"/>
|
||||
<TextBlock Text="Hourly" Foreground="#9BA8B8" FontSize="12"/>
|
||||
<NumericUpDown Value="{Binding HourlySyncIntervalMinutes}"
|
||||
Minimum="1" Width="80" Height="28"
|
||||
Background="#232A35" FontSize="11"
|
||||
IsEnabled="{Binding HourlySyncEnabled}"/>
|
||||
<TextBlock Text="min" Foreground="#5C6A7A" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Divider -->
|
||||
<Border Grid.Column="1" Background="#2D3540"/>
|
||||
|
||||
<!-- Center Panel: Visual Pipeline Flow -->
|
||||
<Border Grid.Column="2" Background="#151920" Padding="16">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Spacing="0" HorizontalAlignment="Center">
|
||||
|
||||
<!-- Pre-Scripts -->
|
||||
<ItemsControl ItemsSource="{Binding PreScripts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<controls:PipelineStepCard DataContext="{Binding}"
|
||||
StepColor="#8B5CF6"/>
|
||||
<controls:FlowArrow/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Source Step (always present) -->
|
||||
<controls:PipelineStepCard DataContext="{Binding Source}"
|
||||
StepColor="#3B82F6"/>
|
||||
<controls:FlowArrow/>
|
||||
|
||||
<!-- Transformers -->
|
||||
<ItemsControl ItemsSource="{Binding Transformers}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<controls:PipelineStepCard DataContext="{Binding}"
|
||||
StepColor="#F59E0B"/>
|
||||
<controls:FlowArrow/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Destination Step (always present) -->
|
||||
<controls:PipelineStepCard DataContext="{Binding Destination}"
|
||||
StepColor="#10B981"/>
|
||||
|
||||
<!-- Post-Scripts -->
|
||||
<ItemsControl ItemsSource="{Binding PostScripts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<controls:FlowArrow/>
|
||||
<controls:PipelineStepCard DataContext="{Binding}"
|
||||
StepColor="#EC4899"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Divider -->
|
||||
<Border Grid.Column="3" Background="#2D3540"/>
|
||||
|
||||
<!-- Right Panel: Properties Editor -->
|
||||
<Border Grid.Column="4" Background="#0D0F12" Padding="16">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="16">
|
||||
<!-- Properties Header with Action Buttons -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="PROPERTIES"
|
||||
Foreground="#5C6A7A" FontSize="11" FontWeight="Medium"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
|
||||
<!-- Move Up Button -->
|
||||
<Button Command="{Binding MoveSelectedStepUpCommand}"
|
||||
Background="#1F2937" Foreground="#9BA8B8"
|
||||
BorderBrush="#3D4550" BorderThickness="1"
|
||||
Height="28" Width="28" Padding="0"
|
||||
ToolTip.Tip="Move step up"
|
||||
IsVisible="{Binding CanDeleteSelectedStep}">
|
||||
<TextBlock Text="▲" FontSize="10" HorizontalAlignment="Center"/>
|
||||
</Button>
|
||||
<!-- Move Down Button -->
|
||||
<Button Command="{Binding MoveSelectedStepDownCommand}"
|
||||
Background="#1F2937" Foreground="#9BA8B8"
|
||||
BorderBrush="#3D4550" BorderThickness="1"
|
||||
Height="28" Width="28" Padding="0"
|
||||
ToolTip.Tip="Move step down"
|
||||
IsVisible="{Binding CanDeleteSelectedStep}">
|
||||
<TextBlock Text="▼" FontSize="10" HorizontalAlignment="Center"/>
|
||||
</Button>
|
||||
<!-- Delete Button -->
|
||||
<Button Command="{Binding DeleteSelectedStepCommand}"
|
||||
Background="#3D1F1F" Foreground="#FF6B6B"
|
||||
BorderBrush="#5C2D2D" BorderThickness="1"
|
||||
Height="28" Padding="12,0"
|
||||
ToolTip.Tip="Delete selected step"
|
||||
IsVisible="{Binding CanDeleteSelectedStep}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="🗑" FontSize="12"/>
|
||||
<TextBlock Text="Delete" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,8,0,0"/>
|
||||
|
||||
<!-- Step Editor Content (changes based on selection) -->
|
||||
<!-- Shows placeholder text when nothing selected, otherwise uses DataTemplates from MainWindow -->
|
||||
<TextBlock Text="Select a step to edit its properties"
|
||||
Foreground="#5C6A7A" FontSize="12"
|
||||
FontStyle="Italic"
|
||||
IsVisible="{Binding SelectedStepEditor, Converter={x:Static ObjectConverters.IsNull}}"/>
|
||||
<ContentControl Content="{Binding SelectedStepEditor}"
|
||||
IsVisible="{Binding SelectedStepEditor, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class PipelineEditorView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipelineEditorView"/> class.
|
||||
/// </summary>
|
||||
public PipelineEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.SearchFormView"
|
||||
x:DataType="vm:SearchFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="Search Settings"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Search Limits Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Search Limits" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Max Result Rows -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Max Result Rows"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding MaxResultRows}"
|
||||
Minimum="100" Maximum="1000000"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Maximum number of rows returned by a single search"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Max Concurrent Searches -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Max Concurrent Searches"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding MaxConcurrentSearches}"
|
||||
Minimum="1" Maximum="50"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Maximum number of searches that can run simultaneously"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Timeout Section -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Timeout" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Search Timeout -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Search Timeout (seconds)"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<NumericUpDown Value="{Binding TimeoutSeconds}"
|
||||
Minimum="10" Maximum="3600"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"/>
|
||||
<TextBlock Text="Maximum time allowed for a search operation to complete"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class SearchFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SearchFormView"/> class.
|
||||
/// </summary>
|
||||
public SearchFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.SecretFormView"
|
||||
x:DataType="vm:SecretFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<PathIcon Data="M12 1L3 5V11C3 16.55 6.84 21.74 12 23C17.16 21.74 21 16.55 21 11V5L12 1M12 7C13.4 7 14.8 8.1 14.8 9.5V11C15.4 11 16 11.6 16 12.3V15.8C16 16.4 15.4 17 14.7 17H9.2C8.6 17 8 16.4 8 15.7V12.2C8 11.6 8.6 11 9.2 11V9.5C9.2 8.1 10.6 7 12 7M12 8.2C11.2 8.2 10.5 8.7 10.5 9.5V11H13.5V9.5C13.5 8.7 12.8 8.2 12 8.2Z"
|
||||
Width="24" Height="24" Foreground="#3B82F6"/>
|
||||
<TextBlock Text="Secret Settings"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Secret Details Card -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Secret Details" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Key (Read-only) -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Key"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding Key}"
|
||||
Background="#1A1F26" Foreground="#9BA8B8"
|
||||
BorderBrush="#2D3540" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
IsReadOnly="True"/>
|
||||
<TextBlock Text="The unique identifier for this secret (cannot be changed)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Value with Show/Hide -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Value"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<!-- Masked value display when hidden -->
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding Value}"
|
||||
Background="#232A35" Foreground="#E6EDF5"
|
||||
BorderBrush="#3D4550" Height="36"
|
||||
FontFamily="JetBrains Mono"
|
||||
PasswordChar="*"
|
||||
RevealPassword="{Binding IsValueVisible}"
|
||||
Watermark="Enter secret value..."/>
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding ToggleVisibilityCommand}"
|
||||
Background="#3D4550" Foreground="#E6EDF5"
|
||||
Padding="12,8" Margin="8,0,0,0"
|
||||
CornerRadius="4" Height="36"
|
||||
VerticalAlignment="Top">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<!-- Eye icon for Show -->
|
||||
<PathIcon Data="M12 9C10.34 9 9 10.34 9 12C9 13.66 10.34 15 12 15C13.66 15 15 13.66 15 12C15 10.34 13.66 9 12 9M12 17C9.24 17 7 14.76 7 12C7 9.24 9.24 7 12 7C14.76 7 17 9.24 17 12C17 14.76 14.76 17 12 17M12 4.5C7 4.5 2.73 7.61 1 12C2.73 16.39 7 19.5 12 19.5C17 19.5 21.27 16.39 23 12C21.27 7.61 17 4.5 12 4.5Z"
|
||||
Width="14" Height="14" Foreground="#E6EDF5"
|
||||
IsVisible="{Binding !IsValueVisible}"/>
|
||||
<!-- Eye-off icon for Hide -->
|
||||
<PathIcon Data="M11.83 9L15 12.16V12C15 10.34 13.66 9 12 9H11.83M7.53 9.8L9.08 11.35C9.03 11.56 9 11.77 9 12C9 13.66 10.34 15 12 15C12.22 15 12.44 14.97 12.65 14.92L14.2 16.47C13.53 16.8 12.79 17 12 17C9.24 17 7 14.76 7 12C7 11.21 7.2 10.47 7.53 9.8M2 4.27L4.28 6.55L4.73 7C3.08 8.3 1.78 10 1 12C2.73 16.39 7 19.5 12 19.5C13.55 19.5 15.03 19.2 16.38 18.66L16.81 19.08L19.73 22L21 20.73L3.27 3M12 7C14.76 7 17 9.24 17 12C17 12.64 16.87 13.26 16.64 13.82L19.57 16.75C21.07 15.5 22.27 13.86 23 12C21.27 7.61 17 4.5 12 4.5C10.6 4.5 9.26 4.75 8 5.2L10.17 7.35C10.74 7.13 11.35 7 12 7Z"
|
||||
Width="14" Height="14" Foreground="#E6EDF5"
|
||||
IsVisible="{Binding IsValueVisible}"/>
|
||||
<TextBlock Text="{Binding VisibilityButtonText}" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBlock Text="The secret value (encrypted at rest)"
|
||||
Foreground="#5C6A7A" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Actions Card -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Actions" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<!-- Copy to Clipboard Button -->
|
||||
<Button Command="{Binding CopyToClipboardCommand}"
|
||||
Background="#3B82F6" Foreground="#FFFFFF"
|
||||
Padding="16,10" CornerRadius="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<PathIcon Data="M19 21H8V7H19M19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1Z"
|
||||
Width="16" Height="16" Foreground="#FFFFFF"/>
|
||||
<TextBlock Text="Copy to Clipboard"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<Button Command="{Binding DeleteCommand}"
|
||||
Background="#DC2626" Foreground="#FFFFFF"
|
||||
Padding="16,10" CornerRadius="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<PathIcon Data="M19 4H15.5L14.5 3H9.5L8.5 4H5V6H19M6 19C6 20.1 6.9 21 8 21H16C17.1 21 18 20.1 18 19V7H6V19Z"
|
||||
Width="16" Height="16" Foreground="#FFFFFF"/>
|
||||
<TextBlock Text="Delete"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Warning: Deleting a secret cannot be undone if the store is saved."
|
||||
Foreground="#FF6B6B" FontSize="11" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class SecretFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretFormView"/> class.
|
||||
/// </summary>
|
||||
public SecretFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.Forms.SecureStoreInfoFormView"
|
||||
x:DataType="vm:SecureStoreInfoFormViewModel">
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="24" MaxWidth="600">
|
||||
<!-- Header -->
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<PathIcon Data="M12.65 10C11.83 7.67 9.61 6 7 6C3.69 6 1 8.69 1 12C1 15.31 3.69 18 7 18C9.61 18 11.83 16.33 12.65 14H17V18H21V14H23V10H12.65M7 14C5.9 14 5 13.1 5 12C5 10.9 5.9 10 7 10C8.1 10 9 10.9 9 12C9 13.1 8.1 14 7 14Z"
|
||||
Width="24" Height="24" Foreground="#F59E0B"/>
|
||||
<TextBlock Text="Secure Store"
|
||||
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Instructions Card -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<PathIcon Data="M11 7V9H13V7H11M14 17V15H13V11H10V13H11V15H10V17H14M22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2C17.5 2 22 6.5 22 12Z"
|
||||
Width="20" Height="20" Foreground="#3B82F6"/>
|
||||
<TextBlock Text="{Binding InstructionText}"
|
||||
Foreground="#E6EDF5" FontSize="14"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Store Info Card -->
|
||||
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
|
||||
CornerRadius="6" Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="Store Information" Foreground="#E6EDF5"
|
||||
FontWeight="SemiBold" FontSize="14"/>
|
||||
|
||||
<!-- Store Path -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Store Path"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding StorePath}"
|
||||
Background="#1A1F26" Foreground="#9BA8B8"
|
||||
BorderBrush="#2D3540" Height="36"
|
||||
FontFamily="JetBrains Mono" FontSize="12"
|
||||
IsReadOnly="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Key File Path -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Key File Path"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBox Text="{Binding KeyFilePath}"
|
||||
Background="#1A1F26" Foreground="#9BA8B8"
|
||||
BorderBrush="#2D3540" Height="36"
|
||||
FontFamily="JetBrains Mono" FontSize="12"
|
||||
IsReadOnly="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Secret Count -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Number of Secrets"
|
||||
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding SecretCount}"
|
||||
Foreground="#E6EDF5" FontSize="14"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views.Forms;
|
||||
|
||||
public partial class SecureStoreInfoFormView : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecureStoreInfoFormView"/> class.
|
||||
/// </summary>
|
||||
public SecureStoreInfoFormView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:JdeScoping.ConfigManager.Ui.ViewModels"
|
||||
xmlns:forms="using:JdeScoping.ConfigManager.Ui.ViewModels.Forms"
|
||||
xmlns:steps="using:JdeScoping.ConfigManager.Ui.ViewModels.PipelineSteps"
|
||||
xmlns:views="using:JdeScoping.ConfigManager.Ui.Views.Forms"
|
||||
xmlns:editors="using:JdeScoping.ConfigManager.Ui.Views.Editors"
|
||||
x:Class="JdeScoping.ConfigManager.Ui.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Title="JdeScoping ConfigManager"
|
||||
Width="1200" Height="800"
|
||||
MinWidth="900" MinHeight="600"
|
||||
Background="#0D0F12">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<Window.DataTemplates>
|
||||
<!-- Settings Form ViewModels -->
|
||||
<DataTemplate DataType="{x:Type forms:DataSyncFormViewModel}">
|
||||
<views:DataSyncFormView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type forms:DataAccessFormViewModel}">
|
||||
<views:DataAccessFormView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type forms:AuthFormViewModel}">
|
||||
<views:AuthFormView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type forms:LdapFormViewModel}">
|
||||
<views:LdapFormView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type forms:SearchFormViewModel}">
|
||||
<views:SearchFormView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type forms:ExcelExportFormViewModel}">
|
||||
<views:ExcelExportFormView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type forms:ConnectionStringsFormViewModel}">
|
||||
<views:ConnectionStringsFormView/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Pipeline Editor (replaces PipelineFormViewModel) -->
|
||||
<DataTemplate DataType="{x:Type forms:PipelineEditorViewModel}">
|
||||
<views:PipelineEditorView/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Pipeline Step Editors (for the properties panel) -->
|
||||
<DataTemplate DataType="{x:Type steps:SourceStepViewModel}">
|
||||
<editors:SourceEditorView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type steps:DestinationStepViewModel}">
|
||||
<editors:DestinationEditorView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type steps:ColumnDropTransformerViewModel}">
|
||||
<editors:ColumnDropEditorView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type steps:ColumnRenameTransformerViewModel}">
|
||||
<editors:ColumnRenameEditorView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type steps:JdeDateTransformerViewModel}">
|
||||
<editors:JdeDateEditorView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type steps:RegexTransformerViewModel}">
|
||||
<editors:RegexEditorView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type steps:PreScriptStepViewModel}">
|
||||
<editors:ScriptEditorView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type steps:PostScriptStepViewModel}">
|
||||
<editors:PostScriptEditorView/>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- SecureStore ViewModels -->
|
||||
<DataTemplate DataType="{x:Type forms:SecureStoreInfoFormViewModel}">
|
||||
<views:SecureStoreInfoFormView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type forms:SecretFormViewModel}">
|
||||
<views:SecretFormView/>
|
||||
</DataTemplate>
|
||||
</Window.DataTemplates>
|
||||
|
||||
<DockPanel>
|
||||
<!-- Menu Bar -->
|
||||
<Menu DockPanel.Dock="Top" Background="#151920" Height="28">
|
||||
<MenuItem Header="_File">
|
||||
<MenuItem Header="_Open Folder..." Command="{Binding OpenFolderCommand}" InputGesture="Ctrl+O"/>
|
||||
<MenuItem Header="_Save" Command="{Binding SaveCommand}" InputGesture="Ctrl+S"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="E_xit" Command="{Binding ExitCommand}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Edit">
|
||||
<MenuItem Header="_Undo" Command="{Binding UndoCommand}" InputGesture="Ctrl+Z"/>
|
||||
<MenuItem Header="_Redo" Command="{Binding RedoCommand}" InputGesture="Ctrl+Y"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Tools">
|
||||
<MenuItem Header="_Validate All" Command="{Binding ValidateCommand}" InputGesture="F5"/>
|
||||
<MenuItem Header="_Test Connection" Command="{Binding TestConnectionCommand}" InputGesture="F6"/>
|
||||
<MenuItem Header="Validate _Runtime Config" Command="{Binding ValidateRuntimeConfigCommand}" InputGesture="F7"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="View _Backups..."/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Pipelines">
|
||||
<MenuItem Header="_New Pipeline..." Command="{Binding AddPipelineCommand}" InputGesture="Ctrl+Shift+P">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="+" FontSize="14" FontWeight="Bold"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Delete Pipeline" Command="{Binding DeletePipelineCommand}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="X" FontSize="12" FontWeight="Bold"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Secure Stores">
|
||||
<MenuItem Header="_New Store..." Command="{Binding NewStoreCommand}" InputGesture="Ctrl+Shift+N">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="+" FontSize="14" FontWeight="Bold"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Add Existing Store..." Command="{Binding AddExistingStoreCommand}" InputGesture="Ctrl+Shift+O">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="..." FontSize="14"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="_Save Store" Command="{Binding SaveStoreCommand}" InputGesture="Ctrl+Shift+S">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="S" FontSize="12" FontWeight="Bold"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="_Generate Key File..." Command="{Binding GenerateKeyFileCommand}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="K" FontSize="12" FontWeight="Bold"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Help">
|
||||
<MenuItem Header="_About ConfigManager"/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Border DockPanel.Dock="Top" Background="#151920" Height="40"
|
||||
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" Margin="8,0" VerticalAlignment="Center" Spacing="4">
|
||||
<Button Content="Open" Command="{Binding OpenFolderCommand}" Classes="toolbar"/>
|
||||
<Button Content="Save" Command="{Binding SaveCommand}" Classes="toolbar"/>
|
||||
<Border Width="1" Height="20" Background="#2D3540" Margin="4,0"/>
|
||||
<Button Content="Undo" Command="{Binding UndoCommand}" Classes="toolbar"/>
|
||||
<Button Content="Redo" Command="{Binding RedoCommand}" Classes="toolbar"/>
|
||||
<Border Width="1" Height="20" Background="#2D3540" Margin="4,0"/>
|
||||
<Button Content="Test" Command="{Binding TestConnectionCommand}" Classes="toolbar"/>
|
||||
<Button Content="Validate" Command="{Binding ValidateCommand}" Classes="toolbar"/>
|
||||
<Button Content="Validate Runtime" Command="{Binding ValidateRuntimeConfigCommand}"
|
||||
ToolTip.Tip="Validate SecureStore, Connection Strings, LDAP" Classes="toolbar"/>
|
||||
<Border Width="1" Height="20" Background="#2D3540" Margin="4,0"/>
|
||||
<Button Content="+ Pipeline" Command="{Binding AddPipelineCommand}" ToolTip.Tip="Add Pipeline" Classes="toolbar"/>
|
||||
<Border Width="1" Height="20" Background="#2D3540" Margin="4,0"/>
|
||||
<Button Content="+ Secret" Command="{Binding AddSecretCommand}" ToolTip.Tip="Add Secret" Classes="toolbar"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#151920" Height="24"
|
||||
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
|
||||
<Grid Margin="8,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="{Binding ConfigFolderPath}"
|
||||
Foreground="#5C6A7A" FontFamily="JetBrains Mono" FontSize="11"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text=" | Modified"
|
||||
Foreground="#5C9AFF" FontSize="11"
|
||||
IsVisible="{Binding HasUnsavedChanges}"
|
||||
VerticalAlignment="Center" Margin="8,0"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding ValidationStatus}"
|
||||
Foreground="{Binding ValidationStatusColor}"
|
||||
FontFamily="JetBrains Mono" FontSize="11"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Main Content -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="280" MinWidth="200" MaxWidth="400"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Tree View Panel -->
|
||||
<Border Grid.Column="0" Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="0,0,1,0">
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Background="#151920" Height="36">
|
||||
<TextBlock Text="CONFIGURATION"
|
||||
Foreground="#5C6A7A" FontSize="12" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" Margin="16,0"
|
||||
LetterSpacing="0.5"/>
|
||||
</Border>
|
||||
<TreeView ItemsSource="{Binding TreeNodes}"
|
||||
SelectedItem="{Binding SelectedNode}"
|
||||
Background="Transparent"
|
||||
Margin="8">
|
||||
<TreeView.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="New Pipeline..." Command="{Binding AddPipelineCommand}"/>
|
||||
<MenuItem Header="Delete Pipeline" Command="{Binding DeletePipelineCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Add Secret..." Command="{Binding AddSecretCommand}"/>
|
||||
<MenuItem Header="Delete Secret" Command="{Binding DeleteSecretCommand}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="Save Store" Command="{Binding SaveStoreCommand}"/>
|
||||
</ContextMenu>
|
||||
</TreeView.ContextMenu>
|
||||
<TreeView.ItemTemplate>
|
||||
<TreeDataTemplate ItemsSource="{Binding Children}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Icon}" FontSize="14"/>
|
||||
<TextBlock Text="{Binding Name}" Foreground="#E6EDF5"/>
|
||||
<TextBlock Text="{Binding StatusIcon}" FontSize="12"/>
|
||||
<TextBlock Text="*" Foreground="#5C9AFF"
|
||||
IsVisible="{Binding IsModified}"/>
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Splitter -->
|
||||
<GridSplitter Grid.Column="1" Width="4" Background="Transparent"
|
||||
ResizeDirection="Columns"/>
|
||||
|
||||
<!-- Form Panel -->
|
||||
<Border Grid.Column="2" Background="#151920" Padding="24">
|
||||
<ContentControl Content="{Binding SelectedFormViewModel}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MainWindow.
|
||||
/// </summary>
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="JdeScoping.ConfigManager"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user