Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .idea/mcp.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
"avalonia-build-mcp-server": {
"url": "https://docs-mcp.avaloniaui.net/mcp",
"type": "http"
},
"avalonia_devtools": {
"type": "stdio",
"command": "avdt",
"args": ["mcp"]
}
},
"inputs": []
Expand Down
2 changes: 1 addition & 1 deletion src/Knarr.App/Features/Containers/ContainersView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@
</TableViewColumn>

<!-- Actions (fixed width so it never shrinks) -->
<TableViewColumn Header="Actions" Width="120" HorizontalContentAlignment="Left">
<TableViewColumn Header="Actions" Width="140" HorizontalContentAlignment="Left">
<TableViewColumn.CellTemplate>
<DataTemplate x:DataType="models:ContainerItem">
<StackPanel Orientation="Horizontal" Spacing="2" VerticalAlignment="Center">
Expand Down
33 changes: 33 additions & 0 deletions src/Knarr.App/Features/Containers/ContainersView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
using Avalonia.Controls;
using Knarr.App.Features.RunContainer;

namespace Knarr.App.Features.Containers;

public partial class ContainersView : UserControl
{
private ContainersViewModel? _viewModel;

public ContainersView()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}

private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_viewModel is not null)
{
_viewModel.RunDialogRequested -= OnRunDialogRequested;
}

_viewModel = DataContext as ContainersViewModel;

if (_viewModel is not null)
{
_viewModel.RunDialogRequested += OnRunDialogRequested;
}
}

private async void OnRunDialogRequested(object? sender, RunContainerDialogViewModel dialogViewModel)
{
RunContainerDialog dialog = new() { DataContext = dialogViewModel };

if (TopLevel.GetTopLevel(this) is Window owner)
{
await dialog.ShowDialog(owner);
}
else
{
dialog.Show();
}
}
}
26 changes: 24 additions & 2 deletions src/Knarr.App/Features/Containers/ContainersViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Avalonia.Threading;
using Knarr.App.Features.RunContainer;
using Knarr.Service.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -17,15 +18,20 @@ public partial class ContainersViewModel : ViewModelBase, IDisposable

private readonly IContainerCliProvider _cliProvider;
private readonly ILogger<ContainersViewModel> _logger;
private readonly Func<RunContainerDialogViewModel>? _runDialogFactory;
private readonly List<ContainerItem> _allContainers = [];

private DispatcherTimer? _refreshTimer;
private bool _loadInFlight;

public ContainersViewModel(IContainerCliProvider cliProvider, ILogger<ContainersViewModel> logger)
public ContainersViewModel(
IContainerCliProvider cliProvider,
ILogger<ContainersViewModel> logger,
Func<RunContainerDialogViewModel>? runDialogFactory = null)
{
_cliProvider = cliProvider;
_logger = logger;
_runDialogFactory = runDialogFactory;
Containers = new ObservableCollection<ContainerItem>();

// Kick off the initial load; property updates marshal back to the UI thread.
Expand All @@ -43,6 +49,12 @@ public ContainersViewModel()

public ObservableCollection<ContainerItem> Containers { get; }

/// <summary>
/// Raised when the run-container dialog should be shown. The view resolves the owner window and
/// displays the supplied, already-initialised dialog view model modally.
/// </summary>
public event EventHandler<RunContainerDialogViewModel>? RunDialogRequested;

/// <summary>Total number of containers, independent of the current search filter.</summary>
public int TotalCount => _allContainers.Count;

Expand Down Expand Up @@ -234,9 +246,19 @@ private Task Refresh()
[RelayCommand]
private void RunContainer()
{
// Run wizard is a later milestone.
if (_runDialogFactory is null)
{
return;
}

RunContainerDialogViewModel dialogViewModel = _runDialogFactory();
dialogViewModel.Reset(initialImage: null, imageEditable: true);
dialogViewModel.ContainerStarted += OnContainerStarted;
RunDialogRequested?.Invoke(this, dialogViewModel);
}

private void OnContainerStarted(object? sender, EventArgs e) => _ = LoadAsync();

// Bulk (multiselect) commands — the provider runs each batch as a single command session.
[RelayCommand]
private Task StartSelected()
Expand Down
2 changes: 1 addition & 1 deletion src/Knarr.App/Features/Images/ImagesView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
</TableViewColumn>

<!-- Image (selectable text) -->
<TableViewColumn Header="Image" Width="*">
<TableViewColumn Header="Image" Width="2*">
<TableViewColumn.CellTemplate>
<DataTemplate x:DataType="models:ImageItem">
<StackPanel VerticalAlignment="Center" Spacing="2">
Expand Down
17 changes: 17 additions & 0 deletions src/Knarr.App/Features/Images/ImagesView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Knarr.App.Features.RunContainer;

namespace Knarr.App.Features.Images;

Expand All @@ -19,13 +20,15 @@ private void OnDataContextChanged(object? sender, EventArgs e)
if (_viewModel is not null)
{
_viewModel.PullDialogRequested -= OnPullDialogRequested;
_viewModel.RunDialogRequested -= OnRunDialogRequested;
}

_viewModel = DataContext as ImagesViewModel;

if (_viewModel is not null)
{
_viewModel.PullDialogRequested += OnPullDialogRequested;
_viewModel.RunDialogRequested += OnRunDialogRequested;
}
}

Expand All @@ -43,6 +46,20 @@ private async void OnPullDialogRequested(object? sender, PullImageDialogViewMode
}
}

private async void OnRunDialogRequested(object? sender, RunContainerDialogViewModel dialogViewModel)
{
RunContainerDialog dialog = new() { DataContext = dialogViewModel };

if (TopLevel.GetTopLevel(this) is Window owner)
{
await dialog.ShowDialog(owner);
}
else
{
dialog.Show();
}
}

private async void OnCopyImageName(object? sender, RoutedEventArgs e)
{
if (sender is Button { DataContext: ImageItem image } &&
Expand Down
24 changes: 22 additions & 2 deletions src/Knarr.App/Features/Images/ImagesViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Avalonia.Threading;
using Knarr.App.Features.RunContainer;
using Knarr.Service.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -12,6 +13,7 @@ public partial class ImagesViewModel : ViewModelBase, IDisposable
private readonly IContainerCliProvider _cliProvider;
private readonly ILogger<ImagesViewModel> _logger;
private readonly Func<PullImageDialogViewModel>? _pullDialogFactory;
private readonly Func<RunContainerDialogViewModel>? _runDialogFactory;
private readonly List<ImageItem> _allImages = [];

private DispatcherTimer? _refreshTimer;
Expand All @@ -20,11 +22,13 @@ public partial class ImagesViewModel : ViewModelBase, IDisposable
public ImagesViewModel(
IContainerCliProvider cliProvider,
ILogger<ImagesViewModel> logger,
Func<PullImageDialogViewModel>? pullDialogFactory = null)
Func<PullImageDialogViewModel>? pullDialogFactory = null,
Func<RunContainerDialogViewModel>? runDialogFactory = null)
{
_cliProvider = cliProvider;
_logger = logger;
_pullDialogFactory = pullDialogFactory;
_runDialogFactory = runDialogFactory;
Images = [];
_ = LoadAsync();
StartAutoRefresh();
Expand All @@ -46,6 +50,12 @@ public ImagesViewModel()
/// </summary>
public event EventHandler<PullImageDialogViewModel>? PullDialogRequested;

/// <summary>
/// Raised when a run-container dialog should be shown. The view resolves the owner window and
/// displays the supplied, already-initialised dialog view model modally.
/// </summary>
public event EventHandler<RunContainerDialogViewModel>? RunDialogRequested;

[ObservableProperty]
public partial string SearchText { get; set; } = string.Empty;

Expand Down Expand Up @@ -204,9 +214,19 @@ private Task DeleteSelected()
[RelayCommand]
private void Run(ImageItem image)
{
// Run wizard is a later milestone.
if (_runDialogFactory is null)
{
return;
}

RunContainerDialogViewModel dialogViewModel = _runDialogFactory();
dialogViewModel.Reset(image.RepoTag, imageEditable: false);
dialogViewModel.ContainerStarted += OnContainerStarted;
RunDialogRequested?.Invoke(this, dialogViewModel);
}

private void OnContainerStarted(object? sender, EventArgs e) => _ = LoadAsync();

[RelayCommand]
private void Tag(ImageItem image)
{
Expand Down
17 changes: 4 additions & 13 deletions src/Knarr.App/Features/Images/PullImageDialog.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,19 @@ public PullImageDialog()

private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_viewModel is not null)
{
_viewModel.CloseRequested -= OnCloseRequested;
}
_viewModel?.CloseRequested -= OnCloseRequested;

_viewModel = DataContext as PullImageDialogViewModel;

if (_viewModel is not null)
{
_viewModel.CloseRequested += OnCloseRequested;
}
_viewModel?.CloseRequested += OnCloseRequested;
}

private void OnCloseRequested(object? sender, EventArgs e) => Close();

private void OnClosed(object? sender, EventArgs e)
{
if (_viewModel is not null)
{
_viewModel.CloseRequested -= OnCloseRequested;
_viewModel = null;
}
_viewModel?.CloseRequested -= OnCloseRequested;
_viewModel = null;

DataContextChanged -= OnDataContextChanged;
Closed -= OnClosed;
Expand Down
11 changes: 11 additions & 0 deletions src/Knarr.App/Features/RunContainer/EnvironmentVariableEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Knarr.App.Features.RunContainer;

/// <summary>An editable environment-variable row (<c>KEY=VALUE</c>) in the run-container dialog.</summary>
public partial class EnvironmentVariableEntry : ObservableObject
{
[ObservableProperty]
public partial string Key { get; set; } = string.Empty;

[ObservableProperty]
public partial string Value { get; set; } = string.Empty;
}
11 changes: 11 additions & 0 deletions src/Knarr.App/Features/RunContainer/PortMappingEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Knarr.App.Features.RunContainer;

/// <summary>An editable port-mapping row (host port to container port) in the run-container dialog.</summary>
public partial class PortMappingEntry : ObservableObject
{
[ObservableProperty]
public partial string HostPort { get; set; } = string.Empty;

[ObservableProperty]
public partial string ContainerPort { get; set; } = string.Empty;
}
Loading
Loading