feat: Add Instance Details ViewModel and UI for managing instance credentials

- Introduced InstanceDetailsViewModel to handle loading and displaying instance-specific credentials.
- Created InstanceDetailsWindow and associated XAML for displaying admin, database, and OAuth2 credentials.
- Updated InstancesViewModel to include command for opening instance details.
- Enhanced SettingsViewModel to manage Bitwarden and Xibo Bootstrap configurations, including connection testing.
- Added UI components for Bitwarden Secrets Manager and Xibo Bootstrap OAuth2 settings in the SettingsView.
- Implemented password visibility toggles and clipboard copy functionality for sensitive information.
This commit is contained in:
Matt Batchelder
2026-02-25 08:05:44 -05:00
parent 28e79459ac
commit a1c987ff21
17 changed files with 1608 additions and 42 deletions

View File

@@ -0,0 +1,146 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:OTSSignsOrchestrator.Desktop.ViewModels"
x:Class="OTSSignsOrchestrator.Desktop.Views.InstanceDetailsWindow"
x:DataType="vm:InstanceDetailsViewModel"
Title="Instance Details"
Width="620" Height="740"
MinWidth="520" MinHeight="600"
WindowStartupLocation="CenterOwner"
CanResize="True">
<DockPanel Margin="24">
<!-- Header -->
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,16">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="{Binding StackName}" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource AccentBrush}" />
</StackPanel>
<TextBlock Text="{Binding HostLabel, StringFormat='Host: {0}'}"
FontSize="12" Foreground="{StaticResource TextMutedBrush}" Margin="0,2,0,0" />
<TextBlock Text="{Binding InstanceUrl}"
FontSize="11" Foreground="{StaticResource TextMutedBrush}" />
</StackPanel>
<!-- Status bar -->
<TextBlock DockPanel.Dock="Bottom" Text="{Binding StatusMessage}" Classes="status"
Margin="0,12,0,0" TextWrapping="Wrap" />
<!-- Main scrollable content -->
<ScrollViewer>
<StackPanel Spacing="16">
<!-- ═══ OTS Admin Account ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
<Border Width="4" Height="20" CornerRadius="2" Background="#F97316" />
<TextBlock Text="OTS Admin Account" FontSize="16" FontWeight="SemiBold"
Foreground="#F97316" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Username" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding AdminUsername}" IsReadOnly="True" />
<Button Grid.Column="1" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyAdminPasswordCommand}" />
</Grid>
<TextBlock Text="Password" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<TextBox Grid.Column="0" Text="{Binding AdminPasswordDisplay}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Show" Margin="4,0,0,0"
IsVisible="{Binding !AdminPasswordVisible}"
Command="{Binding ToggleAdminPasswordVisibilityCommand}" />
<Button Grid.Column="2" Content="Hide" Margin="4,0,0,0"
IsVisible="{Binding AdminPasswordVisible}"
Command="{Binding ToggleAdminPasswordVisibilityCommand}" />
<Button Grid.Column="3" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyAdminPasswordCommand}" />
</Grid>
<Button Content="Rotate Admin Password"
Command="{Binding RotateAdminPasswordCommand}"
IsEnabled="{Binding !IsBusy}"
Classes="accent"
Margin="0,6,0,0" />
</StackPanel>
</Border>
<!-- ═══ Database Credentials ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
<Border Width="4" Height="20" CornerRadius="2" Background="#4ADE80" />
<TextBlock Text="Database Credentials" FontSize="16" FontWeight="SemiBold"
Foreground="#4ADE80" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="MySQL Username" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<Grid ColumnDefinitions="*">
<TextBox Text="{Binding DbUsername}" IsReadOnly="True" />
</Grid>
<TextBlock Text="MySQL Password" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<TextBox Grid.Column="0" Text="{Binding DbPasswordDisplay}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Show" Margin="4,0,0,0"
IsVisible="{Binding !DbPasswordVisible}"
Command="{Binding ToggleDbPasswordVisibilityCommand}" />
<Button Grid.Column="2" Content="Hide" Margin="4,0,0,0"
IsVisible="{Binding DbPasswordVisible}"
Command="{Binding ToggleDbPasswordVisibilityCommand}" />
<Button Grid.Column="3" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyDbPasswordCommand}" />
</Grid>
<Button Content="Rotate DB Password"
Command="{Binding RotateDbPasswordCommand}"
IsEnabled="{Binding !IsBusy}"
Margin="0,6,0,0" />
</StackPanel>
</Border>
<!-- ═══ Xibo OAuth2 Application ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
<Border Width="4" Height="20" CornerRadius="2" Background="#60A5FA" />
<TextBlock Text="OTS OAuth2 Application" FontSize="16" FontWeight="SemiBold"
Foreground="#60A5FA" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Client credentials used by the OTS orchestrator for Xibo API access."
FontSize="12" Foreground="{StaticResource TextMutedBrush}" Margin="0,0,0,6"
TextWrapping="Wrap" />
<TextBlock Text="Client ID" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding OAuthClientId}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyOAuthClientIdCommand}" />
</Grid>
<TextBlock Text="Client Secret" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<TextBox Grid.Column="0" Text="{Binding OAuthSecretDisplay}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Show" Margin="4,0,0,0"
IsVisible="{Binding !OAuthSecretVisible}"
Command="{Binding ToggleOAuthSecretVisibilityCommand}" />
<Button Grid.Column="2" Content="Hide" Margin="4,0,0,0"
IsVisible="{Binding OAuthSecretVisible}"
Command="{Binding ToggleOAuthSecretVisibilityCommand}" />
<Button Grid.Column="3" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyOAuthSecretCommand}" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</DockPanel>
</Window>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace OTSSignsOrchestrator.Desktop.Views;
public partial class InstanceDetailsWindow : Window
{
public InstanceDetailsWindow()
{
InitializeComponent();
}
}

View File

@@ -16,6 +16,9 @@
<Grid ColumnDefinitions="*,Auto">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Refresh" Command="{Binding LoadInstancesCommand}" />
<Button Content="Details" Classes="accent" Command="{Binding OpenDetailsCommand}"
IsEnabled="{Binding !IsBusy}"
ToolTip.Tip="View credentials and manage this instance." />
<Button Content="Inspect" Command="{Binding InspectInstanceCommand}" />
<Button Content="Delete" Classes="danger" Command="{Binding DeleteInstanceCommand}" />
<Border Width="1" Background="{StaticResource BorderSubtleBrush}" Margin="4,2" />

View File

@@ -1,11 +1,37 @@
using Avalonia.Controls;
using OTSSignsOrchestrator.Desktop.ViewModels;
namespace OTSSignsOrchestrator.Desktop.Views;
public partial class InstancesView : UserControl
{
private InstancesViewModel? _vm;
public InstancesView()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_vm is not null)
_vm.OpenDetailsRequested -= OnOpenDetailsRequested;
_vm = DataContext as InstancesViewModel;
if (_vm is not null)
_vm.OpenDetailsRequested += OnOpenDetailsRequested;
}
private async void OnOpenDetailsRequested(InstanceDetailsViewModel detailsVm)
{
var window = new InstanceDetailsWindow { DataContext = detailsVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null)
await window.ShowDialog(owner);
else
window.Show();
}
}

View File

@@ -231,6 +231,77 @@
</StackPanel>
</Border>
<!-- ═══ Bitwarden Secrets Manager ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
<Border Width="4" Height="20" CornerRadius="2" Background="#818CF8" />
<TextBlock Text="Bitwarden Secrets Manager" FontSize="16" FontWeight="SemiBold"
Foreground="#818CF8" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Stores per-instance admin passwords and OAuth2 secrets. Uses a machine account access token."
FontSize="12" Foreground="{StaticResource TextMutedBrush}" Margin="0,0,0,6"
TextWrapping="Wrap" />
<Grid ColumnDefinitions="1*,12,1*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Identity URL" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenIdentityUrl}"
Watermark="https://identity.bitwarden.com" />
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="API URL" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenApiUrl}"
Watermark="https://api.bitwarden.com" />
</StackPanel>
</Grid>
<TextBlock Text="Machine Account Access Token" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenAccessToken}" PasswordChar="●"
Watermark="0.xxxxxxxx.yyyyyyy:zzzzzz" />
<TextBlock Text="Organization ID" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenOrganizationId}"
Watermark="00000000-0000-0000-0000-000000000000" />
<TextBlock Text="Project ID (optional — secrets are organized into this project)" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenProjectId}"
Watermark="00000000-0000-0000-0000-000000000000" />
<Button Content="Test Bitwarden Connection"
Command="{Binding TestBitwardenConnectionCommand}"
IsEnabled="{Binding !IsBusy}"
Margin="0,6,0,0" />
</StackPanel>
</Border>
<!-- ═══ Xibo Bootstrap OAuth2 ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
<Border Width="4" Height="20" CornerRadius="2" Background="#F97316" />
<TextBlock Text="Xibo Bootstrap OAuth2" FontSize="16" FontWeight="SemiBold"
Foreground="#F97316" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="A pre-configured Xibo OAuth2 client_credentials application used for post-install setup (creating admin users, registering OTS app, setting theme). Create once in the Xibo admin panel of any instance."
FontSize="12" Foreground="{StaticResource TextMutedBrush}" Margin="0,0,0,6"
TextWrapping="Wrap" />
<TextBlock Text="Bootstrap Client ID" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding XiboBootstrapClientId}"
Watermark="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" />
<TextBlock Text="Bootstrap Client Secret" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding XiboBootstrapClientSecret}" PasswordChar="●" />
<Button Content="Save &amp; Verify"
Command="{Binding TestXiboBootstrapCommand}"
IsEnabled="{Binding !IsBusy}"
Margin="0,6,0,0" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</DockPanel>