(feature): set kubectl current-context with catalog radios

This commit is contained in:
Maksym Sadovnychyy 2026-08-20 17:10:26 +02:00
parent a741640858
commit bf857135b6
8 changed files with 125 additions and 40 deletions

View File

@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]
## [0.3.0] - 2026-08-20
### Changed
- Catalog radios set kubectl current-context. The asterisk marker and Connections **Use for kubectl** button are gone.
## [0.2.1] - 2026-08-20
### Fixed

View File

@ -26,11 +26,11 @@ Capabilities that are first-class in ClusterConsole, not afterthoughts:
- **Dapr in the navigator** — Components, Configurations, Subscriptions, Resiliency, HTTPEndpoints, sidecars, and control-plane pods as catalog views, not a generic CRD dump.
- **Volume files** — browse, edit, download, and upload files on persistent volumes and claims from the desktop.
- **Limits you can fix** — overview shows container CPU and memory against node capacity and can patch limits that oversubscribe the node.
- **Connections stay in the app** — wizard to add or update a context (token, client certificate, or basic auth) and switch current-context without a separate kubeconfig editor.
- **Connections stay in the app** — wizard to add or update a context (token, client certificate, or basic auth). A catalog radio sets kubectl current-context.
## Features
- **Contexts** — catalog of kubeconfig contexts; mark and switch the current context
- **Contexts** — catalog of kubeconfig contexts; radio selects kubectl current-context
- **Navigator** — Cluster, Nodes, Applications, Workloads, Config, Network, Storage, Namespaces, Events, Helm, Dapr, Access Control, Custom Resources
- **Resource tables** — list and refresh any catalogued type; per-column filters; namespace scope persisted per context
- **Inspect and apply** — YAML view, apply, create, delete; force-delete (grace period 0 and strip finalizers), including namespaces whose objects are already gone

View File

@ -3,7 +3,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.2.1</Version>
<Version>0.3.0</Version>
<Product>MaksIT.ClusterConsole</Product>
<AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle>
</PropertyGroup>

View File

@ -22,7 +22,6 @@
<StackPanel DockPanel.Dock="Top" Margin="12,12,12,8">
<TextBlock Text="MaksIT.ClusterConsole" FontSize="18" FontWeight="SemiBold" />
<TextBlock Text="Catalog" Classes="hint" Margin="0,4,0,0" />
<TextBlock Text="* kubectl current-context" Classes="hint" Margin="0,2,0,0" />
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="12" Spacing="6">
<Button Content="Disconnect" Command="{Binding DisconnectCommand}" IsVisible="{Binding IsClusterOpen}" HorizontalAlignment="Stretch" />
@ -34,23 +33,28 @@
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CatalogItemViewModel">
<Grid ColumnDefinitions="*,Auto" Margin="0,1">
<Button Classes="catalog-item"
Command="{Binding ActivateCommand}"
Classes.selected="{Binding IsActive}"
Classes.connected="{Binding IsConnected}">
<StackPanel>
<Grid ColumnDefinitions="Auto,Auto,*">
<TextBlock Text="*"
Margin="0,0,4,0"
Classes="kubectl-current"
IsVisible="{Binding IsKubectlCurrent}"
ToolTip.Tip="kubectl current-context" />
<TextBlock Grid.Column="1" Text="●" Margin="0,0,6,0" Classes="connected-dot" IsVisible="{Binding IsConnected}" />
<TextBlock Grid.Column="2" Text="{Binding Name}" TextTrimming="CharacterEllipsis" />
</Grid>
<TextBlock Text="{Binding Cluster}" Classes="hint" Margin="18,2,0,0" TextTrimming="CharacterEllipsis" />
</StackPanel>
</Button>
<Grid>
<Button Classes="catalog-item"
Command="{Binding ActivateCommand}"
Classes.selected="{Binding IsActive}"
Classes.connected="{Binding IsConnected}">
<StackPanel>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="●" Margin="0,0,6,0" Classes="connected-dot" IsVisible="{Binding IsConnected}" />
<TextBlock Grid.Column="1" Text="{Binding Name}" TextTrimming="CharacterEllipsis" />
</Grid>
<TextBlock Text="{Binding Cluster}"
Classes="hint"
Margin="18,4,0,0"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Button>
<RadioButton Classes="catalog-current catalog-current-overlay"
GroupName="kubectl-current"
IsChecked="{Binding IsKubectlCurrent, Mode=OneWay}"
Command="{Binding UseForKubectlCommand}"
ToolTip.Tip="kubectl current-context" />
</Grid>
<Button Grid.Column="1"
Content="×"
Classes="catalog-close"

View File

@ -100,10 +100,48 @@
<Style Selector="Button.catalog-close:pointerover">
<Setter Property="Background" Value="#3a2a2a" />
</Style>
<Style Selector="TextBlock.kubectl-current">
<Setter Property="Foreground" Value="#7ee787" />
<Setter Property="FontWeight" Value="SemiBold" />
<Style Selector="RadioButton.catalog-current">
<Setter Property="MinHeight" Value="12" />
<Setter Property="MinWidth" Value="12" />
<Setter Property="Width" Value="12" />
<Setter Property="Height" Value="12" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0,0,6,0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<ControlTemplate>
<Panel Width="12" Height="12">
<Ellipse Name="border"
Width="12"
Height="12"
Stroke="#9aa0a6"
StrokeThickness="1"
Fill="Transparent" />
<Ellipse Name="checkMark"
Width="6"
Height="6"
Fill="#7ee787"
IsVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="RadioButton.catalog-current-overlay">
<Setter Property="Margin" Value="8,0,0,8" />
<Setter Property="VerticalAlignment" Value="Bottom" />
</Style>
<Style Selector="RadioButton.catalog-current:pointerover /template/ Ellipse#border">
<Setter Property="Stroke" Value="#e6e6e6" />
</Style>
<Style Selector="RadioButton.catalog-current:checked /template/ Ellipse#checkMark">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="RadioButton.catalog-current:checked /template/ Ellipse#border">
<Setter Property="Stroke" Value="#7ee787" />
</Style>
<Style Selector="ListBoxItem:selected">
<Setter Property="Background" Value="#3d5afe" />

View File

@ -9,11 +9,18 @@ namespace MaksIT.ClusterConsole.UI.ViewModels;
public partial class ConnectionItemViewModel : ObservableObject {
public required KubeContextDetails Details { get; init; }
public required Action<ConnectionItemViewModel> Use { get; init; }
public string Name => Details.Name;
public string Title => (Details.IsCurrent ? "* " : " ") + Details.Name;
public string Title => Details.Name;
public string Subtitle => Details.Cluster + " · " + Details.Server;
public bool IsCurrent => Details.IsCurrent;
[RelayCommand]
private void UseForKubectl() => Use(this);
}
public partial class ConnectionsViewModel : ObservableObject {
@ -30,7 +37,7 @@ public partial class ConnectionsViewModel : ObservableObject {
public string KubeConfigPath { get; }
[ObservableProperty]
private ConnectionItemViewModel? selected;
public partial ConnectionItemViewModel? Selected { get; set; }
[ObservableProperty]
private string status = "";
@ -44,9 +51,8 @@ public partial class ConnectionsViewModel : ObservableObject {
return "Select a context.";
var d = Selected.Details;
var mark = d.IsCurrent ? "*" : " ";
return
$"{mark} {d.Name}\n"
$"{d.Name}\n"
+ $" Namespace: {d.Namespace ?? "(none)"}\n"
+ $" Cluster: {d.Cluster}\n"
+ $" Server: {d.Server}\n"
@ -78,25 +84,29 @@ public partial class ConnectionsViewModel : ObservableObject {
var selectedName = Selected?.Name;
Items.Clear();
foreach (var details in listed.Value ?? [])
Items.Add(new ConnectionItemViewModel { Details = details });
Items.Add(new ConnectionItemViewModel { Details = details, Use = UseForKubectl });
Selected = Items.FirstOrDefault(i => i.Name == selectedName)
?? Items.FirstOrDefault(i => i.Details.IsCurrent)
?? Items.FirstOrDefault();
Status = Items.Count == 0
? "No contexts in kubeconfig. Add a connection."
: Items.Count + " context(s). * is kubectl current-context.";
: Items.Count + " context(s).";
}
[RelayCommand]
private void UseForKubectl() {
if (Selected is null)
private void UseForKubectl(ConnectionItemViewModel item) {
if (item.IsCurrent)
return;
var used = _kubeConfig.UseContext(Selected.Name);
var used = _kubeConfig.UseContext(item.Name);
if (!used.IsSuccess) {
Status = string.Join("; ", used.Messages);
return;
}
Selected = item;
Reload();
Status = string.Join("; ", used.Messages);
if (used.IsSuccess)
Reload();
}
[RelayCommand]
@ -127,7 +137,9 @@ public partial class ConnectionsViewModel : ObservableObject {
return false;
Reload();
Selected = Items.FirstOrDefault(i => i.Name == request.ContextName) ?? Selected;
if (Items.FirstOrDefault(i => i.Name == request.ContextName) is { } addedItem)
Selected = addedItem;
return true;
}

View File

@ -176,6 +176,8 @@ public partial class CatalogItemViewModel : ObservableObject {
public required Action<CatalogItemViewModel> Close { get; init; }
public required Action<CatalogItemViewModel> UseKubectl { get; init; }
public string Name => Context.Name;
public string Cluster => Context.Cluster;
@ -194,6 +196,9 @@ public partial class CatalogItemViewModel : ObservableObject {
[RelayCommand]
private void Disconnect() => Close(this);
[RelayCommand]
private void UseForKubectl() => UseKubectl(this);
}
public partial class MainViewModel : ObservableObject, IDisposable {
@ -264,7 +269,8 @@ public partial class MainViewModel : ObservableObject, IDisposable {
Catalog.Add(new CatalogItemViewModel {
Context = ctx,
Select = item => _ = OpenOrSwitchAsync(item),
Close = item => DisconnectNamed(item.Name)
Close = item => DisconnectNamed(item.Name),
UseKubectl = UseKubectlCurrent
});
}
@ -430,6 +436,15 @@ public partial class MainViewModel : ObservableObject, IDisposable {
Status = $"Connected to {page.Name}";
}
private void UseKubectlCurrent(CatalogItemViewModel item) {
if (item.IsKubectlCurrent)
return;
var used = _kubeConfig.UseContext(item.Name);
Status = string.Join("; ", used.Messages);
SyncCatalogFlags();
}
private void SyncCatalogFlags() {
var current = _kubeConfig.GetCurrentContext();
var kubectlCurrent = current.IsSuccess ? current.Value : null;

View File

@ -21,7 +21,6 @@
<Grid ColumnDefinitions="*,Auto">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Content="Add…" Command="{Binding AddCommand}" />
<Button Content="Use for kubectl" Command="{Binding UseForKubectlCommand}" IsEnabled="{Binding HasSelection}" />
<Button Content="Connect" Command="{Binding ConnectCommand}" IsEnabled="{Binding HasSelection}" />
<Button Content="Delete" Command="{Binding DeleteCommand}" IsEnabled="{Binding HasSelection}" />
</StackPanel>
@ -41,7 +40,18 @@
<DataTemplate x:DataType="vm:ConnectionItemViewModel">
<StackPanel Margin="0,2">
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" />
<TextBlock Text="{Binding Subtitle}" Classes="hint" TextTrimming="CharacterEllipsis" />
<Grid ColumnDefinitions="Auto,*" Margin="0,4,0,0">
<RadioButton Classes="catalog-current"
GroupName="connection-kubectl-current"
IsChecked="{Binding IsCurrent, Mode=OneWay}"
Command="{Binding UseForKubectlCommand}"
ToolTip.Tip="kubectl current-context" />
<TextBlock Grid.Column="1"
Text="{Binding Subtitle}"
Classes="hint"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>