refactor(configmanager): simplify SecureStore UI with unified info view
Consolidate SecureStoreLockedFormView and SecureStoreUnlockedFormView into a single SecureStoreInfoFormView that displays store status and metadata. Simplifies MainWindowViewModel by removing redundant state management. Also adds design docs for RegexTransformer feature.
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# Regex Transformer Design
|
||||
|
||||
**Date:** 2025-01-22
|
||||
**Status:** Approved
|
||||
**Author:** Claude + User collaboration
|
||||
|
||||
## Overview
|
||||
|
||||
Add a new `RegexTransformer` to the DataSync ETL pipeline that transforms string values in a column using regular expressions. Includes a custom editor for the ConfigManager with a live test/preview feature.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. **Two transformation modes:**
|
||||
- **Find & Replace** - Replace matched text with replacement string
|
||||
- **Match & Extract** - Extract first capture group from pattern
|
||||
|
||||
2. **Single column per transformer** - Each transformer operates on one column; add multiple transformers for multiple columns
|
||||
|
||||
3. **Configurable non-match behavior:**
|
||||
- Keep original value (default)
|
||||
- Return null
|
||||
- Return empty string
|
||||
|
||||
4. **Case-insensitive option** - Optional flag for case-insensitive matching
|
||||
|
||||
5. **Test/preview in editor** - Users can test their regex pattern against sample input before saving
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Transformer
|
||||
|
||||
**File:** `NEW/src/JdeScoping.DataSync/Etl/Transformers/RegexTransformer.cs`
|
||||
|
||||
```csharp
|
||||
public enum NonMatchBehavior
|
||||
{
|
||||
KeepOriginal,
|
||||
ReturnNull,
|
||||
ReturnEmpty
|
||||
}
|
||||
|
||||
public class RegexTransformer : DataTransformerBase
|
||||
{
|
||||
public RegexTransformer(
|
||||
string columnName,
|
||||
string pattern,
|
||||
string? replacement = null, // null = Match & Extract mode
|
||||
bool ignoreCase = false,
|
||||
NonMatchBehavior nonMatchBehavior = NonMatchBehavior.KeepOriginal)
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Extends `DataTransformerBase`
|
||||
- In `OnInitialize()`: finds column ordinal, compiles regex
|
||||
- Overrides `GetValue()`: transforms target column, passes through others
|
||||
- Mode determined by `replacement` parameter: non-null = Find & Replace, null = Match & Extract
|
||||
|
||||
**Transformation logic:**
|
||||
1. If not target column → pass through
|
||||
2. If null/DBNull → pass through
|
||||
3. Find & Replace mode: `regex.Replace(value, replacement)`
|
||||
4. Match & Extract mode: return `match.Groups[1].Value` if match, else apply `NonMatchBehavior`
|
||||
|
||||
### Configuration Model
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs`
|
||||
|
||||
Add enum:
|
||||
```csharp
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum NonMatchBehavior
|
||||
{
|
||||
KeepOriginal,
|
||||
ReturnNull,
|
||||
ReturnEmpty
|
||||
}
|
||||
```
|
||||
|
||||
Add properties to `TransformerModel`:
|
||||
```csharp
|
||||
public string? ColumnName { get; set; }
|
||||
public string? Pattern { get; set; }
|
||||
public string? Replacement { get; set; }
|
||||
public bool IgnoreCase { get; set; }
|
||||
public NonMatchBehavior NonMatchBehavior { get; set; } = NonMatchBehavior.KeepOriginal;
|
||||
```
|
||||
|
||||
**JSON example:**
|
||||
```json
|
||||
{
|
||||
"Type": "Regex",
|
||||
"ColumnName": "BatchID",
|
||||
"Pattern": "^IIS_",
|
||||
"Replacement": "",
|
||||
"IgnoreCase": true,
|
||||
"NonMatchBehavior": "KeepOriginal"
|
||||
}
|
||||
```
|
||||
|
||||
### ViewModel
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/PipelineSteps/TransformerStepViewModels.cs`
|
||||
|
||||
Add `RegexTransformerViewModel` class:
|
||||
|
||||
**Core properties (bound to config):**
|
||||
- `ColumnName` (string)
|
||||
- `Pattern` (string)
|
||||
- `Replacement` (string)
|
||||
- `IgnoreCase` (bool)
|
||||
- `NonMatchBehavior` (enum)
|
||||
|
||||
**Mode properties (computed):**
|
||||
- `IsFindReplaceMode` / `IsMatchExtractMode` - for radio button binding
|
||||
- `PatternHelpText` - changes based on mode
|
||||
|
||||
**Test feature properties:**
|
||||
- `TestInput` (string)
|
||||
- `TestResultValue` (string)
|
||||
- `TestResultLabel` (string) - "Output" or "No Match"
|
||||
- `TestResultIcon` (string) - "✓" or "—"
|
||||
- `TestResultBackground` (string) - green or orange
|
||||
- `HasTestResult` / `HasTestError` (bool)
|
||||
- `TestErrorMessage` (string)
|
||||
|
||||
**Command:**
|
||||
- `TestPatternCommand` - executes regex test
|
||||
|
||||
### Editor View
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Editors/RegexEditorView.axaml`
|
||||
|
||||
Layout sections:
|
||||
1. **Header** - Title "Regex Transformer" + description
|
||||
2. **Column Name** - Text input with required indicator
|
||||
3. **Mode Selection** - Radio buttons: "Find & Replace" / "Match & Extract"
|
||||
4. **Pattern** - Monospace text input with dynamic help text
|
||||
5. **Replacement** - Monospace text input (visible only in Find & Replace mode)
|
||||
6. **Options Row** - Case Insensitive checkbox + Non-Match Behavior dropdown
|
||||
7. **Test Section** - Bordered area with:
|
||||
- Sample input textbox
|
||||
- Test button (blue)
|
||||
- Result display with status icon (green checkmark / orange dash)
|
||||
- Error display (red-tinted) for invalid patterns
|
||||
8. **Help Box** - Pattern examples
|
||||
|
||||
**Design tokens (matching existing editors):**
|
||||
- Background: `#0D0F12`, `#151920`, `#232A35`
|
||||
- Text: `#E6EDF5` (bright), `#9BA8B8` (labels), `#5C6A7A` (dim)
|
||||
- Accent: `#3B82F6` (blue button), `#22C55E` (success), `#F59E0B` (warning), `#FF6B6B` (error)
|
||||
- Font: JetBrains Mono for code fields
|
||||
- Spacing: 16px between sections, 4px within field groups
|
||||
|
||||
### Registration
|
||||
|
||||
**TransformerFactory updates:**
|
||||
```csharp
|
||||
// Create() switch:
|
||||
"regex" => new RegexTransformerViewModel(model, onChanged),
|
||||
|
||||
// CreateNew() switch:
|
||||
"regex" => new RegexTransformerViewModel(onChanged),
|
||||
|
||||
// AvailableTypes:
|
||||
["ColumnDrop", "ColumnRename", "JdeDate", "Regex"]
|
||||
```
|
||||
|
||||
**MainWindow.axaml DataTemplate:**
|
||||
```xml
|
||||
<DataTemplate DataType="{x:Type steps:RegexTransformerViewModel}">
|
||||
<editors:RegexEditorView/>
|
||||
</DataTemplate>
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### RegexTransformer Unit Tests
|
||||
|
||||
| Test Case | Description |
|
||||
|-----------|-------------|
|
||||
| `FindReplace_RemovesPrefix` | `^IIS_` + `""` → `IIS_12345` becomes `12345` |
|
||||
| `FindReplace_ReplacesWithText` | `foo` + `bar` → `foo123` becomes `bar123` |
|
||||
| `FindReplace_UseCaptureGroups` | `(\d+)-(\d+)` + `$2-$1` swaps groups |
|
||||
| `MatchExtract_ExtractsFirstGroup` | `ID_(\d+)` extracts `12345` from `ID_12345` |
|
||||
| `MatchExtract_NoMatch_KeepOriginal` | Returns original when no match |
|
||||
| `MatchExtract_NoMatch_ReturnNull` | Returns DBNull when configured |
|
||||
| `MatchExtract_NoMatch_ReturnEmpty` | Returns `""` when configured |
|
||||
| `IgnoreCase_MatchesDifferentCase` | `^iis_` matches `IIS_12345` |
|
||||
| `NullValue_PassesThrough` | DBNull input returns DBNull |
|
||||
| `NonTargetColumn_Unchanged` | Other columns pass through |
|
||||
| `InvalidRegex_ThrowsOnInitialize` | Bad pattern throws meaningful exception |
|
||||
|
||||
### RegexTransformerViewModel Unit Tests
|
||||
|
||||
| Test Case | Description |
|
||||
|-----------|-------------|
|
||||
| `TestPattern_ValidRegex_ShowsResult` | Test button displays transformed output |
|
||||
| `TestPattern_InvalidRegex_ShowsError` | Bad pattern shows error message |
|
||||
| `ModeSwitch_UpdatesHelpText` | Help text changes with mode |
|
||||
| `ToModel_SerializesCorrectly` | ViewModel produces valid TransformerModel |
|
||||
| `FromModel_LoadsAllProperties` | Constructor populates from existing config |
|
||||
|
||||
## Files Summary
|
||||
|
||||
**Create:**
|
||||
- `NEW/src/JdeScoping.DataSync/Etl/Transformers/RegexTransformer.cs`
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Editors/RegexEditorView.axaml`
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Editors/RegexEditorView.axaml.cs`
|
||||
- `NEW/tests/JdeScoping.DataSync.Tests/Etl/Transformers/RegexTransformerTests.cs`
|
||||
- `NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/RegexTransformerViewModelTests.cs`
|
||||
|
||||
**Modify:**
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Models/PipelineModel.cs` - Add enum + properties
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/PipelineSteps/TransformerStepViewModels.cs` - Add ViewModel + factory
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml` - Add DataTemplate
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,525 @@
|
||||
# Implementation Plan: SecureStore Auto-Initialization & Tree Flattening
|
||||
|
||||
## Overview
|
||||
|
||||
This plan implements automatic SecureStore initialization on config load and flattens the tree hierarchy from "Secure Stores > secrets > [keys]" to "Secure Store > [keys]".
|
||||
|
||||
## Design Decisions (from brainstorming)
|
||||
|
||||
1. **Auto-creation**: Fully automatic on config folder load when `AutoCreateStore=true`
|
||||
2. **Path updates**: Write actual paths back to appsettings.json after creating files
|
||||
3. **Tree structure**: Single "Secure Store" node with secrets as direct children
|
||||
4. **Authentication**: Keyfile-only (remove password option entirely)
|
||||
5. **Lock concept**: Removed - store is always open while config folder is loaded
|
||||
6. **Icon**: Key icon (🔑) for "Secure Store" node
|
||||
7. **Right panel**: Show instructions "Select a secret to edit" when store node selected
|
||||
8. **Error handling**: Show error dialog if keyfile missing/corrupted
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Update TreeNodeType enum
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs`
|
||||
|
||||
**Changes:**
|
||||
- Remove `SecureStoresFolder` enum value (no longer needed)
|
||||
- Keep `SecureStore` for the single store node
|
||||
- Keep `Secret` for individual secrets
|
||||
|
||||
**Before (lines 5-13):**
|
||||
```csharp
|
||||
public enum TreeNodeType
|
||||
{
|
||||
Folder,
|
||||
SettingsSection,
|
||||
Pipeline,
|
||||
SecureStoresFolder, // The "Secure Stores" folder
|
||||
SecureStore, // Individual store files
|
||||
Secret // Individual secrets within a store
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```csharp
|
||||
public enum TreeNodeType
|
||||
{
|
||||
Folder,
|
||||
SettingsSection,
|
||||
Pipeline,
|
||||
SecureStore, // The single secure store node
|
||||
Secret // Individual secrets within a store
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Remove lock-related properties from TreeNodeViewModel
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs`
|
||||
|
||||
**Remove these properties (lines 55-79):**
|
||||
- `IsUnlocked` property and backing field
|
||||
- `IsLocked` computed property
|
||||
- `LockIcon` computed property
|
||||
|
||||
**Keep:**
|
||||
- `StorePath` and `KeyFilePath` (needed for store identification)
|
||||
- `SecretKey` (needed for secret nodes)
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Remove password-related methods from ISecureStoreManager
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs`
|
||||
|
||||
**Remove these methods:**
|
||||
- `CreateStoreWithPassword(string storePath, string password)` (lines 30-35)
|
||||
- `OpenStoreWithPassword(string storePath, string password)` (lines 44-49)
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Remove password-related methods from SecureStoreManager
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs`
|
||||
|
||||
**Remove these methods:**
|
||||
- `CreateStoreWithPassword()` (lines 75-96)
|
||||
- `OpenStoreWithPassword()` (lines 120-140)
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add EnsureRequiredKeys method to ISecureStoreManager
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs`
|
||||
|
||||
**Add new method:**
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Ensures all required keys exist in the store, creating blank values for any missing keys.
|
||||
/// </summary>
|
||||
/// <param name="requiredKeys">List of keys that must exist.</param>
|
||||
/// <returns>List of keys that were added.</returns>
|
||||
IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Implement EnsureRequiredKeys in SecureStoreManager
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs`
|
||||
|
||||
**Add implementation:**
|
||||
```csharp
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (_secretsManager == null)
|
||||
throw new InvalidOperationException("No store is currently open.");
|
||||
|
||||
var addedKeys = new List<string>();
|
||||
foreach (var key in requiredKeys)
|
||||
{
|
||||
if (!_keys.Contains(key))
|
||||
{
|
||||
_logger.LogInformation("Adding missing required key: {Key}", key);
|
||||
SetSecret(key, string.Empty);
|
||||
addedKeys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (addedKeys.Count > 0)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
return addedKeys.AsReadOnly();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Delete locked/unlocked form files
|
||||
|
||||
**Delete these files entirely:**
|
||||
|
||||
ViewModels:
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreLockedFormViewModel.cs`
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreUnlockedFormViewModel.cs`
|
||||
|
||||
Views:
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml`
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml.cs`
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml`
|
||||
- `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml.cs`
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Create SecureStoreInfoFormViewModel
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreInfoFormViewModel.cs` (new file)
|
||||
|
||||
**Content:**
|
||||
```csharp
|
||||
namespace JdeScoping.ConfigManager.ViewModels.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// View model for the SecureStore info panel shown when the store node is selected.
|
||||
/// </summary>
|
||||
public class SecureStoreInfoFormViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the instruction text to display.
|
||||
/// </summary>
|
||||
public string InstructionText => "Select a secret from the tree to view or edit its value.";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the store path for display.
|
||||
/// </summary>
|
||||
public string StorePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key file path for display.
|
||||
/// </summary>
|
||||
public string KeyFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of secrets in the store.
|
||||
/// </summary>
|
||||
public int SecretCount { get; }
|
||||
|
||||
public SecureStoreInfoFormViewModel(string storePath, string keyFilePath, int secretCount)
|
||||
{
|
||||
StorePath = storePath;
|
||||
KeyFilePath = keyFilePath;
|
||||
SecretCount = secretCount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Create SecureStoreInfoFormView
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml` (new file)
|
||||
|
||||
**Content:** Simple panel with instruction text and optional store info display.
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml.cs` (new file)
|
||||
|
||||
**Content:** Code-behind for the view.
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Update MainWindowViewModel - Remove lock/unlock commands and state
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
|
||||
|
||||
**Remove:**
|
||||
- `_openStores` dictionary (line 41)
|
||||
- `_selectedStoreNode` field (line 42)
|
||||
- `UnlockStoreCommand` and related methods
|
||||
- `LockStoreCommand` and related methods
|
||||
- `RaiseSecureStoreCommandsCanExecuteChanged()` method
|
||||
- `CreateLockedStoreFormViewModel()` method
|
||||
- `CreateUnlockedStoreFormViewModel()` method
|
||||
- `FindParentStoreNode()` method (if only used for lock state)
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Update MainWindowViewModel - Add auto-initialization logic
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
|
||||
|
||||
**Add new method `InitializeSecureStoreAsync()`:**
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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))
|
||||
{
|
||||
await _dialogService!.ShowErrorAsync(
|
||||
"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 keys exist
|
||||
if (secureStoreConfig.RequiredKeys?.Count > 0)
|
||||
{
|
||||
var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys);
|
||||
if (addedKeys.Count > 0)
|
||||
{
|
||||
_logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to initialize SecureStore");
|
||||
await _dialogService!.ShowErrorAsync(
|
||||
"SecureStore Error",
|
||||
$"Failed to initialize SecureStore:\n\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Update MainWindowViewModel - Modify BuildTreeNodes
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
|
||||
|
||||
**Replace the Secure Stores section in `BuildTreeNodes()` (around lines 464-467):**
|
||||
|
||||
**Before:**
|
||||
```csharp
|
||||
// Secure Stores folder
|
||||
var secureStoresFolder = new TreeNodeViewModel("Secure Stores", "🔑", TreeNodeType.SecureStoresFolder) { IsExpanded = true };
|
||||
LoadConfiguredSecureStore(secureStoresFolder);
|
||||
TreeNodes.Add(secureStoresFolder);
|
||||
```
|
||||
|
||||
**After:**
|
||||
```csharp
|
||||
// Secure Store node (single store, secrets as direct children)
|
||||
var secureStoreNode = CreateSecureStoreNode();
|
||||
if (secureStoreNode != null)
|
||||
{
|
||||
TreeNodes.Add(secureStoreNode);
|
||||
}
|
||||
```
|
||||
|
||||
**Add new method `CreateSecureStoreNode()`:**
|
||||
```csharp
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 13: Update MainWindowViewModel - Modify OnSelectedNodeChanged
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
|
||||
|
||||
**Update the switch statement in `OnSelectedNodeChanged()` (around lines 530-557):**
|
||||
|
||||
**Remove:**
|
||||
- `case TreeNodeType.SecureStoresFolder:` block
|
||||
- Lock/unlock logic in `case TreeNodeType.SecureStore:` block
|
||||
|
||||
**Replace with:**
|
||||
```csharp
|
||||
case TreeNodeType.SecureStore:
|
||||
SelectedFormViewModel = CreateSecureStoreInfoFormViewModel();
|
||||
return;
|
||||
|
||||
case TreeNodeType.Secret:
|
||||
SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode);
|
||||
return;
|
||||
```
|
||||
|
||||
**Add method:**
|
||||
```csharp
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 14: Update MainWindowViewModel - Call InitializeSecureStoreAsync on load
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
|
||||
|
||||
**Find the method that loads the config folder (likely `LoadConfigFolderAsync` or similar).**
|
||||
|
||||
**Add call to `InitializeSecureStoreAsync()` after loading appsettings.json but before building tree nodes:**
|
||||
|
||||
```csharp
|
||||
// After loading appsettings
|
||||
_appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath, ct);
|
||||
|
||||
// Initialize SecureStore (auto-create if needed, open, sync required keys)
|
||||
await InitializeSecureStoreAsync();
|
||||
|
||||
// Then build tree nodes
|
||||
BuildTreeNodes();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 15: Remove LoadConfiguredSecureStore method
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs`
|
||||
|
||||
**Delete the `LoadConfiguredSecureStore()` method entirely (lines 474-514).**
|
||||
|
||||
This is replaced by `CreateSecureStoreNode()` and `InitializeSecureStoreAsync()`.
|
||||
|
||||
---
|
||||
|
||||
## Task 16: Update DataTemplates for form views
|
||||
|
||||
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml` (or wherever DataTemplates are defined)
|
||||
|
||||
**Remove:**
|
||||
- DataTemplate for `SecureStoreLockedFormViewModel`
|
||||
- DataTemplate for `SecureStoreUnlockedFormViewModel`
|
||||
|
||||
**Add:**
|
||||
- DataTemplate for `SecureStoreInfoFormViewModel`
|
||||
|
||||
---
|
||||
|
||||
## Task 17: Update tests
|
||||
|
||||
**File:** `NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs`
|
||||
|
||||
**Update or remove tests related to:**
|
||||
- `SecureStoresFolder` node type
|
||||
- Lock/unlock commands
|
||||
- Password-based store operations
|
||||
|
||||
**Add tests for:**
|
||||
- Auto-creation of SecureStore on config load
|
||||
- Sync of missing required keys
|
||||
- Flattened tree structure
|
||||
|
||||
---
|
||||
|
||||
## Task 18: Build and verify
|
||||
|
||||
```bash
|
||||
dotnet build NEW/JdeScoping.slnx
|
||||
dotnet test NEW/JdeScoping.slnx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Tasks 1-2: TreeNodeViewModel changes (enum, remove lock properties)
|
||||
2. Tasks 3-6: SecureStoreManager interface and implementation changes
|
||||
3. Tasks 7-9: Delete old form files, create new info form
|
||||
4. Tasks 10-15: MainWindowViewModel changes (largest task)
|
||||
5. Task 16: Update XAML DataTemplates
|
||||
6. Task 17: Update tests
|
||||
7. Task 18: Build and verify
|
||||
|
||||
---
|
||||
|
||||
## Risk Areas
|
||||
|
||||
1. **Missing references**: Removing lock/unlock commands may break menu items or toolbar buttons
|
||||
2. **View DataTemplates**: Must update form selector to use new `SecureStoreInfoFormViewModel`
|
||||
3. **Test coverage**: Existing tests may expect lock/unlock behavior
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] App builds without errors
|
||||
- [ ] All tests pass
|
||||
- [ ] Opening a config folder without SecureStore creates it automatically
|
||||
- [ ] Opening a config folder with existing SecureStore opens it
|
||||
- [ ] Missing required keys are added as blank values
|
||||
- [ ] Tree shows "Secure Store" with secrets directly underneath
|
||||
- [ ] Clicking "Secure Store" shows instruction panel
|
||||
- [ ] Clicking a secret shows the secret editor
|
||||
- [ ] Error dialog shown when keyfile is missing
|
||||
Reference in New Issue
Block a user