feat: add startup config validation and document ConfigManager pipeline editor

Add ConfigurationValidationRunner with IConfigurationValidator interface for
validating required settings at startup. Includes SecureStore and LDAP validators.
Expand ConfigManager with pipeline editing UI, dialogs, and step editors.
Update documentation with config validation guidance.
This commit is contained in:
Joseph Doherty
2026-01-21 17:47:15 -05:00
parent ceb63bfefb
commit e5fe2f06e9
88 changed files with 4995 additions and 201 deletions
@@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="JdeScoping.ConfigManager.Views.Controls.FlowArrow"
Width="200"
Height="24">
<Canvas HorizontalAlignment="Center" Width="20" Height="24">
<!-- Vertical line -->
<Line StartPoint="10,0" EndPoint="10,16"
Stroke="#3D4550" StrokeThickness="2"/>
<!-- Arrow head (pointing down) -->
<Polygon Points="10,24 4,16 16,16"
Fill="#3D4550"/>
</Canvas>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Controls;
public partial class FlowArrow : UserControl
{
public FlowArrow()
{
InitializeComponent();
}
}
@@ -0,0 +1,53 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Controls.PipelineStepCard"
x:DataType="steps:PipelineStepViewModelBase"
Width="200">
<UserControl.Styles>
<!-- Selected state style -->
<Style Selector="Border.card">
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="#3D4550"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="Border.card:pointerover">
<Setter Property="BorderBrush" Value="#5C6A7A"/>
</Style>
</UserControl.Styles>
<Border Classes="card"
Background="#1A1F26"
CornerRadius="8"
Padding="12"
MinHeight="60"
Name="CardBorder">
<Grid ColumnDefinitions="Auto,12,*">
<!-- Icon with colored background -->
<Border Grid.Column="0"
Width="36" Height="36"
CornerRadius="6"
Name="IconBackground"
Background="#3B82F6">
<TextBlock Text="{Binding Icon}"
FontSize="18"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"/>
</Border>
<!-- Step info -->
<StackPanel Grid.Column="2" VerticalAlignment="Center" Spacing="2">
<TextBlock Text="{Binding DisplayName}"
Foreground="#E6EDF5"
FontWeight="SemiBold"
FontSize="13"/>
<TextBlock Text="{Binding Summary}"
Foreground="#5C6A7A"
FontSize="11"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,106 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using JdeScoping.ConfigManager.ViewModels.Forms;
using JdeScoping.ConfigManager.ViewModels.PipelineSteps;
namespace JdeScoping.ConfigManager.Views.Controls;
public partial class PipelineStepCard : UserControl
{
public static readonly StyledProperty<string> StepColorProperty =
AvaloniaProperty.Register<PipelineStepCard, string>(nameof(StepColor), "#3B82F6");
public PipelineStepCard()
{
InitializeComponent();
PointerPressed += OnPointerPressed;
PropertyChanged += OnPropertyChangedHandler;
DataContextChanged += OnDataContextChanged;
}
/// <summary>
/// Gets or sets the step color for the icon background.
/// </summary>
public string StepColor
{
get => GetValue(StepColorProperty);
set => SetValue(StepColorProperty, value);
}
private void OnPropertyChangedHandler(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == StepColorProperty)
{
UpdateIconColor();
}
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
UpdateIconColor();
UpdateSelectionState();
if (DataContext is PipelineStepViewModelBase vm)
{
vm.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(PipelineStepViewModelBase.IsSelected))
{
UpdateSelectionState();
}
};
}
}
private void UpdateIconColor()
{
var iconBg = this.FindControl<Border>("IconBackground");
if (iconBg != null && !string.IsNullOrEmpty(StepColor))
{
if (Color.TryParse(StepColor, out var color))
{
iconBg.Background = new SolidColorBrush(color);
}
}
}
private void UpdateSelectionState()
{
var cardBorder = this.FindControl<Border>("CardBorder");
if (cardBorder != null && DataContext is PipelineStepViewModelBase vm)
{
if (vm.IsSelected)
{
cardBorder.BorderBrush = new SolidColorBrush(Color.Parse("#3B82F6"));
cardBorder.Background = new SolidColorBrush(Color.Parse("#1E2A3A"));
}
else
{
cardBorder.BorderBrush = new SolidColorBrush(Color.Parse("#3D4550"));
cardBorder.Background = new SolidColorBrush(Color.Parse("#1A1F26"));
}
}
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
// Find the PipelineEditorViewModel from the visual tree
var parent = this.Parent;
while (parent != null)
{
if (parent is UserControl uc && uc.DataContext is PipelineEditorViewModel editorVm)
{
if (DataContext is PipelineStepViewModelBase stepVm)
{
editorVm.SelectedStep = stepVm;
}
break;
}
parent = parent.Parent;
}
e.Handled = true;
}
}
@@ -0,0 +1,53 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="JdeScoping.ConfigManager.Views.Dialogs.InputDialog"
Title="Input"
Width="450" Height="200"
MinWidth="350" MinHeight="180"
Background="#151920"
WindowStartupLocation="CenterOwner"
CanResize="False"
ShowInTaskbar="False">
<DockPanel>
<!-- Header -->
<Border DockPanel.Dock="Top" Background="#1C2128" Padding="24,16"
BorderBrush="#2D3540" BorderThickness="0,0,0,1">
<TextBlock x:Name="TitleText"
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"/>
</Border>
<!-- Footer -->
<Border DockPanel.Dock="Bottom" Background="#1C2128" Padding="24,16"
BorderBrush="#2D3540" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<Button Content="Cancel" Click="CancelButton_Click"
Background="Transparent" BorderBrush="#3D4550"
Foreground="#9BA8B8" Padding="16,8" MinWidth="80"/>
<Button Content="OK" Click="OkButton_Click"
Background="#5C9AFF" Foreground="#0D0F12"
Padding="16,8" FontWeight="Medium" MinWidth="80"/>
</StackPanel>
</Border>
<!-- Content -->
<Grid Background="#151920" Margin="24,16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Prompt -->
<TextBlock Grid.Row="0" x:Name="PromptText"
Foreground="#9BA8B8" FontSize="14"
Margin="0,0,0,12" TextWrapping="Wrap"/>
<!-- Input -->
<TextBox Grid.Row="1" x:Name="InputTextBox"
Background="#0D0F12" Foreground="#E6EDF5"
BorderBrush="#3D4550" Padding="12"
VerticalContentAlignment="Center"
FontFamily="JetBrains Mono" FontSize="14"/>
</Grid>
</DockPanel>
</Window>
@@ -0,0 +1,59 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace JdeScoping.ConfigManager.Views.Dialogs;
/// <summary>
/// A simple input dialog for collecting text from the user.
/// </summary>
public partial class InputDialog : Window
{
/// <summary>
/// Gets the text entered by the user.
/// </summary>
public string? InputText => InputTextBox.Text;
/// <summary>
/// Design-time constructor for XAML previewer.
/// </summary>
public InputDialog() : this("Input", "Enter value:")
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InputDialog"/> class.
/// </summary>
/// <param name="title">The dialog title.</param>
/// <param name="prompt">The prompt message to display.</param>
/// <param name="defaultValue">Optional default value for the input field.</param>
public InputDialog(string title, string prompt, string? defaultValue = null)
{
InitializeComponent();
TitleText.Text = title;
Title = title;
PromptText.Text = prompt;
if (!string.IsNullOrEmpty(defaultValue))
{
InputTextBox.Text = defaultValue;
}
// Focus the input when loaded
Loaded += (_, _) =>
{
InputTextBox.Focus();
InputTextBox.SelectAll();
};
}
private void OkButton_Click(object? sender, RoutedEventArgs e)
{
Close(true);
}
private void CancelButton_Click(object? sender, RoutedEventArgs e)
{
Close(false);
}
}
@@ -0,0 +1,35 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Editors.ColumnDropEditorView"
x:DataType="steps:ColumnDropTransformerViewModel">
<StackPanel Spacing="16">
<!-- Header -->
<StackPanel>
<TextBlock Text="Column Drop Transformer"
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="Remove columns from the data stream"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Columns to Drop -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Columns to Drop (one per line)"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding ColumnsText}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550"
FontFamily="JetBrains Mono" FontSize="11"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="150"
Watermark="TempColumn1&#x0a;TempColumn2&#x0a;..."/>
<TextBlock Text="Enter column names to remove from the data, one per line"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Editors;
public partial class ColumnDropEditorView : UserControl
{
public ColumnDropEditorView()
{
InitializeComponent();
}
}
@@ -0,0 +1,61 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Editors.ColumnRenameEditorView"
x:DataType="steps:ColumnRenameTransformerViewModel">
<StackPanel Spacing="16">
<!-- Header -->
<StackPanel>
<TextBlock Text="Column Rename Transformer"
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="Rename columns in the data stream"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Mappings List -->
<StackPanel Spacing="8">
<TextBlock Text="Column Mappings"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<ItemsControl ItemsSource="{Binding Mappings}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="steps:ColumnMappingViewModel">
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="4" Padding="8" Margin="0,0,0,4">
<Grid ColumnDefinitions="*,Auto,*,8,Auto">
<TextBox Grid.Column="0"
Text="{Binding OldName}"
Background="#232A35" Height="32" FontSize="11"
FontFamily="JetBrains Mono"
Watermark="Old Name"/>
<TextBlock Grid.Column="1"
Text="→" Foreground="#5C6A7A"
FontSize="14" Margin="8,0"
VerticalAlignment="Center"/>
<TextBox Grid.Column="2"
Text="{Binding NewName}"
Background="#232A35" Height="32" FontSize="11"
FontFamily="JetBrains Mono"
Watermark="New Name"/>
<Button Grid.Column="4" Content="X"
Background="Transparent" Foreground="#FF6B6B"
BorderThickness="0" FontSize="11" Width="28"
VerticalAlignment="Center"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="+ Add Mapping"
Background="#232A35" Foreground="#9BA8B8"
BorderBrush="#3D4550" Height="32"
HorizontalAlignment="Left" Padding="12,0"
Command="{Binding AddMappingCommand}"/>
<TextBlock Text="Map original column names to new names"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Editors;
public partial class ColumnRenameEditorView : UserControl
{
public ColumnRenameEditorView()
{
InitializeComponent();
}
}
@@ -0,0 +1,102 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Editors.DestinationEditorView"
x:DataType="steps:DestinationStepViewModel">
<StackPanel Spacing="16">
<!-- Header -->
<StackPanel>
<TextBlock Text="Destination Configuration"
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="Configure how data is loaded into the destination table"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Destination Type Toggle -->
<StackPanel Spacing="4">
<TextBlock Text="Load Type" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<StackPanel Orientation="Horizontal" Spacing="12">
<RadioButton GroupName="DestType"
IsChecked="{Binding IsBulkMerge}"
Foreground="#E6EDF5">
<TextBlock Text="Bulk Merge (Upsert)" FontSize="12"/>
</RadioButton>
<RadioButton GroupName="DestType"
IsChecked="{Binding !IsBulkMerge}"
Foreground="#E6EDF5">
<TextBlock Text="Bulk Import (Truncate+Load)" FontSize="12"/>
</RadioButton>
</StackPanel>
<TextBlock Text="{Binding TypeDescription}"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Destination Table -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Destination Table"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding Table}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="dbo.WorkOrder_Curr"/>
<TextBlock Text="Target table in SQL Server (include schema prefix)"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- BulkMerge-specific fields -->
<StackPanel Spacing="16" IsVisible="{Binding IsBulkMerge}">
<!-- Match Columns -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Match Columns (one per line)"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding MatchColumnsText}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550"
FontFamily="JetBrains Mono" FontSize="11"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="80"
Watermark="OrderNumber&#x0a;OrderType"/>
<TextBlock Text="Columns to match source rows with existing destination rows"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Exclude From Update -->
<StackPanel Spacing="4">
<TextBlock Text="Exclude From Update (one per line)"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding ExcludeFromUpdateText}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550"
FontFamily="JetBrains Mono" FontSize="11"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="60"
Watermark="CreatedDate&#x0a;CreatedBy"/>
<TextBlock Text="Columns to skip when updating existing rows"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
</StackPanel>
<!-- BulkImport info box -->
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="4" Padding="12" Margin="0,8,0,0"
IsVisible="{Binding !IsBulkMerge}">
<StackPanel Spacing="4">
<TextBlock Text="Bulk Import Mode" Foreground="#F59E0B" FontSize="11" FontWeight="Medium"/>
<TextBlock Text="All existing data will be deleted before loading new data."
Foreground="#5C6A7A" FontSize="10"/>
<TextBlock Text="Use this for full table refreshes during mass sync operations."
Foreground="#5C6A7A" FontSize="10"/>
</StackPanel>
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Editors;
public partial class DestinationEditorView : UserControl
{
public DestinationEditorView()
{
InitializeComponent();
}
}
@@ -0,0 +1,73 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Editors.JdeDateEditorView"
x:DataType="steps:JdeDateTransformerViewModel">
<StackPanel Spacing="16">
<!-- Header -->
<StackPanel>
<TextBlock Text="JDE Date Transformer"
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="Convert JDE Julian date/time columns to DateTime"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Date Column -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Date Column"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding DateColumn}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="TRDJ"/>
<TextBlock Text="Column containing JDE Julian date (e.g., TRDJ)"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Time Column -->
<StackPanel Spacing="4">
<TextBlock Text="Time Column (Optional)"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding TimeColumn}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="TRTM"/>
<TextBlock Text="Column containing JDE time value (e.g., TRTM)"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Output Column -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Output Column"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding OutputColumn}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="TransactionDateTime"/>
<TextBlock Text="Name for the new DateTime column"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Info Box -->
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="4" Padding="12" Margin="0,8,0,0">
<StackPanel Spacing="4">
<TextBlock Text="JDE Date Format" Foreground="#9BA8B8" FontSize="11" FontWeight="Medium"/>
<TextBlock Text="JDE Julian dates are in CYYDDD format where:" Foreground="#5C6A7A" FontSize="10"/>
<TextBlock Text=" C = Century (1=20th, 2=21st)" Foreground="#5C6A7A" FontSize="10"/>
<TextBlock Text=" YY = Year" Foreground="#5C6A7A" FontSize="10"/>
<TextBlock Text=" DDD = Day of year (001-366)" Foreground="#5C6A7A" FontSize="10"/>
</StackPanel>
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Editors;
public partial class JdeDateEditorView : UserControl
{
public JdeDateEditorView()
{
InitializeComponent();
}
}
@@ -0,0 +1,51 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Editors.PostScriptEditorView"
x:DataType="steps:PostScriptStepViewModel">
<StackPanel Spacing="16">
<!-- Header (dynamic based on script type) -->
<StackPanel>
<TextBlock Text="{Binding DisplayName, StringFormat='{}{0} Configuration'}"
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="SQL script to execute"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Script Content -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Script"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
</StackPanel>
<TextBox Text="{Binding Script}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550"
FontFamily="JetBrains Mono" FontSize="11"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="200"
Watermark="-- SQL Script&#x0a;EXEC dbo.MyProcedure @Param1"/>
<TextBlock Text="SQL script or stored procedure call to execute"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Info Box -->
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="4" Padding="12" Margin="0,8,0,0">
<StackPanel Spacing="4">
<TextBlock Text="Script Execution" Foreground="#9BA8B8" FontSize="11" FontWeight="Medium"/>
<TextBlock Foreground="#5C6A7A" FontSize="10">
<Run Text="Pre-scripts run before data extraction."/>
</TextBlock>
<TextBlock Foreground="#5C6A7A" FontSize="10">
<Run Text="Post-scripts run after data loading completes."/>
</TextBlock>
<TextBlock Foreground="#5C6A7A" FontSize="10">
<Run Text="Use for index management, statistics updates, or cleanup tasks."/>
</TextBlock>
</StackPanel>
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Editors;
public partial class PostScriptEditorView : UserControl
{
public PostScriptEditorView()
{
InitializeComponent();
}
}
@@ -0,0 +1,51 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Editors.ScriptEditorView"
x:DataType="steps:PreScriptStepViewModel">
<StackPanel Spacing="16">
<!-- Header (dynamic based on script type) -->
<StackPanel>
<TextBlock Text="{Binding DisplayName, StringFormat='{}{0} Configuration'}"
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="SQL script to execute"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Script Content -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Script"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
</StackPanel>
<TextBox Text="{Binding Script}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550"
FontFamily="JetBrains Mono" FontSize="11"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="200"
Watermark="-- SQL Script&#x0a;EXEC dbo.MyProcedure @Param1"/>
<TextBlock Text="SQL script or stored procedure call to execute"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Info Box -->
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="4" Padding="12" Margin="0,8,0,0">
<StackPanel Spacing="4">
<TextBlock Text="Script Execution" Foreground="#9BA8B8" FontSize="11" FontWeight="Medium"/>
<TextBlock Foreground="#5C6A7A" FontSize="10">
<Run Text="Pre-scripts run before data extraction."/>
</TextBlock>
<TextBlock Foreground="#5C6A7A" FontSize="10">
<Run Text="Post-scripts run after data loading completes."/>
</TextBlock>
<TextBlock Foreground="#5C6A7A" FontSize="10">
<Run Text="Use for index management, statistics updates, or cleanup tasks."/>
</TextBlock>
</StackPanel>
</Border>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Editors;
public partial class ScriptEditorView : UserControl
{
public ScriptEditorView()
{
InitializeComponent();
}
}
@@ -0,0 +1,156 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
x:Class="JdeScoping.ConfigManager.Views.Editors.SourceEditorView"
x:DataType="steps:SourceStepViewModel">
<StackPanel Spacing="16">
<!-- Header -->
<StackPanel>
<TextBlock Text="Source Configuration"
Foreground="#E6EDF5" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Text="Configure the data source for this pipeline"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Source Type Toggle -->
<StackPanel Spacing="4">
<TextBlock Text="Source Type" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<StackPanel Orientation="Horizontal" Spacing="12">
<RadioButton GroupName="SourceType"
IsChecked="{Binding IsDatabaseSource}"
Foreground="#E6EDF5">
<TextBlock Text="Database" FontSize="12"/>
</RadioButton>
<RadioButton GroupName="SourceType"
IsChecked="{Binding IsFileSource}"
Foreground="#E6EDF5">
<TextBlock Text="File" FontSize="12"/>
</RadioButton>
</StackPanel>
</StackPanel>
<!-- Database Source Fields -->
<StackPanel Spacing="16" IsVisible="{Binding IsDatabaseSource}">
<!-- Connection -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Connection"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding Connection}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="jde"/>
<TextBlock Text="Connection string name (e.g., jde, cms, giw)"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Query -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="Query"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding Query}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550"
FontFamily="JetBrains Mono" FontSize="11"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="100"
Watermark="SELECT ... FROM ... WHERE ..."/>
<TextBlock Text="SQL query for incremental updates (use @LastSync parameter)"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Mass Query -->
<StackPanel Spacing="4">
<TextBlock Text="Mass Query (Optional)"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding MassQuery}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550"
FontFamily="JetBrains Mono" FontSize="11"
AcceptsReturn="True"
TextWrapping="NoWrap"
MinHeight="80"
Watermark="SELECT ... FROM ... (no date filter)"/>
<TextBlock Text="Query for full table reload during mass sync"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Parameters Section -->
<Expander IsExpanded="False">
<Expander.Header>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Parameters" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="{Binding Parameters.Count, StringFormat='({0})'}"
Foreground="#5C6A7A" FontSize="12"/>
</StackPanel>
</Expander.Header>
<StackPanel Spacing="8" Margin="0,8,0,0">
<ItemsControl ItemsSource="{Binding Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="steps:ParameterViewModel">
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="4" Padding="8" Margin="0,0,0,4">
<Grid ColumnDefinitions="*,8,*,8,*,8,Auto">
<StackPanel Grid.Column="0" Spacing="2">
<TextBlock Text="Key" Foreground="#5C6A7A" FontSize="10"/>
<TextBox Text="{Binding Key}"
Background="#232A35" Height="28" FontSize="11"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="2">
<TextBlock Text="Format" Foreground="#5C6A7A" FontSize="10"/>
<TextBox Text="{Binding Format}"
Background="#232A35" Height="28" FontSize="11"
Watermark="jdeJulian"/>
</StackPanel>
<StackPanel Grid.Column="4" Spacing="2">
<TextBlock Text="Source" Foreground="#5C6A7A" FontSize="10"/>
<TextBox Text="{Binding Source}"
Background="#232A35" Height="28" FontSize="11"
Watermark="offset"/>
</StackPanel>
<Button Grid.Column="6" Content="X"
Background="Transparent" Foreground="#FF6B6B"
BorderThickness="0" FontSize="11" Width="24"
VerticalAlignment="Bottom"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="+ Add Parameter"
Background="#232A35" Foreground="#9BA8B8"
BorderBrush="#3D4550" Height="32"
HorizontalAlignment="Left" Padding="12,0"
Command="{Binding AddParameterCommand}"/>
</StackPanel>
</Expander>
</StackPanel>
<!-- File Source Fields -->
<StackPanel Spacing="16" IsVisible="{Binding IsFileSource}">
<!-- File Name -->
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="File Name"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="*" Foreground="#FF6B6B" FontSize="12"/>
</StackPanel>
<TextBox Text="{Binding FileName}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="data.pb.zstd"/>
<TextBlock Text="Protobuf+Zstd compressed file name"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Editors;
public partial class SourceEditorView : UserControl
{
public SourceEditorView()
{
InitializeComponent();
}
}
@@ -0,0 +1,210 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Forms"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
xmlns:controls="using:JdeScoping.ConfigManager.Views.Controls"
xmlns:editors="using:JdeScoping.ConfigManager.Views.Editors"
x:Class="JdeScoping.ConfigManager.Views.Forms.PipelineEditorView"
x:DataType="vm:PipelineEditorViewModel">
<UserControl.Resources>
<!-- Step card colors by type -->
<SolidColorBrush x:Key="PreScriptBrush" Color="#8B5CF6"/>
<SolidColorBrush x:Key="SourceBrush" Color="#3B82F6"/>
<SolidColorBrush x:Key="TransformerBrush" Color="#F59E0B"/>
<SolidColorBrush x:Key="DestinationBrush" Color="#10B981"/>
<SolidColorBrush x:Key="PostScriptBrush" Color="#EC4899"/>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="180"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="*" MinWidth="300"/>
<ColumnDefinition Width="1"/>
<ColumnDefinition Width="350" MinWidth="280"/>
</Grid.ColumnDefinitions>
<!-- Left Panel: Toolbar and Add Buttons -->
<Border Grid.Column="0" Background="#0D0F12" Padding="12">
<StackPanel Spacing="16">
<!-- Header -->
<TextBlock Text="{Binding Name, StringFormat='{}{0}'}"
Foreground="#E6EDF5" FontSize="16" FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<Border Height="1" Background="#2D3540"/>
<!-- Add Steps Section -->
<TextBlock Text="ADD STEPS" Foreground="#5C6A7A" FontSize="11" FontWeight="Medium"/>
<!-- Add Pre-Script Button -->
<Button Command="{Binding AddPreScriptCommand}"
Background="#151920" Foreground="#E6EDF5"
BorderBrush="#2D3540" BorderThickness="1"
HorizontalAlignment="Stretch" Height="36"
HorizontalContentAlignment="Left" Padding="12,0">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="+" Foreground="#8B5CF6" FontWeight="Bold" FontSize="14"/>
<TextBlock Text="Pre-Script" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<!-- Add Transformer Dropdown -->
<StackPanel Spacing="4">
<ComboBox ItemsSource="{Binding AvailableTransformerTypes}"
SelectedItem="{Binding SelectedTransformerType}"
Background="#151920" Foreground="#E6EDF5"
BorderBrush="#2D3540" BorderThickness="1"
HorizontalAlignment="Stretch" Height="32"
PlaceholderText="Select Transformer..."/>
<Button Command="{Binding AddTransformerCommand}"
Background="#151920" Foreground="#E6EDF5"
BorderBrush="#2D3540" BorderThickness="1"
HorizontalAlignment="Stretch" Height="36"
HorizontalContentAlignment="Left" Padding="12,0">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="+" Foreground="#F59E0B" FontWeight="Bold" FontSize="14"/>
<TextBlock Text="Transformer" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<!-- Add Post-Script Button -->
<Button Command="{Binding AddPostScriptCommand}"
Background="#151920" Foreground="#E6EDF5"
BorderBrush="#2D3540" BorderThickness="1"
HorizontalAlignment="Stretch" Height="36"
HorizontalContentAlignment="Left" Padding="12,0">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="+" Foreground="#EC4899" FontWeight="Bold" FontSize="14"/>
<TextBlock Text="Post-Script" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Border Height="1" Background="#2D3540" Margin="0,8,0,0"/>
<!-- Schedules Section -->
<Expander IsExpanded="False">
<Expander.Header>
<TextBlock Text="Schedules" Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
</Expander.Header>
<StackPanel Spacing="8" Margin="0,8,0,0">
<StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox IsChecked="{Binding MassSchedule.Enabled}"/>
<TextBlock Text="Mass" Foreground="#9BA8B8" FontSize="12"/>
<NumericUpDown Value="{Binding MassSchedule.IntervalMinutes}"
Minimum="1" Width="80" Height="28"
Background="#232A35" FontSize="11"
IsEnabled="{Binding MassSchedule.Enabled}"/>
<TextBlock Text="min" Foreground="#5C6A7A" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox IsChecked="{Binding DailySchedule.Enabled}"/>
<TextBlock Text="Daily" Foreground="#9BA8B8" FontSize="12"/>
<NumericUpDown Value="{Binding DailySchedule.IntervalMinutes}"
Minimum="1" Width="80" Height="28"
Background="#232A35" FontSize="11"
IsEnabled="{Binding DailySchedule.Enabled}"/>
<TextBlock Text="min" Foreground="#5C6A7A" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox IsChecked="{Binding HourlySchedule.Enabled}"/>
<TextBlock Text="Hourly" Foreground="#9BA8B8" FontSize="12"/>
<NumericUpDown Value="{Binding HourlySchedule.IntervalMinutes}"
Minimum="1" Width="80" Height="28"
Background="#232A35" FontSize="11"
IsEnabled="{Binding HourlySchedule.Enabled}"/>
<TextBlock Text="min" Foreground="#5C6A7A" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Expander>
</StackPanel>
</Border>
<!-- Divider -->
<Border Grid.Column="1" Background="#2D3540"/>
<!-- Center Panel: Visual Pipeline Flow -->
<Border Grid.Column="2" Background="#151920" Padding="16">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Spacing="0" HorizontalAlignment="Center">
<!-- Pre-Scripts -->
<ItemsControl ItemsSource="{Binding PreScripts}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<controls:PipelineStepCard DataContext="{Binding}"
StepColor="#8B5CF6"/>
<controls:FlowArrow/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Source Step (always present) -->
<controls:PipelineStepCard DataContext="{Binding Source}"
StepColor="#3B82F6"/>
<controls:FlowArrow/>
<!-- Transformers -->
<ItemsControl ItemsSource="{Binding Transformers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<controls:PipelineStepCard DataContext="{Binding}"
StepColor="#F59E0B"/>
<controls:FlowArrow/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Destination Step (always present) -->
<controls:PipelineStepCard DataContext="{Binding Destination}"
StepColor="#10B981"/>
<!-- Post-Scripts -->
<ItemsControl ItemsSource="{Binding PostScripts}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<controls:FlowArrow/>
<controls:PipelineStepCard DataContext="{Binding}"
StepColor="#EC4899"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Border>
<!-- Divider -->
<Border Grid.Column="3" Background="#2D3540"/>
<!-- Right Panel: Properties Editor -->
<Border Grid.Column="4" Background="#0D0F12" Padding="16">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="16">
<!-- Properties Header -->
<StackPanel>
<TextBlock Text="PROPERTIES"
Foreground="#5C6A7A" FontSize="11" FontWeight="Medium"/>
<Border Height="1" Background="#2D3540" Margin="0,8,0,0"/>
</StackPanel>
<!-- Step Editor Content (changes based on selection) -->
<!-- Shows placeholder text when nothing selected, otherwise uses DataTemplates from MainWindow -->
<TextBlock Text="Select a step to edit its properties"
Foreground="#5C6A7A" FontSize="12"
FontStyle="Italic"
IsVisible="{Binding SelectedStepEditor, Converter={x:Static ObjectConverters.IsNull}}"/>
<ContentControl Content="{Binding SelectedStepEditor}"
IsVisible="{Binding SelectedStepEditor, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Forms;
public partial class PipelineEditorView : UserControl
{
public PipelineEditorView()
{
InitializeComponent();
}
}
@@ -2,7 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels"
xmlns:forms="using:JdeScoping.ConfigManager.ViewModels.Forms"
xmlns:steps="using:JdeScoping.ConfigManager.ViewModels.PipelineSteps"
xmlns:views="using:JdeScoping.ConfigManager.Views.Forms"
xmlns:editors="using:JdeScoping.ConfigManager.Views.Editors"
x:Class="JdeScoping.ConfigManager.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="JdeScoping ConfigManager"
@@ -15,6 +17,7 @@
</Design.DataContext>
<Window.DataTemplates>
<!-- Settings Form ViewModels -->
<DataTemplate DataType="{x:Type forms:DataSyncFormViewModel}">
<views:DataSyncFormView/>
</DataTemplate>
@@ -33,9 +36,36 @@
<DataTemplate DataType="{x:Type forms:ExcelExportFormViewModel}">
<views:ExcelExportFormView/>
</DataTemplate>
<DataTemplate DataType="{x:Type forms:PipelineFormViewModel}">
<views:PipelineFormView/>
<!-- Pipeline Editor (replaces PipelineFormViewModel) -->
<DataTemplate DataType="{x:Type forms:PipelineEditorViewModel}">
<views:PipelineEditorView/>
</DataTemplate>
<!-- Pipeline Step Editors (for the properties panel) -->
<DataTemplate DataType="{x:Type steps:SourceStepViewModel}">
<editors:SourceEditorView/>
</DataTemplate>
<DataTemplate DataType="{x:Type steps:DestinationStepViewModel}">
<editors:DestinationEditorView/>
</DataTemplate>
<DataTemplate DataType="{x:Type steps:ColumnDropTransformerViewModel}">
<editors:ColumnDropEditorView/>
</DataTemplate>
<DataTemplate DataType="{x:Type steps:ColumnRenameTransformerViewModel}">
<editors:ColumnRenameEditorView/>
</DataTemplate>
<DataTemplate DataType="{x:Type steps:JdeDateTransformerViewModel}">
<editors:JdeDateEditorView/>
</DataTemplate>
<DataTemplate DataType="{x:Type steps:PreScriptStepViewModel}">
<editors:ScriptEditorView/>
</DataTemplate>
<DataTemplate DataType="{x:Type steps:PostScriptStepViewModel}">
<editors:PostScriptEditorView/>
</DataTemplate>
<!-- SecureStore ViewModels -->
<DataTemplate DataType="{x:Type forms:SecureStoreLockedFormViewModel}">
<views:SecureStoreLockedFormView/>
</DataTemplate>
@@ -66,6 +96,18 @@
<Separator/>
<MenuItem Header="View _Backups..."/>
</MenuItem>
<MenuItem Header="_Pipelines">
<MenuItem Header="_New Pipeline..." Command="{Binding AddPipelineCommand}" InputGesture="Ctrl+Shift+P">
<MenuItem.Icon>
<TextBlock Text="+" FontSize="14" FontWeight="Bold"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Delete Pipeline" Command="{Binding DeletePipelineCommand}">
<MenuItem.Icon>
<TextBlock Text="X" FontSize="12" FontWeight="Bold"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Secure Stores">
<MenuItem Header="_New Store..." Command="{Binding NewStoreCommand}" InputGesture="Ctrl+Shift+N">
<MenuItem.Icon>
@@ -113,6 +155,8 @@
<Button Content="Test" Command="{Binding TestConnectionCommand}" Classes="toolbar"/>
<Button Content="Validate" Command="{Binding ValidateCommand}" Classes="toolbar"/>
<Border Width="1" Height="20" Background="#2D3540" Margin="4,0"/>
<Button Content="+ Pipeline" Command="{Binding AddPipelineCommand}" ToolTip.Tip="Add Pipeline" Classes="toolbar"/>
<Border Width="1" Height="20" Background="#2D3540" Margin="4,0"/>
<Button Content="Unlock" Command="{Binding UnlockStoreCommand}" ToolTip.Tip="Unlock/Lock Store" Classes="toolbar"/>
<Button Content="+ Secret" Command="{Binding AddSecretCommand}" ToolTip.Tip="Add Secret" Classes="toolbar"/>
</StackPanel>
@@ -166,6 +210,9 @@
Margin="8">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="New Pipeline..." Command="{Binding AddPipelineCommand}"/>
<MenuItem Header="Delete Pipeline" Command="{Binding DeletePipelineCommand}"/>
<Separator/>
<MenuItem Header="Unlock Store..." Command="{Binding UnlockStoreCommand}"/>
<MenuItem Header="Lock Store" Command="{Binding LockStoreCommand}"/>
<Separator/>