(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] ## [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 ## [0.2.1] - 2026-08-20
### Fixed ### 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. - **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. - **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. - **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 ## 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 - **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 - **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 - **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> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Version>0.2.1</Version> <Version>0.3.0</Version>
<Product>MaksIT.ClusterConsole</Product> <Product>MaksIT.ClusterConsole</Product>
<AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle> <AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle>
</PropertyGroup> </PropertyGroup>

View File

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

View File

@ -100,10 +100,48 @@
<Style Selector="Button.catalog-close:pointerover"> <Style Selector="Button.catalog-close:pointerover">
<Setter Property="Background" Value="#3a2a2a" /> <Setter Property="Background" Value="#3a2a2a" />
</Style> </Style>
<Style Selector="TextBlock.kubectl-current"> <Style Selector="RadioButton.catalog-current">
<Setter Property="Foreground" Value="#7ee787" /> <Setter Property="MinHeight" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" /> <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="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>
<Style Selector="ListBoxItem:selected"> <Style Selector="ListBoxItem:selected">
<Setter Property="Background" Value="#3d5afe" /> <Setter Property="Background" Value="#3d5afe" />

View File

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

View File

@ -176,6 +176,8 @@ public partial class CatalogItemViewModel : ObservableObject {
public required Action<CatalogItemViewModel> Close { get; init; } public required Action<CatalogItemViewModel> Close { get; init; }
public required Action<CatalogItemViewModel> UseKubectl { get; init; }
public string Name => Context.Name; public string Name => Context.Name;
public string Cluster => Context.Cluster; public string Cluster => Context.Cluster;
@ -194,6 +196,9 @@ public partial class CatalogItemViewModel : ObservableObject {
[RelayCommand] [RelayCommand]
private void Disconnect() => Close(this); private void Disconnect() => Close(this);
[RelayCommand]
private void UseForKubectl() => UseKubectl(this);
} }
public partial class MainViewModel : ObservableObject, IDisposable { public partial class MainViewModel : ObservableObject, IDisposable {
@ -264,7 +269,8 @@ public partial class MainViewModel : ObservableObject, IDisposable {
Catalog.Add(new CatalogItemViewModel { Catalog.Add(new CatalogItemViewModel {
Context = ctx, Context = ctx,
Select = item => _ = OpenOrSwitchAsync(item), 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}"; 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() { private void SyncCatalogFlags() {
var current = _kubeConfig.GetCurrentContext(); var current = _kubeConfig.GetCurrentContext();
var kubectlCurrent = current.IsSuccess ? current.Value : null; var kubectlCurrent = current.IsSuccess ? current.Value : null;

View File

@ -21,7 +21,6 @@
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<Button Content="Add…" Command="{Binding AddCommand}" /> <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="Connect" Command="{Binding ConnectCommand}" IsEnabled="{Binding HasSelection}" />
<Button Content="Delete" Command="{Binding DeleteCommand}" IsEnabled="{Binding HasSelection}" /> <Button Content="Delete" Command="{Binding DeleteCommand}" IsEnabled="{Binding HasSelection}" />
</StackPanel> </StackPanel>
@ -41,7 +40,18 @@
<DataTemplate x:DataType="vm:ConnectionItemViewModel"> <DataTemplate x:DataType="vm:ConnectionItemViewModel">
<StackPanel Margin="0,2"> <StackPanel Margin="0,2">
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" /> <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> </StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>