using System.Collections.ObjectModel; using System.Windows.Input; using Avalonia.Media; using JdeScoping.ConfigManager.Core.Constants; using JdeScoping.ConfigManager.Core.Models; using JdeScoping.ConfigManager.Core.Services; using JdeScoping.ConfigManager.Core.Services.SecureStore; using JdeScoping.ConfigManager.Services; using JdeScoping.ConfigManager.ViewModels.Dialogs; using JdeScoping.ConfigManager.ViewModels.Forms; using JdeScoping.DataSync.Configuration; using Microsoft.Extensions.Logging; namespace JdeScoping.ConfigManager.ViewModels; /// /// Main window view model. /// public class MainWindowViewModel : ViewModelBase { private readonly IFileSystem _fileSystem; private readonly IConfigFileService _configFileService; private readonly IValidationService _validationService; private readonly IBackupService _backupService; private readonly IAutoDiscoveryService _autoDiscoveryService; private readonly IDialogService? _dialogService; private readonly ISecureStoreManager _secureStoreManager; private readonly IClipboardService _clipboardService; private readonly IRuntimeConfigValidationService _runtimeValidationService; private readonly IConnectionTestService _connectionTestService; private readonly ILogger? _logger; private string _configFolderPath = "No folder selected"; private bool _hasUnsavedChanges; private string _validationStatus = "Valid"; private IBrush _validationStatusColor = Brushes.LightGreen; private TreeNodeViewModel? _selectedNode; private object? _selectedFormViewModel; private ConfigModel? _appSettings; private Dictionary? _pipelines; /// /// Gets or sets the currently loaded configuration folder path. /// public string ConfigFolderPath { get => _configFolderPath; set => SetProperty(ref _configFolderPath, value); } /// /// Gets or sets a value indicating whether there are unsaved configuration changes. /// public bool HasUnsavedChanges { get => _hasUnsavedChanges; set => SetProperty(ref _hasUnsavedChanges, value); } /// /// Gets or sets the validation status message displayed to the user. /// public string ValidationStatus { get => _validationStatus; set => SetProperty(ref _validationStatus, value); } /// /// Gets or sets the brush color for the validation status indicator. /// public IBrush ValidationStatusColor { get => _validationStatusColor; set => SetProperty(ref _validationStatusColor, value); } /// /// Gets or sets the currently selected tree node in the configuration tree view. /// public TreeNodeViewModel? SelectedNode { get => _selectedNode; set { if (SetProperty(ref _selectedNode, value)) OnSelectedNodeChanged(); } } /// /// Gets or sets the view model for the form displayed when a node is selected. /// public object? SelectedFormViewModel { get => _selectedFormViewModel; set => SetProperty(ref _selectedFormViewModel, value); } /// /// Gets the collection of tree nodes representing the configuration structure. /// public ObservableCollection TreeNodes { get; } = []; /// /// Gets the command for opening a configuration folder. /// public ICommand OpenFolderCommand { get; } /// /// Gets the command for saving configuration changes. /// public ICommand SaveCommand { get; } /// /// Gets the command for exiting the application. /// public ICommand ExitCommand { get; } /// /// Gets the command for undoing the last configuration change. /// public ICommand UndoCommand { get; } /// /// Gets the command for redoing the last undone configuration change. /// public ICommand RedoCommand { get; } /// /// Gets the command for validating the current configuration. /// public ICommand ValidateCommand { get; } /// /// Gets the command for testing database connections. /// public ICommand TestConnectionCommand { get; } /// /// Gets the command for creating a new secure store. /// public ICommand NewStoreCommand { get; } /// /// Gets the command for adding an existing secure store. /// public ICommand AddExistingStoreCommand { get; } /// /// Gets the command for saving a secure store. /// public ICommand SaveStoreCommand { get; } /// /// Gets the command for generating a new key file. /// public ICommand GenerateKeyFileCommand { get; } /// /// Gets the command for adding a secret to the current store. /// public ICommand AddSecretCommand { get; } /// /// Gets the command for deleting a secret from the current store. /// public ICommand DeleteSecretCommand { get; } /// /// Gets the command for adding a new pipeline. /// public ICommand AddPipelineCommand { get; } /// /// Gets the command for deleting the selected pipeline. /// public ICommand DeletePipelineCommand { get; } /// /// Gets the command for validating runtime configuration using Infrastructure validators. /// public ICommand ValidateRuntimeConfigCommand { get; } /// /// Initializes a new instance of the class. /// /// File system abstraction for I/O operations. /// Service for loading and saving configuration files. /// Service for validating configuration settings. /// Service for creating configuration backups. /// Service for discovering configuration folder locations. /// Service for showing platform dialogs. /// Service for managing encrypted secret stores. /// Service for clipboard operations. /// Service for runtime configuration validation. /// Service for testing database connections. /// Optional logger for recording view model activities. public MainWindowViewModel( IFileSystem fileSystem, IConfigFileService configFileService, IValidationService validationService, IBackupService backupService, IAutoDiscoveryService autoDiscoveryService, IDialogService? dialogService, ISecureStoreManager secureStoreManager, IClipboardService clipboardService, IRuntimeConfigValidationService runtimeValidationService, IConnectionTestService connectionTestService, ILogger? logger) { _fileSystem = fileSystem; _configFileService = configFileService; _validationService = validationService; _backupService = backupService; _autoDiscoveryService = autoDiscoveryService; _dialogService = dialogService; _secureStoreManager = secureStoreManager; _clipboardService = clipboardService; _runtimeValidationService = runtimeValidationService; _connectionTestService = connectionTestService; _logger = logger; OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync); SaveCommand = new AsyncRelayCommand(SaveAsync, () => HasUnsavedChanges); ExitCommand = new RelayCommand(() => Environment.Exit(0)); UndoCommand = new RelayCommand(() => { }, () => false); // TODO: Implement undo/redo RedoCommand = new RelayCommand(() => { }, () => false); // TODO: Implement undo/redo ValidateCommand = new RelayCommand(Validate); TestConnectionCommand = new AsyncRelayCommand(TestConnectionAsync); // SecureStore commands NewStoreCommand = new AsyncRelayCommand(NewStoreAsync); AddExistingStoreCommand = new AsyncRelayCommand(AddExistingStoreAsync); SaveStoreCommand = new AsyncRelayCommand(SaveStoreAsync, CanSaveStore); GenerateKeyFileCommand = new AsyncRelayCommand(GenerateKeyFileAsync); AddSecretCommand = new AsyncRelayCommand(AddSecretAsync, CanAddSecret); DeleteSecretCommand = new AsyncRelayCommand(DeleteSecretAsync, CanDeleteSecret); // Pipeline commands AddPipelineCommand = new AsyncRelayCommand(AddPipelineAsync, CanAddPipeline); DeletePipelineCommand = new AsyncRelayCommand(DeletePipelineAsync, CanDeletePipeline); // Validation commands ValidateRuntimeConfigCommand = new AsyncRelayCommand( ValidateRuntimeConfigAsync, () => ConfigFolderPath != "No folder selected"); _ = InitializeAsync(); } /// /// Design-time constructor for XAML previewer. /// public MainWindowViewModel() : this( new FileSystem(), new ConfigFileService(new FileSystem()), new ValidationService(), new BackupService(new FileSystem()), new AutoDiscoveryService(new FileSystem()), null, new SecureStoreManager(), new NullClipboardService(), new RuntimeConfigValidationService(new SecureStoreManager()), new ConnectionTestService(), null) { } /// /// Null implementation of clipboard service for design-time. /// private class NullClipboardService : IClipboardService { /// /// Sets text to clipboard (no-op for design-time). /// /// The text to set (ignored in this implementation). /// A completed task. public Task SetTextAsync(string text) => Task.CompletedTask; } /// /// Initializes the view model by auto-discovering and loading configuration. /// private async Task InitializeAsync() { var folder = await _autoDiscoveryService.FindConfigFolderAsync(); if (folder != null) { await LoadConfigAsync(folder); } } /// /// Initializes the SecureStore automatically on config load. /// Creates the store if it doesn't exist and AutoCreateStore is true. /// Opens the store and ensures all required keys exist. /// private async Task InitializeSecureStoreAsync() { if (_appSettings?.SecureStore == null) return; if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected") return; var secureStoreConfig = _appSettings.SecureStore; // Resolve paths relative to config folder var storePath = Path.IsPathRooted(secureStoreConfig.StorePath) ? secureStoreConfig.StorePath : Path.Combine(ConfigFolderPath, secureStoreConfig.StorePath); var keyFilePath = Path.IsPathRooted(secureStoreConfig.KeyFilePath) ? secureStoreConfig.KeyFilePath : Path.Combine(ConfigFolderPath, secureStoreConfig.KeyFilePath); try { if (!File.Exists(storePath)) { if (!secureStoreConfig.AutoCreateStore) { _logger?.LogWarning("SecureStore not found and AutoCreateStore is false"); return; } // Create new store with keyfile _logger?.LogInformation("Creating SecureStore at {StorePath}", storePath); _secureStoreManager.CreateStore(storePath, keyFilePath); // Update appsettings.json with actual paths secureStoreConfig.StorePath = GetRelativePath(ConfigFolderPath, storePath); secureStoreConfig.KeyFilePath = GetRelativePath(ConfigFolderPath, keyFilePath); await SaveAppSettingsAsync(); } else { // Open existing store if (!File.Exists(keyFilePath)) { if (_dialogService != null) { await _dialogService.ShowMessageAsync( "SecureStore Error", $"Key file not found: {keyFilePath}\n\nThe SecureStore cannot be opened without its key file."); } return; } _secureStoreManager.OpenStore(storePath, keyFilePath); } // Ensure all required entries exist (both RequiredKeys and connection strings) var connectionStringNames = _appSettings?.ConnectionStrings?.Entries .Select(e => e.Name) .Where(n => !string.IsNullOrEmpty(n)) ?? Enumerable.Empty(); var requiredKeys = secureStoreConfig.RequiredKeys ?? new List(); var addedKeys = _secureStoreManager.EnsureAllRequiredEntries(requiredKeys, connectionStringNames); if (addedKeys.Count > 0) { _logger?.LogInformation("Added {Count} missing required SecureStore entries", addedKeys.Count); } } catch (Exception ex) { _logger?.LogError(ex, "Failed to initialize SecureStore"); if (_dialogService != null) { await _dialogService.ShowMessageAsync( "SecureStore Error", $"Failed to initialize SecureStore:\n\n{ex.Message}"); } } } /// /// Gets the relative path from a base path to a full path. /// private static string GetRelativePath(string basePath, string fullPath) { var baseUri = new Uri(basePath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar); var fullUri = new Uri(fullPath); return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fullUri).ToString().Replace('/', Path.DirectorySeparatorChar)); } /// /// Saves the appsettings.json file. /// private async Task SaveAppSettingsAsync() { if (_appSettings == null) return; var appSettingsPath = Path.Combine(ConfigFolderPath, "appsettings.json"); await _configFileService.SaveAppSettingsAsync(appSettingsPath, _appSettings); } /// /// Opens a file picker dialog to select a configuration file. /// private async Task OpenFolderAsync() { if (_dialogService == null) { _logger?.LogWarning("Dialog service is not available"); return; } var filePath = await _dialogService.ShowFilePickerAsync("Select Configuration File"); if (filePath != null) { var folder = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(folder)) { await LoadConfigAsync(folder); } } } /// /// Loads configuration files from the specified folder. /// /// Path to the configuration folder. private async Task LoadConfigAsync(string folderPath) { try { ConfigFolderPath = folderPath; var appSettingsPath = Path.Combine(folderPath, "appsettings.json"); _appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath); // Load pipelines from directory containing pipeline.*.json files var pipelinesDirectory = Path.Combine(folderPath, _appSettings?.Pipelines?.ConfigDirectory ?? "Pipelines"); if (Directory.Exists(pipelinesDirectory)) { _pipelines = await _configFileService.LoadAllPipelinesAsync(pipelinesDirectory); } else { _pipelines = new Dictionary(StringComparer.OrdinalIgnoreCase); } // Initialize SecureStore (auto-create if needed, open, sync required keys) await InitializeSecureStoreAsync(); BuildTreeNodes(); Validate(); _logger?.LogInformation("Loaded configuration from {Path}", folderPath); } catch (Exception ex) { _logger?.LogError(ex, "Failed to load configuration from {Path}", folderPath); } } /// /// Builds the tree nodes representing the configuration structure. /// private void BuildTreeNodes() { TreeNodes.Clear(); // Settings folder var settingsFolder = new TreeNodeViewModel("Settings", "⚙️", TreeNodeType.Folder) { IsExpanded = true }; settingsFolder.Children.Add(new TreeNodeViewModel("DataSync", "🔄", TreeNodeType.SettingsSection) { SectionKey = "DataSync" }); settingsFolder.Children.Add(new TreeNodeViewModel("DataAccess", "🗄️", TreeNodeType.SettingsSection) { SectionKey = "DataAccess" }); settingsFolder.Children.Add(new TreeNodeViewModel("Auth", "🔐", TreeNodeType.SettingsSection) { SectionKey = "Auth" }); settingsFolder.Children.Add(new TreeNodeViewModel("Ldap", "👥", TreeNodeType.SettingsSection) { SectionKey = "Ldap" }); settingsFolder.Children.Add(new TreeNodeViewModel("Search", "🔍", TreeNodeType.SettingsSection) { SectionKey = "Search" }); settingsFolder.Children.Add(new TreeNodeViewModel("ExcelExport", "📊", TreeNodeType.SettingsSection) { SectionKey = "ExcelExport" }); settingsFolder.Children.Add(new TreeNodeViewModel("ConnectionStrings", "🔗", TreeNodeType.SettingsSection) { SectionKey = "ConnectionStrings" }); TreeNodes.Add(settingsFolder); // Pipelines folder var pipelinesFolder = new TreeNodeViewModel("Pipelines", "⚡", TreeNodeType.Folder) { IsExpanded = true }; if (_pipelines != null) { foreach (var name in _pipelines.Keys.OrderBy(k => k)) { pipelinesFolder.Children.Add(new TreeNodeViewModel(name, "📦", TreeNodeType.Pipeline) { SectionKey = name }); } } TreeNodes.Add(pipelinesFolder); // Secure Store node (single store, secrets as direct children) var secureStoreNode = CreateSecureStoreNode(); if (secureStoreNode != null) { TreeNodes.Add(secureStoreNode); } } /// /// Creates the SecureStore tree node with secrets as direct children. /// private TreeNodeViewModel? CreateSecureStoreNode() { if (_appSettings?.SecureStore == null) return null; if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected") return null; var storePath = Path.IsPathRooted(_appSettings.SecureStore.StorePath) ? _appSettings.SecureStore.StorePath : Path.Combine(ConfigFolderPath, _appSettings.SecureStore.StorePath); var keyFilePath = Path.IsPathRooted(_appSettings.SecureStore.KeyFilePath) ? _appSettings.SecureStore.KeyFilePath : Path.Combine(ConfigFolderPath, _appSettings.SecureStore.KeyFilePath); var storeNode = new TreeNodeViewModel("Secure Store", "🔑", TreeNodeType.SecureStore) { StorePath = storePath, KeyFilePath = keyFilePath, SectionKey = storePath, IsExpanded = true }; // Add secrets as direct children if store is open if (_secureStoreManager.IsStoreOpen) { foreach (var key in _secureStoreManager.GetKeys().OrderBy(k => k)) { var secretNode = new TreeNodeViewModel(key, "🔐", TreeNodeType.Secret) { SecretKey = key }; storeNode.Children.Add(secretNode); } } return storeNode; } /// /// Called when the selected tree node changes; updates the form view model. /// private void OnSelectedNodeChanged() { RaisePipelineCommandsCanExecuteChanged(); if (_selectedNode == null) { SelectedFormViewModel = null; return; } // Handle SecureStore-related node types first switch (_selectedNode.NodeType) { case TreeNodeType.SecureStore: SelectedFormViewModel = CreateSecureStoreInfoFormViewModel(); RaiseSecureStoreCommandsCanExecuteChanged(); return; case TreeNodeType.Secret: SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode); RaiseSecureStoreCommandsCanExecuteChanged(); return; } // Handle standard configuration sections if (_appSettings == null) { SelectedFormViewModel = null; return; } SelectedFormViewModel = _selectedNode.SectionKey switch { "DataSync" => new DataSyncFormViewModel(_appSettings.DataSync, MarkAsChanged), "DataAccess" => new DataAccessFormViewModel(_appSettings.DataAccess, MarkAsChanged), "Auth" => new AuthFormViewModel(_appSettings.Auth, MarkAsChanged), "Ldap" => new LdapFormViewModel(_appSettings.Ldap, MarkAsChanged), "Search" => new SearchFormViewModel(_appSettings.Search, MarkAsChanged), "ExcelExport" => new ExcelExportFormViewModel(_appSettings.ExcelExport, MarkAsChanged), "ConnectionStrings" when _dialogService != null => new ConnectionStringsFormViewModel( _appSettings.ConnectionStrings, _secureStoreManager, MarkAsChanged, _dialogService, _connectionTestService), _ when _selectedNode.NodeType == TreeNodeType.Pipeline && _pipelines != null && _dialogService != null => _pipelines.TryGetValue(_selectedNode.SectionKey!, out var pipeline) ? new PipelineEditorViewModel(_selectedNode.SectionKey!, pipeline, GetAvailableConnections(), _dialogService, MarkAsChanged) : null, _ => null }; RaiseSecureStoreCommandsCanExecuteChanged(); } /// /// Creates a form view model for the SecureStore info panel. /// private SecureStoreInfoFormViewModel CreateSecureStoreInfoFormViewModel() { var storePath = _appSettings?.SecureStore?.StorePath ?? "Unknown"; var keyFilePath = _appSettings?.SecureStore?.KeyFilePath ?? "Unknown"; var secretCount = _secureStoreManager.IsStoreOpen ? _secureStoreManager.GetKeys().Count : 0; return new SecureStoreInfoFormViewModel(storePath, keyFilePath, secretCount); } /// /// Creates a form view model for a secret. /// private SecretFormViewModel? CreateSecretFormViewModel(TreeNodeViewModel secretNode) { if (string.IsNullOrEmpty(secretNode.SecretKey)) return null; try { var value = _secureStoreManager.GetSecret(secretNode.SecretKey); return new SecretFormViewModel( secretNode.SecretKey, value, _clipboardService, newValue => OnSecretValueChanged(secretNode.SecretKey, newValue), () => _ = DeleteSecretAsync()); } catch (Exception ex) { _logger?.LogError(ex, "Failed to get secret value for key {Key}", secretNode.SecretKey); return null; } } /// /// Called when a secret value changes in the form. /// private void OnSecretValueChanged(string key, string newValue) { try { _secureStoreManager.SetSecret(key, newValue); _logger?.LogDebug("Secret {Key} value updated", key); } catch (Exception ex) { _logger?.LogError(ex, "Failed to update secret {Key}", key); } } /// /// Raises CanExecuteChanged for all SecureStore commands. /// private void RaiseSecureStoreCommandsCanExecuteChanged() { (SaveStoreCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); (AddSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); (DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); } /// /// Marks the configuration as having unsaved changes. /// private void MarkAsChanged() { HasUnsavedChanges = true; if (_selectedNode != null) _selectedNode.IsModified = true; } /// /// Loads configuration for testing purposes. /// /// The application settings configuration model. /// The pipelines dictionary. public void LoadConfigForTesting(ConfigModel? appSettings, Dictionary? pipelines) { _appSettings = appSettings; _pipelines = pipelines; BuildTreeNodes(); } /// /// Saves all configuration changes to disk. /// private async Task SaveAsync() { if (_appSettings == null) return; try { var appSettingsPath = Path.Combine(ConfigFolderPath, "appsettings.json"); // Create backup before saving if (File.Exists(appSettingsPath)) { await _backupService.CreateBackupAsync(appSettingsPath); } // Save appsettings await _configFileService.SaveAppSettingsAsync(appSettingsPath, _appSettings); // Save each pipeline to its own file if (_pipelines != null) { var pipelinesDirectory = Path.Combine(ConfigFolderPath, _appSettings?.Pipelines?.ConfigDirectory ?? "Pipelines"); Directory.CreateDirectory(pipelinesDirectory); foreach (var (name, pipeline) in _pipelines) { var filePath = Path.Combine(pipelinesDirectory, $"pipeline.{name}.json"); await _configFileService.SavePipelineAsync(filePath, pipeline); } } HasUnsavedChanges = false; _logger?.LogInformation("Configuration saved"); } catch (Exception ex) { _logger?.LogError(ex, "Failed to save configuration"); } } /// /// Validates the current configuration and updates the status bar. /// private void Validate() { var errors = 0; var warnings = 0; if (_appSettings != null) { var result = _validationService.ValidateAppSettings(_appSettings); errors += result.Errors.Count; warnings += result.Warnings.Count; } if (_pipelines != null) { var result = _validationService.ValidatePipelines(_pipelines); errors += result.Errors.Count; warnings += result.Warnings.Count; } if (errors > 0) { ValidationStatus = $"Errors: {errors}, Warnings: {warnings}"; ValidationStatusColor = new SolidColorBrush(Color.Parse("#FF6B6B")); } else if (warnings > 0) { ValidationStatus = $"Warnings: {warnings}"; ValidationStatusColor = new SolidColorBrush(Color.Parse("#FFB84D")); } else { ValidationStatus = "Valid"; ValidationStatusColor = new SolidColorBrush(Color.Parse("#3DD68C")); } } /// /// Validates runtime configuration using Infrastructure validators (SecureStore, Connection Strings, LDAP). /// private async Task ValidateRuntimeConfigAsync() { if (_dialogService == null || ConfigFolderPath == "No folder selected") return; var results = _runtimeValidationService.ValidateRuntimeConfig(ConfigFolderPath); // Convert to ValidationResult for dialog var appSettingsResult = new ValidationResult(); var pipelinesResult = new ValidationResult(); // Will remain empty for runtime validation foreach (var result in results) { foreach (var error in result.Errors) appSettingsResult.AddError($"[{result.ValidatorName}] {error}"); foreach (var warning in result.Warnings) appSettingsResult.AddWarning($"[{result.ValidatorName}] {warning}"); } await _dialogService.ShowValidationResultsAsync(appSettingsResult, pipelinesResult); } /// /// Tests database connections defined in the configuration. /// private async Task TestConnectionAsync() { // TODO: Implement connection testing _logger?.LogInformation("Test connection requested"); await Task.CompletedTask; } /// /// Gets the list of available connection names from the configuration. /// private IReadOnlyList GetAvailableConnections() { // Return well-known connection names for the JDE Scoping Tool // These match the connection string names in appsettings.json return new List { "jde", "cms", "giw", "lotfinder" }.AsReadOnly(); } #region Pipeline Commands /// /// Determines whether a new pipeline can be added. /// private bool CanAddPipeline() { return _pipelines != null; } /// /// Determines whether the selected pipeline can be deleted. /// private bool CanDeletePipeline() { return _selectedNode?.NodeType == TreeNodeType.Pipeline && _pipelines != null; } /// /// Adds a new pipeline to the configuration. /// private async Task AddPipelineAsync() { if (_pipelines == null || _dialogService == null) return; // Get pipeline name from user var name = await _dialogService.ShowInputDialogAsync( "New Pipeline", "Enter pipeline name:"); if (string.IsNullOrWhiteSpace(name)) return; // Check for duplicate if (_pipelines.ContainsKey(name)) { await _dialogService.ShowMessageAsync("Error", $"Pipeline '{name}' already exists."); return; } // Create default pipeline using EtlPipelineConfig var pipeline = new EtlPipelineConfig { Name = name, IsEnabled = true, Source = new SourceElement { Connection = "lotfinder", Query = "" }, Destination = new DestinationElement { Table = name } }; _pipelines[name] = pipeline; // Add tree node var pipelinesFolder = TreeNodes.FirstOrDefault(n => n.Name == "Pipelines" && n.NodeType == TreeNodeType.Folder); if (pipelinesFolder != null) { var node = new TreeNodeViewModel(name, "📦", TreeNodeType.Pipeline) { SectionKey = name }; pipelinesFolder.Children.Add(node); SelectedNode = node; } MarkAsChanged(); RaisePipelineCommandsCanExecuteChanged(); _logger?.LogInformation("Pipeline created: {Name}", name); } /// /// Deletes the selected pipeline from the configuration. /// private async Task DeletePipelineAsync() { if (_selectedNode?.NodeType != TreeNodeType.Pipeline || _pipelines == null || _dialogService == null || _appSettings == null) return; var name = _selectedNode.SectionKey!; var confirmed = await _dialogService.ShowConfirmationAsync( "Delete Pipeline", $"Are you sure you want to delete pipeline '{name}'?"); if (!confirmed) return; // Remove from model _pipelines.Remove(name); // Delete the pipeline file var pipelinesDirectory = Path.Combine(ConfigFolderPath, _appSettings.Pipelines?.ConfigDirectory ?? "Pipelines"); var filePath = Path.Combine(pipelinesDirectory, $"pipeline.{name}.json"); if (File.Exists(filePath)) { await _configFileService.DeletePipelineFileAsync(filePath); } // Remove tree node var pipelinesFolder = TreeNodes.FirstOrDefault(n => n.Name == "Pipelines" && n.NodeType == TreeNodeType.Folder); pipelinesFolder?.Children.Remove(_selectedNode); // Clear selection SelectedNode = pipelinesFolder; SelectedFormViewModel = null; MarkAsChanged(); RaisePipelineCommandsCanExecuteChanged(); _logger?.LogInformation("Pipeline deleted: {Name}", name); } /// /// Raises CanExecuteChanged for all Pipeline commands. /// private void RaisePipelineCommandsCanExecuteChanged() { (AddPipelineCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); (DeletePipelineCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged(); } #endregion #region SecureStore Commands /// /// Determines whether the current store can be saved. /// private bool CanSaveStore() { return _secureStoreManager.IsStoreOpen && _secureStoreManager.HasUnsavedChanges; } /// /// Determines whether a secret can be added. /// private bool CanAddSecret() { return _secureStoreManager.IsStoreOpen; } /// /// Determines whether a secret can be deleted. /// private bool CanDeleteSecret() { return _selectedNode != null && _selectedNode.NodeType == TreeNodeType.Secret && _secureStoreManager.IsStoreOpen; } /// /// Creates a new secure store. /// private async Task NewStoreAsync() { if (_dialogService == null) { _logger?.LogWarning("Dialog service is not available"); return; } // Note: In a full implementation, this would show the NewStoreDialog // For now, we use a simple message dialog to inform the user // A proper implementation would wire up the NewStoreDialogViewModel events await _dialogService.ShowMessageAsync( "New Store", "New store creation dialog not yet implemented. Use Add Existing Store instead."); } /// /// Adds an existing secure store to the tree. /// private async Task AddExistingStoreAsync() { if (_dialogService == null) { _logger?.LogWarning("Dialog service is not available"); return; } // Note: In a full implementation, this would show a file picker await _dialogService.ShowMessageAsync( "Add Existing Store", "File picker for existing stores not yet implemented."); } /// /// Saves the currently open secure store. /// private async Task SaveStoreAsync() { if (!_secureStoreManager.IsStoreOpen) return; try { _secureStoreManager.Save(); // Refresh the form to update the HasUnsavedChanges state OnSelectedNodeChanged(); RaiseSecureStoreCommandsCanExecuteChanged(); _logger?.LogInformation("Store saved: {StorePath}", _secureStoreManager.CurrentStorePath); } catch (Exception ex) { _logger?.LogError(ex, "Failed to save store"); if (_dialogService != null) { await _dialogService.ShowMessageAsync( SecureStoreStrings.ErrorTitle, string.Format(SecureStoreStrings.FailedToSaveStoreFormat, ex.Message)); } } } /// /// Generates a new key file. /// private async Task GenerateKeyFileAsync() { if (_dialogService == null) { _logger?.LogWarning("Dialog service is not available"); return; } // Note: In a full implementation, this would show a save file dialog await _dialogService.ShowMessageAsync( SecureStoreStrings.GenerateKeyFileTitle, "Key file generation dialog not yet implemented."); } /// /// Adds a new secret to the current store. /// private async Task AddSecretAsync() { if (!_secureStoreManager.IsStoreOpen) return; if (_dialogService == null) { _logger?.LogWarning("Dialog service is not available"); return; } // Note: In a full implementation, this would show the SecretEditDialog // For demonstration, we'll show a message await _dialogService.ShowMessageAsync( "Add Secret", "Secret edit dialog not yet implemented.\n\nTo add secrets, the SecretEditDialog must be wired up."); } /// /// Deletes the currently selected secret. /// private async Task DeleteSecretAsync() { if (_selectedNode == null || _selectedNode.NodeType != TreeNodeType.Secret || string.IsNullOrEmpty(_selectedNode.SecretKey)) return; if (_dialogService == null) { _logger?.LogWarning("Dialog service is not available"); return; } var confirmed = await _dialogService.ShowConfirmationAsync( SecureStoreStrings.ConfirmDeleteTitle, string.Format(SecureStoreStrings.ConfirmDeleteFormat, _selectedNode.SecretKey)); if (!confirmed) return; try { _secureStoreManager.RemoveSecret(_selectedNode.SecretKey); // Find the Secure Store node and remove the secret from its children var secureStoreNode = TreeNodes.FirstOrDefault(n => n.NodeType == TreeNodeType.SecureStore); if (secureStoreNode != null) { secureStoreNode.Children.Remove(_selectedNode); // Select the parent store node SelectedNode = secureStoreNode; } _logger?.LogInformation("Secret deleted: {Key}", _selectedNode.SecretKey); } catch (Exception ex) { _logger?.LogError(ex, "Failed to delete secret: {Key}", _selectedNode.SecretKey); await _dialogService.ShowMessageAsync( SecureStoreStrings.ErrorTitle, string.Format(SecureStoreStrings.FailedToDeleteSecretFormat, ex.Message)); } } /// /// Refreshes the secret children of a store node. /// private void RefreshStoreChildren(TreeNodeViewModel storeNode) { storeNode.Children.Clear(); if (!_secureStoreManager.IsStoreOpen) return; try { var keys = _secureStoreManager.GetKeys(); foreach (var key in keys.OrderBy(k => k)) { var secretNode = new TreeNodeViewModel(key, "🔐", TreeNodeType.Secret) { SecretKey = key, SectionKey = key }; storeNode.Children.Add(secretNode); } _logger?.LogDebug("Refreshed {Count} secrets for store", keys.Count); } catch (Exception ex) { _logger?.LogError(ex, "Failed to refresh store children"); } } #endregion }