docs: add XML documentation and ConfigManager implementation plans
Add comprehensive XML documentation (param/returns tags) across 132 source files to improve IntelliSense and API discoverability. Include ConfigManager design documents and implementation plans for phases 1-9.
This commit is contained in:
@@ -7,69 +7,226 @@ namespace JdeScoping.ConfigManager.Models;
|
||||
/// </summary>
|
||||
public class ConfigModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the data synchronization configuration.
|
||||
/// </summary>
|
||||
public DataSyncSection DataSync { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data access configuration.
|
||||
/// </summary>
|
||||
public DataAccessSection DataAccess { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication configuration.
|
||||
/// </summary>
|
||||
public AuthSection Auth { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP directory configuration.
|
||||
/// </summary>
|
||||
public LdapSection Ldap { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the search processing configuration.
|
||||
/// </summary>
|
||||
public SearchSection Search { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Excel export configuration.
|
||||
/// </summary>
|
||||
public ExcelExportSection ExcelExport { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection strings for external data sources.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ConnectionStrings { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DataSyncSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the interval between successive data sync checks.
|
||||
/// </summary>
|
||||
public TimeSpan CheckInterval { get; set; } = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum degree of parallelism for sync operations.
|
||||
/// </summary>
|
||||
public int MaxDegreeOfParallelism { get; set; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the batch size for data sync operations.
|
||||
/// </summary>
|
||||
public int BatchSize { get; set; } = 50000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the batch size for bulk copy operations.
|
||||
/// </summary>
|
||||
public int BulkCopyBatchSize { get; set; } = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lookback multiplier for data sync delta calculations.
|
||||
/// </summary>
|
||||
public double LookbackMultiplier { get; set; } = 1.5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of days to retain synced data before purging.
|
||||
/// </summary>
|
||||
public int PurgeRetentionDays { get; set; } = 90;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in seconds for sync operations.
|
||||
/// </summary>
|
||||
public int SyncTimeoutSeconds { get; set; } = 3600;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether data synchronization is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public class DataAccessSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default timeout in seconds for database queries.
|
||||
/// </summary>
|
||||
public int DefaultTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in seconds for lot usage queries.
|
||||
/// </summary>
|
||||
public int LotUsageTimeoutSeconds { get; set; } = 120;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in seconds for MIS data queries.
|
||||
/// </summary>
|
||||
public int MisDataTimeoutSeconds { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the schema name for production data.
|
||||
/// </summary>
|
||||
public string ProductionSchema { get; set; } = "prod";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the schema name for archive data.
|
||||
/// </summary>
|
||||
public string ArchiveSchema { get; set; } = "archive";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the schema name for staging data.
|
||||
/// </summary>
|
||||
public string StageSchema { get; set; } = "stage";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether detailed query logging is enabled.
|
||||
/// </summary>
|
||||
public bool EnableDetailedLogging { get; set; } = false;
|
||||
}
|
||||
|
||||
public class AuthSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the authentication cookie.
|
||||
/// </summary>
|
||||
public string CookieName { get; set; } = ".JdeScoping.Auth";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cookie expiration time in minutes.
|
||||
/// </summary>
|
||||
public int CookieExpirationMinutes { get; set; } = 480;
|
||||
}
|
||||
|
||||
public class LdapSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP server URLs to connect to.
|
||||
/// </summary>
|
||||
public string[] ServerUrls { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the distinguished name of the LDAP group for authorization.
|
||||
/// </summary>
|
||||
public string GroupDn { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base distinguished name for LDAP searches.
|
||||
/// </summary>
|
||||
public string SearchBase { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection timeout in seconds for LDAP operations.
|
||||
/// </summary>
|
||||
public int ConnectionTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use fake authentication instead of LDAP.
|
||||
/// </summary>
|
||||
public bool UseFakeAuth { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an array of user names that bypass group membership validation.
|
||||
/// </summary>
|
||||
public string[] AdminBypassUsers { get; set; } = [];
|
||||
}
|
||||
|
||||
public class SearchSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of result rows returned by a search.
|
||||
/// </summary>
|
||||
public int MaxResultRows { get; set; } = 100000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in seconds for search operations.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of concurrent search operations allowed.
|
||||
/// </summary>
|
||||
public int MaxConcurrentSearches { get; set; } = 5;
|
||||
}
|
||||
|
||||
public class ExcelExportSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the password for protecting the criteria worksheet.
|
||||
/// </summary>
|
||||
public string CriteriaSheetPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for protecting the data worksheet.
|
||||
/// </summary>
|
||||
public string DataSheetPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of rows per Excel worksheet.
|
||||
/// </summary>
|
||||
public int MaxRowsPerSheet { get; set; } = 1000000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default date format for Excel exports.
|
||||
/// </summary>
|
||||
public string DefaultDateFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to write debug output to files.
|
||||
/// </summary>
|
||||
public bool DebugWriteToFile { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the directory path for debug output files.
|
||||
/// </summary>
|
||||
public string DebugOutputDirectory { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time zone identifier for date/time conversions.
|
||||
/// </summary>
|
||||
public string TimezoneId { get; set; } = "America/Chicago";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time zone abbreviation for display purposes.
|
||||
/// </summary>
|
||||
public string TimezoneAbbreviation { get; set; } = "CT";
|
||||
}
|
||||
|
||||
@@ -5,56 +5,144 @@ namespace JdeScoping.ConfigManager.Models;
|
||||
/// </summary>
|
||||
public class PipelinesConfigModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the pipeline settings.
|
||||
/// </summary>
|
||||
public PipelineSettings Settings { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default schedules for all pipelines.
|
||||
/// </summary>
|
||||
public ScheduleDefaults ScheduleDefaults { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of named pipelines.
|
||||
/// </summary>
|
||||
public Dictionary<string, PipelineModel> Pipelines { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PipelineSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the timezone for scheduling operations.
|
||||
/// </summary>
|
||||
public string Timezone { get; set; } = "UTC";
|
||||
}
|
||||
|
||||
public class ScheduleDefaults
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default mass data refresh schedule.
|
||||
/// </summary>
|
||||
public ScheduleModel Mass { get; set; } = new() { Enabled = true, IntervalMinutes = 10080, PrePurge = true, ReIndex = true };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default daily data refresh schedule.
|
||||
/// </summary>
|
||||
public ScheduleModel Daily { get; set; } = new() { Enabled = true, IntervalMinutes = 1440 };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default hourly data refresh schedule.
|
||||
/// </summary>
|
||||
public ScheduleModel Hourly { get; set; } = new() { Enabled = true, IntervalMinutes = 60 };
|
||||
}
|
||||
|
||||
public class PipelineModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the source configuration for data extraction.
|
||||
/// </summary>
|
||||
public PipelineSource Source { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the schedule configurations for this pipeline.
|
||||
/// </summary>
|
||||
public PipelineSchedules Schedules { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the destination configuration for data loading.
|
||||
/// </summary>
|
||||
public PipelineDestination Destination { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional scripts to execute after pipeline completion.
|
||||
/// </summary>
|
||||
public string[]? PostScripts { get; set; }
|
||||
}
|
||||
|
||||
public class PipelineSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the source database connection name.
|
||||
/// </summary>
|
||||
public string Connection { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query to extract data from the source.
|
||||
/// </summary>
|
||||
public string Query { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional mass query for full data extraction.
|
||||
/// </summary>
|
||||
public string? MassQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query parameters and their definitions.
|
||||
/// </summary>
|
||||
public Dictionary<string, ParameterDefinition> Parameters { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ParameterDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the parameter name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional parameter format string.
|
||||
/// </summary>
|
||||
public string? Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional parameter source or derivation logic.
|
||||
/// </summary>
|
||||
public string? Source { get; set; }
|
||||
}
|
||||
|
||||
public class PipelineSchedules
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the mass refresh schedule for this pipeline.
|
||||
/// </summary>
|
||||
public ScheduleModel? Mass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the daily refresh schedule for this pipeline.
|
||||
/// </summary>
|
||||
public ScheduleModel? Daily { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hourly refresh schedule for this pipeline.
|
||||
/// </summary>
|
||||
public ScheduleModel? Hourly { get; set; }
|
||||
}
|
||||
|
||||
public class PipelineDestination
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the destination table name.
|
||||
/// </summary>
|
||||
public string Table { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the columns used to match existing records for updates.
|
||||
/// </summary>
|
||||
public string[] MatchColumns { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the columns to exclude from update operations.
|
||||
/// </summary>
|
||||
public string[] ExcludeFromUpdate { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -5,8 +5,23 @@ namespace JdeScoping.ConfigManager.Models;
|
||||
/// </summary>
|
||||
public class ScheduleModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the scheduled task is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in minutes between scheduled task executions.
|
||||
/// </summary>
|
||||
public int IntervalMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to purge data before task execution.
|
||||
/// </summary>
|
||||
public bool PrePurge { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to reindex after task execution.
|
||||
/// </summary>
|
||||
public bool ReIndex { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,18 @@ namespace JdeScoping.ConfigManager;
|
||||
|
||||
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()
|
||||
|
||||
@@ -17,12 +17,22 @@ public class AutoDiscoveryService : IAutoDiscoveryService
|
||||
private const string EnvVarName = "JDESCOPING_CONFIG_PATH";
|
||||
private const string AppSettingsFileName = "appsettings.json";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AutoDiscoveryService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">The file system abstraction to use for directory and file checks.</param>
|
||||
/// <param name="logger">Optional logger for recording discovery process information.</param>
|
||||
public AutoDiscoveryService(IFileSystem fileSystem, ILogger<AutoDiscoveryService>? logger = null)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the configuration folder using a prioritized search strategy.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token for the async operation.</param>
|
||||
/// <returns>The path to a valid configuration folder, or null if none is found.</returns>
|
||||
public Task<string?> FindConfigFolderAsync(CancellationToken ct = default)
|
||||
{
|
||||
// 1. Check environment variable
|
||||
|
||||
@@ -12,12 +12,24 @@ public class BackupService : IBackupService
|
||||
private readonly ILogger<BackupService>? _logger;
|
||||
private const string TimestampFormat = "yyyy-MM-dd_HHmmss";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackupService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">The file system abstraction for file operations.</param>
|
||||
/// <param name="logger">Optional logger for diagnostic messages.</param>
|
||||
public BackupService(IFileSystem fileSystem, ILogger<BackupService>? logger = null)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a backup copy of the specified file with a timestamp suffix.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to backup.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The path of the created backup file.</returns>
|
||||
/// <exception cref="FileNotFoundException">Thrown when the source file does not exist.</exception>
|
||||
public async Task<string> CreateBackupAsync(string filePath, CancellationToken ct = default)
|
||||
{
|
||||
if (!_fileSystem.FileExists(filePath))
|
||||
@@ -34,6 +46,12 @@ public class BackupService : IBackupService
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of existing backups for the specified file, sorted by timestamp descending.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The original file path to find backups for.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A read-only list of backup information sorted by timestamp.</returns>
|
||||
public async Task<IReadOnlyList<BackupInfo>> GetBackupsAsync(string filePath, CancellationToken ct = default)
|
||||
{
|
||||
var directory = _fileSystem.GetDirectoryName(filePath);
|
||||
@@ -59,12 +77,24 @@ public class BackupService : IBackupService
|
||||
return backups.OrderByDescending(b => b.Timestamp).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a backup file by copying it to the target location.
|
||||
/// </summary>
|
||||
/// <param name="backupPath">The path of the backup file to restore from.</param>
|
||||
/// <param name="targetPath">The target path where the backup should be restored.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
public async Task RestoreBackupAsync(string backupPath, string targetPath, CancellationToken ct = default)
|
||||
{
|
||||
_logger?.LogInformation("Restoring backup from {BackupPath} to {TargetPath}", backupPath, targetPath);
|
||||
await _fileSystem.CopyFileAsync(backupPath, targetPath, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes old backup files, keeping only the most recent backups.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The original file path to find backups for.</param>
|
||||
/// <param name="keepCount">The number of most recent backups to retain (default: 10).</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
public async Task CleanupOldBackupsAsync(string filePath, int keepCount = 10, CancellationToken ct = default)
|
||||
{
|
||||
var backups = await GetBackupsAsync(filePath, ct);
|
||||
|
||||
@@ -21,12 +21,24 @@ public class ConfigFileService : IConfigFileService
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigFileService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">The file system abstraction for file operations.</param>
|
||||
/// <param name="logger">Optional logger for diagnostic messages.</param>
|
||||
public ConfigFileService(IFileSystem fileSystem, ILogger<ConfigFileService>? logger = null)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the application settings configuration from the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to load appsettings from.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The loaded configuration model or a new empty model if deserialization fails.</returns>
|
||||
/// <exception cref="ConfigLoadException">Thrown when the JSON cannot be parsed.</exception>
|
||||
public async Task<ConfigModel> LoadAppSettingsAsync(string path, CancellationToken ct = default)
|
||||
{
|
||||
_logger?.LogInformation("Loading appsettings from {Path}", path);
|
||||
@@ -43,6 +55,13 @@ public class ConfigFileService : IConfigFileService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the pipelines configuration from the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to load pipelines from.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The loaded pipelines configuration model or a new empty model if deserialization fails.</returns>
|
||||
/// <exception cref="ConfigLoadException">Thrown when the JSON cannot be parsed.</exception>
|
||||
public async Task<PipelinesConfigModel> LoadPipelinesAsync(string path, CancellationToken ct = default)
|
||||
{
|
||||
_logger?.LogInformation("Loading pipelines from {Path}", path);
|
||||
@@ -59,6 +78,12 @@ public class ConfigFileService : IConfigFileService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the application settings configuration to the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to save appsettings to.</param>
|
||||
/// <param name="config">The configuration model to save.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
public async Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default)
|
||||
{
|
||||
_logger?.LogInformation("Saving appsettings to {Path}", path);
|
||||
@@ -66,6 +91,12 @@ public class ConfigFileService : IConfigFileService
|
||||
await _fileSystem.WriteAllTextAsync(path, json, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the pipelines configuration to the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to save pipelines to.</param>
|
||||
/// <param name="config">The pipelines configuration model to save.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
public async Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default)
|
||||
{
|
||||
_logger?.LogInformation("Saving pipelines to {Path}", path);
|
||||
|
||||
@@ -5,8 +5,17 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public class ConfigLoadException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path to the configuration file that failed to load.
|
||||
/// </summary>
|
||||
public string FilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ConfigLoadException class.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the configuration file that failed to load.</param>
|
||||
/// <param name="message">The error message describing the failure.</param>
|
||||
/// <param name="inner">The inner exception that caused this exception, if any.</param>
|
||||
public ConfigLoadException(string filePath, string message, Exception? inner = null)
|
||||
: base(message, inner)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,12 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public class DiffService : IDiffService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a diff between original and modified text content.
|
||||
/// </summary>
|
||||
/// <param name="original">The original text content.</param>
|
||||
/// <param name="modified">The modified text content.</param>
|
||||
/// <returns>A diff result containing added, removed, and unchanged lines with counts.</returns>
|
||||
public DiffResult GenerateDiff(string original, string modified)
|
||||
{
|
||||
var diffBuilder = new InlineDiffBuilder(new Differ());
|
||||
|
||||
@@ -5,36 +5,96 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public class FileSystem : IFileSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified file exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to check.</param>
|
||||
/// <returns>True if the file exists; otherwise, false.</returns>
|
||||
public bool FileExists(string path) => File.Exists(path);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified directory exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The directory path to check.</param>
|
||||
/// <returns>True if the directory exists; otherwise, false.</returns>
|
||||
public bool DirectoryExists(string path) => Directory.Exists(path);
|
||||
|
||||
/// <summary>
|
||||
/// Reads all text from the specified file asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to read from.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The file contents as a string.</returns>
|
||||
public async Task<string> ReadAllTextAsync(string path, CancellationToken ct = default)
|
||||
=> await File.ReadAllTextAsync(path, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Writes all text to the specified file asynchronously, overwriting if it exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to write to.</param>
|
||||
/// <param name="content">The text content to write.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
public async Task WriteAllTextAsync(string path, string content, CancellationToken ct = default)
|
||||
=> await File.WriteAllTextAsync(path, content, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an array of file paths matching the pattern in the specified directory.
|
||||
/// </summary>
|
||||
/// <param name="directory">The directory path to search in.</param>
|
||||
/// <param name="pattern">The search pattern to match files.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>An array of file paths matching the pattern.</returns>
|
||||
public Task<string[]> GetFilesAsync(string directory, string pattern, CancellationToken ct = default)
|
||||
=> Task.FromResult(Directory.GetFiles(directory, pattern));
|
||||
|
||||
/// <summary>
|
||||
/// Copies a file from source to destination asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="source">The source file path.</param>
|
||||
/// <param name="destination">The destination file path.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
public async Task CopyFileAsync(string source, string destination, CancellationToken ct = default)
|
||||
{
|
||||
var content = await File.ReadAllBytesAsync(source, ct);
|
||||
await File.WriteAllBytesAsync(destination, content, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified file asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to delete.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
public Task DeleteFileAsync(string path, CancellationToken ct = default)
|
||||
{
|
||||
File.Delete(path);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the directory name for the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file or directory path.</param>
|
||||
/// <returns>The directory name, or an empty string if not available.</returns>
|
||||
public string GetDirectoryName(string path) => Path.GetDirectoryName(path) ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file name and extension for the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path.</param>
|
||||
/// <returns>The file name with extension.</returns>
|
||||
public string GetFileName(string path) => Path.GetFileName(path);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file name without the extension for the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path.</param>
|
||||
/// <returns>The file name without extension.</returns>
|
||||
public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
/// <summary>
|
||||
/// Combines multiple path components into a single path.
|
||||
/// </summary>
|
||||
/// <param name="paths">The path components to combine.</param>
|
||||
/// <returns>The combined path.</returns>
|
||||
public string Combine(params string[] paths) => Path.Combine(paths);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,19 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public class BackupInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the full path to the backup file.
|
||||
/// </summary>
|
||||
public required string Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the backup was created.
|
||||
/// </summary>
|
||||
public required DateTime Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file size in bytes.
|
||||
/// </summary>
|
||||
public required long Size { get; init; }
|
||||
}
|
||||
|
||||
@@ -15,8 +26,35 @@ public class BackupInfo
|
||||
/// </summary>
|
||||
public interface IBackupService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a backup copy of the specified file with a timestamp suffix.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to backup.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The path of the created backup file.</returns>
|
||||
Task<string> CreateBackupAsync(string filePath, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of existing backups for the specified file, sorted by timestamp descending.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The original file path to find backups for.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A read-only list of backup information sorted by timestamp.</returns>
|
||||
Task<IReadOnlyList<BackupInfo>> GetBackupsAsync(string filePath, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Restores a backup file by copying it to the target location.
|
||||
/// </summary>
|
||||
/// <param name="backupPath">The path of the backup file to restore from.</param>
|
||||
/// <param name="targetPath">The target path where the backup should be restored.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task RestoreBackupAsync(string backupPath, string targetPath, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes old backup files, keeping only the most recent backups.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The original file path to find backups for.</param>
|
||||
/// <param name="keepCount">The number of most recent backups to retain (default: 10).</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task CleanupOldBackupsAsync(string filePath, int keepCount = 10, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,35 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public interface IConfigFileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the application settings configuration from the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to load appsettings from.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The loaded configuration model or a new empty model if deserialization fails.</returns>
|
||||
Task<ConfigModel> LoadAppSettingsAsync(string path, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the pipelines configuration from the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to load pipelines from.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The loaded pipelines configuration model or a new empty model if deserialization fails.</returns>
|
||||
Task<PipelinesConfigModel> LoadPipelinesAsync(string path, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the application settings configuration to the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to save appsettings to.</param>
|
||||
/// <param name="config">The configuration model to save.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the pipelines configuration to the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to save pipelines to.</param>
|
||||
/// <param name="config">The pipelines configuration model to save.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,24 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public class DiffLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the line number in the original text, or null for added lines.
|
||||
/// </summary>
|
||||
public required int? OldLineNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line number in the modified text, or null for removed lines.
|
||||
/// </summary>
|
||||
public required int? NewLineNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text content of the line.
|
||||
/// </summary>
|
||||
public required string Text { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of change for this line (added, removed, or unchanged).
|
||||
/// </summary>
|
||||
public required DiffLineType Type { get; init; }
|
||||
}
|
||||
|
||||
@@ -23,9 +38,24 @@ public enum DiffLineType
|
||||
/// </summary>
|
||||
public class DiffResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the diff contains any changes.
|
||||
/// </summary>
|
||||
public bool HasChanges { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of lines representing the diff result.
|
||||
/// </summary>
|
||||
public List<DiffLine> Lines { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines added in the modification.
|
||||
/// </summary>
|
||||
public int Insertions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines removed in the modification.
|
||||
/// </summary>
|
||||
public int Deletions { get; init; }
|
||||
}
|
||||
|
||||
@@ -34,5 +64,11 @@ public class DiffResult
|
||||
/// </summary>
|
||||
public interface IDiffService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a diff between original and modified text content.
|
||||
/// </summary>
|
||||
/// <param name="original">The original text content.</param>
|
||||
/// <param name="modified">The modified text content.</param>
|
||||
/// <returns>A diff result containing added, removed, and unchanged lines with counts.</returns>
|
||||
DiffResult GenerateDiff(string original, string modified);
|
||||
}
|
||||
|
||||
@@ -5,15 +5,85 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public interface IFileSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified file exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to check.</param>
|
||||
/// <returns>True if the file exists; otherwise, false.</returns>
|
||||
bool FileExists(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified directory exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The directory path to check.</param>
|
||||
/// <returns>True if the directory exists; otherwise, false.</returns>
|
||||
bool DirectoryExists(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Reads all text from the specified file asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to read from.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The file contents as a string.</returns>
|
||||
Task<string> ReadAllTextAsync(string path, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes all text to the specified file asynchronously, overwriting if it exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to write to.</param>
|
||||
/// <param name="content">The text content to write.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task WriteAllTextAsync(string path, string content, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an array of file paths matching the pattern in the specified directory.
|
||||
/// </summary>
|
||||
/// <param name="directory">The directory path to search in.</param>
|
||||
/// <param name="pattern">The search pattern to match files.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>An array of file paths matching the pattern.</returns>
|
||||
Task<string[]> GetFilesAsync(string directory, string pattern, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Copies a file from source to destination asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="source">The source file path.</param>
|
||||
/// <param name="destination">The destination file path.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task CopyFileAsync(string source, string destination, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified file asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to delete.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
Task DeleteFileAsync(string path, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the directory name for the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file or directory path.</param>
|
||||
/// <returns>The directory name, or an empty string if not available.</returns>
|
||||
string GetDirectoryName(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file name and extension for the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path.</param>
|
||||
/// <returns>The file name with extension.</returns>
|
||||
string GetFileName(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file name without the extension for the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path.</param>
|
||||
/// <returns>The file name without extension.</returns>
|
||||
string GetFileNameWithoutExtension(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Combines multiple path components into a single path.
|
||||
/// </summary>
|
||||
/// <param name="paths">The path components to combine.</param>
|
||||
/// <returns>The combined path.</returns>
|
||||
string Combine(params string[] paths);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,31 @@ namespace JdeScoping.ConfigManager.Services;
|
||||
/// </summary>
|
||||
public class ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the validation succeeded (no errors).
|
||||
/// </summary>
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of validation errors encountered.
|
||||
/// </summary>
|
||||
public List<string> Errors { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of validation warnings encountered.
|
||||
/// </summary>
|
||||
public List<string> Warnings { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds an error message to the validation result.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message to add.</param>
|
||||
public void AddError(string message) => Errors.Add(message);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a warning message to the validation result.
|
||||
/// </summary>
|
||||
/// <param name="message">The warning message to add.</param>
|
||||
public void AddWarning(string message) => Warnings.Add(message);
|
||||
}
|
||||
|
||||
@@ -20,6 +40,17 @@ public class ValidationResult
|
||||
/// </summary>
|
||||
public interface IValidationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the application settings configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration model to validate.</param>
|
||||
/// <returns>A validation result containing any errors or warnings found.</returns>
|
||||
ValidationResult ValidateAppSettings(ConfigModel config);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the pipelines configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">The pipelines configuration model to validate.</param>
|
||||
/// <returns>A validation result containing any errors or warnings found.</returns>
|
||||
ValidationResult ValidatePipelines(PipelinesConfigModel config);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ public class ValidationService : IValidationService
|
||||
{
|
||||
private static readonly string[] ValidConnections = ["jde", "cms", "giw", "lotfinderdb"];
|
||||
|
||||
/// <summary>
|
||||
/// Validates the application settings configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration model to validate.</param>
|
||||
/// <returns>A validation result containing any errors or warnings found.</returns>
|
||||
public ValidationResult ValidateAppSettings(ConfigModel config)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
@@ -50,6 +55,11 @@ public class ValidationService : IValidationService
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the pipelines configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">The pipelines configuration model to validate.</param>
|
||||
/// <returns>A validation result containing any errors or warnings found.</returns>
|
||||
public ValidationResult ValidatePipelines(PipelinesConfigModel config)
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
@@ -12,20 +12,38 @@ public class AsyncRelayCommand : ICommand
|
||||
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;
|
||||
@@ -44,5 +62,8 @@ public class AsyncRelayCommand : ICommand
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="CanExecuteChanged"/> event to notify command bindings of state changes.
|
||||
/// </summary>
|
||||
public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -11,26 +11,52 @@ public class RelayCommand : ICommand
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -27,30 +27,61 @@ public class TreeNodeViewModel : ViewModelBase
|
||||
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 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;
|
||||
@@ -61,6 +92,9 @@ public class TreeNodeViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon character representing the validation state.
|
||||
/// </summary>
|
||||
public string StatusIcon => ValidationState switch
|
||||
{
|
||||
ValidationState.Valid => "✓",
|
||||
@@ -69,6 +103,12 @@ public class TreeNodeViewModel : ViewModelBase
|
||||
_ => ""
|
||||
};
|
||||
|
||||
/// <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;
|
||||
|
||||
@@ -8,13 +8,28 @@ namespace JdeScoping.ConfigManager.ViewModels;
|
||||
/// </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))
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace JdeScoping.ConfigManager.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MainWindow.
|
||||
/// </summary>
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -13,13 +13,18 @@ namespace JdeScoping.SecureStoreManager;
|
||||
|
||||
public partial class App : Avalonia.Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the service provider instance for dependency injection.
|
||||
/// </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();
|
||||
|
||||
@@ -11,6 +11,11 @@ public class SecretUseCases
|
||||
private readonly ISecureStoreManager _storeManager;
|
||||
private readonly ILogger<SecretUseCases> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretUseCases"/> class.
|
||||
/// </summary>
|
||||
/// <param name="storeManager">The secure store manager.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public SecretUseCases(ISecureStoreManager storeManager, ILogger<SecretUseCases> logger)
|
||||
{
|
||||
_storeManager = storeManager ?? throw new ArgumentNullException(nameof(storeManager));
|
||||
@@ -20,6 +25,8 @@ public class SecretUseCases
|
||||
/// <summary>
|
||||
/// Sets a secret with the given key and value.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <param name="value">The secret value.</param>
|
||||
public void SetSecret(string key, string value)
|
||||
{
|
||||
_logger.LogInformation("Setting secret {Key}", key);
|
||||
@@ -29,6 +36,7 @@ public class SecretUseCases
|
||||
/// <summary>
|
||||
/// Removes a secret by key.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key to remove.</param>
|
||||
public void RemoveSecret(string key)
|
||||
{
|
||||
_logger.LogInformation("Removing secret {Key}", key);
|
||||
@@ -46,6 +54,8 @@ public class SecretUseCases
|
||||
/// <summary>
|
||||
/// Gets the value of a secret by key.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <returns>The secret value.</returns>
|
||||
public string GetSecret(string key)
|
||||
{
|
||||
return _storeManager.GetSecret(key);
|
||||
|
||||
@@ -11,6 +11,11 @@ public class StoreUseCases
|
||||
private readonly ISecureStoreManager _storeManager;
|
||||
private readonly ILogger<StoreUseCases> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StoreUseCases"/> class.
|
||||
/// </summary>
|
||||
/// <param name="storeManager">The secure store manager instance.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public StoreUseCases(ISecureStoreManager storeManager, ILogger<StoreUseCases> logger)
|
||||
{
|
||||
_storeManager = storeManager ?? throw new ArgumentNullException(nameof(storeManager));
|
||||
@@ -20,6 +25,9 @@ public class StoreUseCases
|
||||
/// <summary>
|
||||
/// Creates a new store with either key file or password authentication.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path where the store will be created.</param>
|
||||
/// <param name="keyFilePath">The path to the key file, or null for password-based authentication.</param>
|
||||
/// <param name="password">The password for authentication, or null for key file-based authentication.</param>
|
||||
public void CreateStore(string storePath, string? keyFilePath, string? password)
|
||||
{
|
||||
_logger.LogInformation("Creating store at {StorePath}", storePath);
|
||||
@@ -43,6 +51,9 @@ public class StoreUseCases
|
||||
/// <summary>
|
||||
/// Opens an existing store with either key file or password authentication.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path to the existing store.</param>
|
||||
/// <param name="keyFilePath">The path to the key file, or null for password-based authentication.</param>
|
||||
/// <param name="password">The password for authentication, or null for key file-based authentication.</param>
|
||||
public void OpenStore(string storePath, string? keyFilePath, string? password)
|
||||
{
|
||||
_logger.LogInformation("Opening store at {StorePath}", storePath);
|
||||
@@ -84,6 +95,7 @@ public class StoreUseCases
|
||||
/// <summary>
|
||||
/// Generates a new key file at the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path where the key file will be generated.</param>
|
||||
public void GenerateKeyFile(string path)
|
||||
{
|
||||
_logger.LogInformation("Generating key file at {Path}", path);
|
||||
@@ -94,6 +106,7 @@ public class StoreUseCases
|
||||
/// <summary>
|
||||
/// Exports the current store's key to a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path where the key will be exported.</param>
|
||||
public void ExportKey(string path)
|
||||
{
|
||||
_logger.LogInformation("Exporting key to {Path}", path);
|
||||
|
||||
@@ -8,6 +8,14 @@ namespace JdeScoping.SecureStoreManager.Converters;
|
||||
/// </summary>
|
||||
public class InverseBooleanConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a boolean value to its inverted counterpart.
|
||||
/// </summary>
|
||||
/// <param name="value">The boolean value to invert.</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>The inverted boolean value, or false if the input is not a boolean.</returns>
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
@@ -17,6 +25,14 @@ public class InverseBooleanConverter : IValueConverter
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a value back to a boolean by inverting it.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to invert.</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>The inverted boolean value, or false if the input is not a boolean.</returns>
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
@@ -32,6 +48,14 @@ public class InverseBooleanConverter : IValueConverter
|
||||
/// </summary>
|
||||
public class BooleanToVisibilityIconConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a boolean to a visibility icon string.
|
||||
/// </summary>
|
||||
/// <param name="value">The boolean value indicating visibility.</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>"Hide" if true, "Show" if false or input is not boolean.</returns>
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool isVisible)
|
||||
@@ -42,6 +66,15 @@ public class BooleanToVisibilityIconConverter : IValueConverter
|
||||
return "Show";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a value back (not implemented for visibility icons).
|
||||
/// </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();
|
||||
@@ -53,11 +86,28 @@ public class BooleanToVisibilityIconConverter : IValueConverter
|
||||
/// </summary>
|
||||
public class NullToBoolConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a value to a boolean based on null status.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check for null.</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 value is not null, false otherwise.</returns>
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return value != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a value back (not implemented for null 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();
|
||||
@@ -69,6 +119,14 @@ public class NullToBoolConverter : IValueConverter
|
||||
/// </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)
|
||||
@@ -78,6 +136,15 @@ public class StringToBoolConverter : IValueConverter
|
||||
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();
|
||||
|
||||
@@ -4,13 +4,17 @@ namespace JdeScoping.SecureStoreManager;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called.
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
/// <param name="args">Command-line arguments.</param>
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
/// <summary>
|
||||
/// Builds the Avalonia application configuration.
|
||||
/// </summary>
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
|
||||
@@ -18,6 +18,10 @@ public class AvaloniaClipboardService : IClipboardService
|
||||
_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();
|
||||
|
||||
@@ -22,6 +22,12 @@ public class AvaloniaDialogService : IDialogService
|
||||
_getOwnerWindow = getOwnerWindow ?? throw new ArgumentNullException(nameof(getOwnerWindow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an error dialog with the specified message and title.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message to display.</param>
|
||||
/// <param name="title">The title of the error dialog.</param>
|
||||
/// <returns>A task that completes when the dialog is dismissed.</returns>
|
||||
public async Task ShowErrorAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, Icon.Error);
|
||||
@@ -36,6 +42,12 @@ public class AvaloniaDialogService : IDialogService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an informational dialog with the specified message and title.
|
||||
/// </summary>
|
||||
/// <param name="message">The information message to display.</param>
|
||||
/// <param name="title">The title of the information dialog.</param>
|
||||
/// <returns>A task that completes when the dialog is dismissed.</returns>
|
||||
public async Task ShowInfoAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, Icon.Info);
|
||||
@@ -50,6 +62,12 @@ public class AvaloniaDialogService : IDialogService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a confirmation dialog with Yes/No buttons.
|
||||
/// </summary>
|
||||
/// <param name="message">The confirmation message to display.</param>
|
||||
/// <param name="title">The title of the confirmation dialog.</param>
|
||||
/// <returns>A task that completes with true if Yes was clicked; otherwise, false.</returns>
|
||||
public async Task<bool> ShowConfirmationAsync(string message, string title)
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.YesNo, Icon.Warning);
|
||||
@@ -66,6 +84,10 @@ public class AvaloniaDialogService : IDialogService
|
||||
return result == ButtonResult.Yes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a prompt asking the user whether to save unsaved changes.
|
||||
/// </summary>
|
||||
/// <returns>A task that completes with the user's choice: Save, DontSave, or Cancel.</returns>
|
||||
public async Task<UnsavedChangesResult> ShowUnsavedChangesPromptAsync()
|
||||
{
|
||||
var box = MessageBoxManager.GetMessageBoxStandard(
|
||||
@@ -93,6 +115,14 @@ public class AvaloniaDialogService : IDialogService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a save file dialog allowing the user to select a file path.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the save file dialog.</param>
|
||||
/// <param name="fileTypeName">The friendly name of the file type (e.g., "Excel Files").</param>
|
||||
/// <param name="pattern">The file extension pattern (e.g., "*.xlsx").</param>
|
||||
/// <param name="defaultExtension">The default file extension (e.g., ".xlsx").</param>
|
||||
/// <returns>A task that completes with the selected file path, or null if canceled.</returns>
|
||||
public async Task<string?> ShowSaveFileDialogAsync(string title, string fileTypeName, string pattern, string defaultExtension)
|
||||
{
|
||||
var window = _getOwnerWindow();
|
||||
@@ -113,6 +143,13 @@ public class AvaloniaDialogService : IDialogService
|
||||
return file?.Path.LocalPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays an open file dialog allowing the user to select a file to open.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the open file dialog.</param>
|
||||
/// <param name="fileTypeName">The friendly name of the file type (e.g., "Excel Files").</param>
|
||||
/// <param name="pattern">The file extension pattern (e.g., "*.xlsx").</param>
|
||||
/// <returns>A task that completes with the selected file path, or null if canceled.</returns>
|
||||
public async Task<string?> ShowOpenFileDialogAsync(string title, string fileTypeName, string pattern)
|
||||
{
|
||||
var window = _getOwnerWindow();
|
||||
|
||||
@@ -19,16 +19,22 @@ public interface IDialogService
|
||||
/// <summary>
|
||||
/// Shows an error message dialog.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message to display.</param>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
Task ShowErrorAsync(string message, string title);
|
||||
|
||||
/// <summary>
|
||||
/// Shows an informational message dialog.
|
||||
/// </summary>
|
||||
/// <param name="message">The informational message to display.</param>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
Task ShowInfoAsync(string message, string title);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a confirmation dialog with Yes/No options.
|
||||
/// </summary>
|
||||
/// <param name="message">The confirmation message to display.</param>
|
||||
/// <param name="title">The dialog title.</param>
|
||||
/// <returns>True if user clicked Yes, false otherwise.</returns>
|
||||
Task<bool> ShowConfirmationAsync(string message, string title);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable
|
||||
/// <summary>
|
||||
/// Creates a new SecureStoreManager with the specified logger.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger instance for diagnostic output.</param>
|
||||
public SecureStoreManager(ILogger<SecureStoreManager> logger)
|
||||
{
|
||||
_logger = logger ?? NullLogger<SecureStoreManager>.Instance;
|
||||
@@ -320,6 +321,9 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the resources used by the <see cref="SecureStoreManager"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
@@ -12,6 +12,9 @@ public class AsyncRelayCommand : ICommand
|
||||
private bool _isExecuting;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the result of CanExecute() may have changed.
|
||||
/// </summary>
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
@@ -38,11 +41,19 @@ public class AsyncRelayCommand : ICommand
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the command can be executed in its current state.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The command parameter (unused).</param>
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the async command if it can execute.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The command parameter (unused).</param>
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
|
||||
@@ -15,12 +15,18 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
private bool _useKeyFile = true;
|
||||
private bool _usePassword;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NewStoreDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
public NewStoreDialogViewModel()
|
||||
{
|
||||
BrowseStorePathCommand = new RelayCommand(BrowseStorePath);
|
||||
BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the store file to create.
|
||||
/// </summary>
|
||||
public string StorePath
|
||||
{
|
||||
get => _storePath;
|
||||
@@ -31,6 +37,9 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the key file for encryption.
|
||||
/// </summary>
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
@@ -41,6 +50,9 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for store encryption.
|
||||
/// </summary>
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
@@ -51,6 +63,9 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password confirmation value.
|
||||
/// </summary>
|
||||
public string ConfirmPassword
|
||||
{
|
||||
get => _confirmPassword;
|
||||
@@ -61,6 +76,9 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use key file for store encryption.
|
||||
/// </summary>
|
||||
public bool UseKeyFile
|
||||
{
|
||||
get => _useKeyFile;
|
||||
@@ -74,6 +92,9 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use password for store encryption.
|
||||
/// </summary>
|
||||
public bool UsePassword
|
||||
{
|
||||
get => _usePassword;
|
||||
@@ -93,9 +114,19 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
/// <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 a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
@@ -113,6 +144,9 @@ public class NewStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
@@ -187,12 +221,18 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
private bool _useKeyFile = true;
|
||||
private bool _usePassword;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenStoreDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
public OpenStoreDialogViewModel()
|
||||
{
|
||||
BrowseStorePathCommand = new RelayCommand(BrowseStorePath);
|
||||
BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the store file to open.
|
||||
/// </summary>
|
||||
public string StorePath
|
||||
{
|
||||
get => _storePath;
|
||||
@@ -203,6 +243,9 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the key file for decryption.
|
||||
/// </summary>
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
@@ -213,6 +256,9 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password for store decryption.
|
||||
/// </summary>
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
@@ -223,6 +269,9 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use key file for store decryption.
|
||||
/// </summary>
|
||||
public bool UseKeyFile
|
||||
{
|
||||
get => _useKeyFile;
|
||||
@@ -236,6 +285,9 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use password for store decryption.
|
||||
/// </summary>
|
||||
public bool UsePassword
|
||||
{
|
||||
get => _usePassword;
|
||||
@@ -255,9 +307,19 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
/// <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 a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
@@ -275,6 +337,9 @@ public class OpenStoreDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
@@ -348,10 +413,18 @@ public class SecretEditDialogViewModel : ViewModelBase
|
||||
private string _value = string.Empty;
|
||||
private bool _isNewSecret = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
public SecretEditDialogViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialogViewModel"/> class with a key and value for editing.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <param name="value">The secret value.</param>
|
||||
public SecretEditDialogViewModel(string key, string value)
|
||||
{
|
||||
_key = key;
|
||||
@@ -359,6 +432,9 @@ public class SecretEditDialogViewModel : ViewModelBase
|
||||
_isNewSecret = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key.
|
||||
/// </summary>
|
||||
public string Key
|
||||
{
|
||||
get => _key;
|
||||
@@ -369,24 +445,42 @@ public class SecretEditDialogViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 => SetProperty(ref _isNewSecret, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the key field is editable.
|
||||
/// </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
|
||||
|
||||
@@ -16,6 +16,12 @@ public class MainWindowViewModel : ViewModelBase
|
||||
private SecretItemViewModel? _selectedSecret;
|
||||
private string _statusMessage = "Ready";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MainWindowViewModel.
|
||||
/// </summary>
|
||||
/// <param name="storeManager">The secure store manager service.</param>
|
||||
/// <param name="dialogService">The dialog service for user interactions.</param>
|
||||
/// <param name="clipboardService">The clipboard service for copying secrets.</param>
|
||||
public MainWindowViewModel(
|
||||
ISecureStoreManager storeManager,
|
||||
IDialogService dialogService,
|
||||
@@ -102,24 +108,64 @@ public class MainWindowViewModel : ViewModelBase
|
||||
public bool HasUnsavedChanges => _storeManager.HasUnsavedChanges;
|
||||
|
||||
// File Commands
|
||||
/// <summary>
|
||||
/// Gets the command to create a new store.
|
||||
/// </summary>
|
||||
public ICommand NewStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to open an existing store.
|
||||
/// </summary>
|
||||
public ICommand OpenStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to save the current store.
|
||||
/// </summary>
|
||||
public ICommand SaveCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to close the current store.
|
||||
/// </summary>
|
||||
public ICommand CloseStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to exit the application.
|
||||
/// </summary>
|
||||
public ICommand ExitCommand { get; }
|
||||
|
||||
// Secret Commands
|
||||
/// <summary>
|
||||
/// Gets the command to add a new secret.
|
||||
/// </summary>
|
||||
public ICommand AddSecretCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to edit the selected secret.
|
||||
/// </summary>
|
||||
public ICommand EditSecretCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to delete the selected secret.
|
||||
/// </summary>
|
||||
public ICommand DeleteSecretCommand { get; }
|
||||
|
||||
// Tools Commands
|
||||
/// <summary>
|
||||
/// Gets the command to generate a new key file.
|
||||
/// </summary>
|
||||
public ICommand GenerateKeyFileCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to export the store's key.
|
||||
/// </summary>
|
||||
public ICommand ExportKeyCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new store. Called by the dialog.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path where the store file will be created.</param>
|
||||
/// <param name="keyFilePath">Optional path to a key file for encryption.</param>
|
||||
/// <param name="password">Optional password for encryption.</param>
|
||||
public async Task CreateNewStoreAsync(string storePath, string? keyFilePath, string? password)
|
||||
{
|
||||
try
|
||||
@@ -154,6 +200,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// <summary>
|
||||
/// Opens an existing store. Called by the dialog.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path to the store file to open.</param>
|
||||
/// <param name="keyFilePath">Optional path to a key file for decryption.</param>
|
||||
/// <param name="password">Optional password for decryption.</param>
|
||||
public async Task OpenExistingStoreAsync(string storePath, string? keyFilePath, string? password)
|
||||
{
|
||||
try
|
||||
@@ -188,6 +237,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// <summary>
|
||||
/// Adds or updates a secret. Called by the dialog.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key identifier.</param>
|
||||
/// <param name="value">The secret value to store.</param>
|
||||
/// <param name="isNew">True if this is a new secret, false if updating an existing one.</param>
|
||||
public async Task SaveSecretAsync(string key, string value, bool isNew)
|
||||
{
|
||||
try
|
||||
@@ -415,9 +467,28 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
// Events for view to show dialogs (these require view-specific DataContext setup)
|
||||
/// <summary>
|
||||
/// Raised when a new store creation dialog should be shown.
|
||||
/// </summary>
|
||||
public event Action? OnRequestNewStoreDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an open store dialog should be shown.
|
||||
/// </summary>
|
||||
public event Action? OnRequestOpenStoreDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a new secret dialog should be shown.
|
||||
/// </summary>
|
||||
public event Action? OnRequestAddSecretDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an edit secret dialog should be shown with the specified key and value.
|
||||
/// </summary>
|
||||
public event Action<string, string>? OnRequestEditSecretDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the application should close.
|
||||
/// </summary>
|
||||
public event Action? OnRequestClose;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ public class RelayCommand : ICommand
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the command's ability to execute may have changed.
|
||||
/// </summary>
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
@@ -56,11 +59,20 @@ public class RelayCommand : ICommand
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the command can execute.
|
||||
/// </summary>
|
||||
/// <param name="parameter">An optional command parameter.</param>
|
||||
/// <returns>True if the command can execute, false otherwise.</returns>
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return _canExecute == null || _canExecute(parameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command with the specified parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">An optional command parameter.</param>
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
_execute(parameter);
|
||||
|
||||
@@ -13,6 +13,12 @@ public class SecretItemViewModel : ViewModelBase
|
||||
private bool _isValueVisible;
|
||||
private const string MaskedValue = "********";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretItemViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key name.</param>
|
||||
/// <param name="value">The secret value.</param>
|
||||
/// <param name="clipboardService">The clipboard service for copy operations.</param>
|
||||
public SecretItemViewModel(string key, string value, IClipboardService clipboardService)
|
||||
{
|
||||
Key = key;
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>Occurs when a property value changes.</summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,6 +9,9 @@ public partial class MainWindow : Window
|
||||
{
|
||||
private MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MainWindow"/> class.
|
||||
/// </summary>
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -10,8 +10,14 @@ namespace JdeScoping.SecureStoreManager.Views;
|
||||
|
||||
public partial class NewStoreDialog : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the view model for this dialog.
|
||||
/// </summary>
|
||||
public NewStoreDialogViewModel ViewModel => (NewStoreDialogViewModel)DataContext!;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the NewStoreDialog.
|
||||
/// </summary>
|
||||
public NewStoreDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -10,8 +10,14 @@ namespace JdeScoping.SecureStoreManager.Views;
|
||||
|
||||
public partial class OpenStoreDialog : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the view model for this dialog.
|
||||
/// </summary>
|
||||
public OpenStoreDialogViewModel ViewModel => (OpenStoreDialogViewModel)DataContext!;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenStoreDialog"/> class.
|
||||
/// </summary>
|
||||
public OpenStoreDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -8,14 +8,25 @@ namespace JdeScoping.SecureStoreManager.Views;
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user