244 lines
7.8 KiB
C#
244 lines
7.8 KiB
C#
|
|
using System.Collections.ObjectModel;
|
||
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||
|
|
using CommunityToolkit.Mvvm.Input;
|
||
|
|
using Microsoft.EntityFrameworkCore;
|
||
|
|
using Microsoft.Extensions.DependencyInjection;
|
||
|
|
using OTSSignsOrchestrator.Core.Data;
|
||
|
|
using OTSSignsOrchestrator.Core.Models.Entities;
|
||
|
|
using OTSSignsOrchestrator.Desktop.Services;
|
||
|
|
|
||
|
|
namespace OTSSignsOrchestrator.Desktop.ViewModels;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// ViewModel for managing SSH host connections.
|
||
|
|
/// Allows adding, editing, testing, and removing remote Docker Swarm hosts.
|
||
|
|
/// </summary>
|
||
|
|
public partial class HostsViewModel : ObservableObject
|
||
|
|
{
|
||
|
|
private readonly IServiceProvider _services;
|
||
|
|
|
||
|
|
[ObservableProperty] private ObservableCollection<SshHost> _hosts = new();
|
||
|
|
[ObservableProperty] private SshHost? _selectedHost;
|
||
|
|
[ObservableProperty] private bool _isEditing;
|
||
|
|
[ObservableProperty] private string _statusMessage = string.Empty;
|
||
|
|
[ObservableProperty] private bool _isBusy;
|
||
|
|
|
||
|
|
// Edit form fields
|
||
|
|
[ObservableProperty] private string _editLabel = string.Empty;
|
||
|
|
[ObservableProperty] private string _editHost = string.Empty;
|
||
|
|
[ObservableProperty] private int _editPort = 22;
|
||
|
|
[ObservableProperty] private string _editUsername = string.Empty;
|
||
|
|
[ObservableProperty] private string _editPrivateKeyPath = string.Empty;
|
||
|
|
[ObservableProperty] private string _editKeyPassphrase = string.Empty;
|
||
|
|
[ObservableProperty] private string _editPassword = string.Empty;
|
||
|
|
[ObservableProperty] private bool _editUseKeyAuth = true;
|
||
|
|
private Guid? _editingHostId;
|
||
|
|
|
||
|
|
public HostsViewModel(IServiceProvider services)
|
||
|
|
{
|
||
|
|
_services = services;
|
||
|
|
_ = LoadHostsAsync();
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
private async Task LoadHostsAsync()
|
||
|
|
{
|
||
|
|
IsBusy = true;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
using var scope = _services.CreateScope();
|
||
|
|
var db = scope.ServiceProvider.GetRequiredService<XiboContext>();
|
||
|
|
var hosts = await db.SshHosts.OrderBy(h => h.Label).ToListAsync();
|
||
|
|
|
||
|
|
Hosts = new ObservableCollection<SshHost>(hosts);
|
||
|
|
StatusMessage = $"Loaded {hosts.Count} host(s).";
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
StatusMessage = $"Error loading hosts: {ex.Message}";
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
IsBusy = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
private void NewHost()
|
||
|
|
{
|
||
|
|
_editingHostId = null;
|
||
|
|
EditLabel = string.Empty;
|
||
|
|
EditHost = string.Empty;
|
||
|
|
EditPort = 22;
|
||
|
|
EditUsername = string.Empty;
|
||
|
|
EditPrivateKeyPath = string.Empty;
|
||
|
|
EditKeyPassphrase = string.Empty;
|
||
|
|
EditPassword = string.Empty;
|
||
|
|
EditUseKeyAuth = true;
|
||
|
|
IsEditing = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
private void EditSelectedHost()
|
||
|
|
{
|
||
|
|
if (SelectedHost == null) return;
|
||
|
|
|
||
|
|
_editingHostId = SelectedHost.Id;
|
||
|
|
EditLabel = SelectedHost.Label;
|
||
|
|
EditHost = SelectedHost.Host;
|
||
|
|
EditPort = SelectedHost.Port;
|
||
|
|
EditUsername = SelectedHost.Username;
|
||
|
|
EditPrivateKeyPath = SelectedHost.PrivateKeyPath ?? string.Empty;
|
||
|
|
EditKeyPassphrase = string.Empty; // Don't show existing passphrase
|
||
|
|
EditPassword = string.Empty; // Don't show existing password
|
||
|
|
EditUseKeyAuth = SelectedHost.UseKeyAuth;
|
||
|
|
IsEditing = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
private void CancelEdit()
|
||
|
|
{
|
||
|
|
IsEditing = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
private async Task SaveHostAsync()
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(EditLabel) || string.IsNullOrWhiteSpace(EditHost) || string.IsNullOrWhiteSpace(EditUsername))
|
||
|
|
{
|
||
|
|
StatusMessage = "Label, Host, and Username are required.";
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
IsBusy = true;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
using var scope = _services.CreateScope();
|
||
|
|
var db = scope.ServiceProvider.GetRequiredService<XiboContext>();
|
||
|
|
|
||
|
|
SshHost host;
|
||
|
|
if (_editingHostId.HasValue)
|
||
|
|
{
|
||
|
|
host = await db.SshHosts.FindAsync(_editingHostId.Value)
|
||
|
|
?? throw new KeyNotFoundException("Host not found.");
|
||
|
|
|
||
|
|
host.Label = EditLabel;
|
||
|
|
host.Host = EditHost;
|
||
|
|
host.Port = EditPort;
|
||
|
|
host.Username = EditUsername;
|
||
|
|
host.PrivateKeyPath = string.IsNullOrWhiteSpace(EditPrivateKeyPath) ? null : EditPrivateKeyPath;
|
||
|
|
host.UseKeyAuth = EditUseKeyAuth;
|
||
|
|
host.UpdatedAt = DateTime.UtcNow;
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(EditKeyPassphrase))
|
||
|
|
host.KeyPassphrase = EditKeyPassphrase;
|
||
|
|
if (!string.IsNullOrEmpty(EditPassword))
|
||
|
|
host.Password = EditPassword;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
host = new SshHost
|
||
|
|
{
|
||
|
|
Label = EditLabel,
|
||
|
|
Host = EditHost,
|
||
|
|
Port = EditPort,
|
||
|
|
Username = EditUsername,
|
||
|
|
PrivateKeyPath = string.IsNullOrWhiteSpace(EditPrivateKeyPath) ? null : EditPrivateKeyPath,
|
||
|
|
KeyPassphrase = string.IsNullOrEmpty(EditKeyPassphrase) ? null : EditKeyPassphrase,
|
||
|
|
Password = string.IsNullOrEmpty(EditPassword) ? null : EditPassword,
|
||
|
|
UseKeyAuth = EditUseKeyAuth
|
||
|
|
};
|
||
|
|
db.SshHosts.Add(host);
|
||
|
|
}
|
||
|
|
|
||
|
|
await db.SaveChangesAsync();
|
||
|
|
|
||
|
|
IsEditing = false;
|
||
|
|
StatusMessage = $"Host '{host.Label}' saved.";
|
||
|
|
await LoadHostsAsync();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
StatusMessage = $"Error saving host: {ex.Message}";
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
IsBusy = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
private async Task DeleteHostAsync()
|
||
|
|
{
|
||
|
|
if (SelectedHost == null) return;
|
||
|
|
|
||
|
|
IsBusy = true;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
using var scope = _services.CreateScope();
|
||
|
|
var db = scope.ServiceProvider.GetRequiredService<XiboContext>();
|
||
|
|
|
||
|
|
var host = await db.SshHosts.FindAsync(SelectedHost.Id);
|
||
|
|
if (host != null)
|
||
|
|
{
|
||
|
|
db.SshHosts.Remove(host);
|
||
|
|
await db.SaveChangesAsync();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Disconnect if connected
|
||
|
|
var ssh = _services.GetRequiredService<SshConnectionService>();
|
||
|
|
ssh.Disconnect(SelectedHost.Id);
|
||
|
|
|
||
|
|
StatusMessage = $"Host '{SelectedHost.Label}' deleted.";
|
||
|
|
await LoadHostsAsync();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
StatusMessage = $"Error deleting host: {ex.Message}";
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
IsBusy = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[RelayCommand]
|
||
|
|
private async Task TestConnectionAsync()
|
||
|
|
{
|
||
|
|
if (SelectedHost == null) return;
|
||
|
|
|
||
|
|
IsBusy = true;
|
||
|
|
StatusMessage = $"Testing connection to {SelectedHost.Label}...";
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var ssh = _services.GetRequiredService<SshConnectionService>();
|
||
|
|
var (success, message) = await ssh.TestConnectionAsync(SelectedHost);
|
||
|
|
|
||
|
|
// Update DB
|
||
|
|
using var scope = _services.CreateScope();
|
||
|
|
var db = scope.ServiceProvider.GetRequiredService<XiboContext>();
|
||
|
|
var host = await db.SshHosts.FindAsync(SelectedHost.Id);
|
||
|
|
if (host != null)
|
||
|
|
{
|
||
|
|
host.LastTestedAt = DateTime.UtcNow;
|
||
|
|
host.LastTestSuccess = success;
|
||
|
|
await db.SaveChangesAsync();
|
||
|
|
}
|
||
|
|
|
||
|
|
StatusMessage = success
|
||
|
|
? $"✓ {SelectedHost.Label}: {message}"
|
||
|
|
: $"✗ {SelectedHost.Label}: {message}";
|
||
|
|
|
||
|
|
await LoadHostsAsync();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
StatusMessage = $"Connection test error: {ex.Message}";
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
IsBusy = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|