mirror of
https://github.com/MAKS-IT-COM/maksit-cluster-console.git
synced 2026-11-14 20:56:12 +01:00
676 lines
19 KiB
C#
676 lines
19 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using MaksIT.ClusterConsole.Shared;
|
|
using MaksIT.ClusterConsole.Client.Ollama;
|
|
using MaksIT.ClusterConsole.UI.ViewModels;
|
|
using MaksIT.ClusterConsole.Client.Cluster;
|
|
using MaksIT.ClusterConsole.Client.KubeConfig;
|
|
using MaksIT.ClusterConsole.UI.ViewModels.Cluster;
|
|
using MaksIT.ClusterConsole.UI.ViewModels.Storage;
|
|
using MaksIT.ClusterConsole.UI.ViewModels.Connections;
|
|
|
|
|
|
namespace MaksIT.ClusterConsole.UI.ViewModels.Shell;
|
|
|
|
public partial class NavItemViewModel : ObservableObject {
|
|
public required NavigatorItem Item { get; init; }
|
|
|
|
public required Action<NavigatorItem> Select { get; init; }
|
|
|
|
public string Title => Item.Title;
|
|
|
|
[ObservableProperty]
|
|
private bool isSelected;
|
|
|
|
[RelayCommand]
|
|
private void Activate() => Select(Item);
|
|
}
|
|
|
|
public partial class NodeUsageViewModel : ObservableObject {
|
|
public required string Name { get; init; }
|
|
|
|
[ObservableProperty]
|
|
private ResourceSlice cpu = ResourceSlice.Empty("cpu");
|
|
|
|
[ObservableProperty]
|
|
private ResourceSlice memory = ResourceSlice.Empty("memory");
|
|
|
|
[ObservableProperty]
|
|
private ResourceSlice pods = ResourceSlice.Empty("pods");
|
|
|
|
[ObservableProperty]
|
|
private double cpuPercent;
|
|
|
|
[ObservableProperty]
|
|
private double memoryPercent;
|
|
|
|
[ObservableProperty]
|
|
private double podPercent;
|
|
|
|
[ObservableProperty]
|
|
private IReadOnlyList<double> cpuHistory = [];
|
|
|
|
[ObservableProperty]
|
|
private IReadOnlyList<double> memoryHistory = [];
|
|
|
|
private readonly List<double> _cpuHistory = [];
|
|
private readonly List<double> _memoryHistory = [];
|
|
|
|
public void Update(NodeUsage usage, int historyPoints, bool sampleHistory) {
|
|
Cpu = usage.Cpu;
|
|
Memory = usage.Memory;
|
|
Pods = usage.Pods;
|
|
SetPercents(usage.Cpu, usage.Memory, usage.Pods);
|
|
|
|
if (!sampleHistory)
|
|
return;
|
|
|
|
AppendHistory(historyPoints);
|
|
}
|
|
|
|
public void UpdateUsed(NodeUsage usage, int historyPoints, bool sampleHistory) {
|
|
Cpu = WithUsed(Cpu, usage.Cpu);
|
|
Memory = WithUsed(Memory, usage.Memory);
|
|
Pods = WithUsed(Pods, usage.Pods);
|
|
SetPercents(Cpu, Memory, Pods);
|
|
|
|
if (!sampleHistory)
|
|
return;
|
|
|
|
AppendHistory(historyPoints);
|
|
}
|
|
|
|
private void SetPercents(ResourceSlice cpu, ResourceSlice memory, ResourceSlice pods) {
|
|
CpuPercent = cpu.Allocatable <= 0 ? 0 : Math.Clamp(cpu.Used / cpu.Allocatable * 100, 0, 100);
|
|
MemoryPercent = memory.Allocatable <= 0 ? 0 : Math.Clamp(memory.Used / memory.Allocatable * 100, 0, 100);
|
|
PodPercent = pods.Allocatable <= 0 ? 0 : Math.Clamp(pods.Used / pods.Allocatable * 100, 0, 100);
|
|
}
|
|
|
|
private void AppendHistory(int historyPoints) {
|
|
Append(_cpuHistory, CpuPercent, historyPoints);
|
|
Append(_memoryHistory, MemoryPercent, historyPoints);
|
|
CpuHistory = _cpuHistory.ToArray();
|
|
MemoryHistory = _memoryHistory.ToArray();
|
|
}
|
|
|
|
private static ResourceSlice WithUsed(ResourceSlice current, ResourceSlice incoming) =>
|
|
new(incoming.Used, current.Requests, current.Limits, incoming.Allocatable, incoming.Capacity, incoming.Kind);
|
|
|
|
private static void Append(List<double> history, double value, int historyPoints) {
|
|
history.Add(value);
|
|
|
|
if (history.Count > historyPoints)
|
|
history.RemoveAt(0);
|
|
}
|
|
}
|
|
|
|
public partial class WorkloadKindCount : ObservableObject {
|
|
public required string Id { get; init; }
|
|
|
|
public required string Title { get; init; }
|
|
|
|
public required int Count { get; init; }
|
|
|
|
public required Action<string> Open { get; init; }
|
|
|
|
[RelayCommand]
|
|
private void Activate() => Open(Id);
|
|
}
|
|
|
|
public partial class NavGroupViewModel : ObservableObject {
|
|
public required string Section { get; init; }
|
|
|
|
public required string Path { get; init; }
|
|
|
|
public string IconData { get; init; } = "";
|
|
|
|
public required IReadOnlyList<NavItemViewModel> Items { get; init; }
|
|
|
|
public IReadOnlyList<NavGroupViewModel> Groups { get; init; } = [];
|
|
|
|
public Action<NavGroupViewModel>? ExpandedChanged { get; init; }
|
|
|
|
public bool HasIcon => !string.IsNullOrWhiteSpace(IconData);
|
|
|
|
[ObservableProperty]
|
|
private bool isExpanded;
|
|
|
|
public string Chevron => IsExpanded ? "▾" : "▸";
|
|
|
|
[RelayCommand]
|
|
private void Toggle() {
|
|
IsExpanded = !IsExpanded;
|
|
ExpandedChanged?.Invoke(this);
|
|
}
|
|
|
|
partial void OnIsExpandedChanged(bool value) =>
|
|
OnPropertyChanged(nameof(Chevron));
|
|
|
|
public bool Contains(string? itemId) =>
|
|
AllItems().Any(i => i.Item.Id == itemId);
|
|
|
|
public IEnumerable<NavItemViewModel> AllItems() {
|
|
foreach (var item in Items)
|
|
yield return item;
|
|
|
|
foreach (var group in Groups) {
|
|
foreach (var item in group.AllItems())
|
|
yield return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class PortForwardItemViewModel {
|
|
public required PortForwardHandle Handle { get; set; }
|
|
|
|
public string Cluster { get; init; } = "";
|
|
|
|
public required string Uid { get; set; }
|
|
|
|
public string Kind { get; init; } = "Pod";
|
|
|
|
public string ResourceName { get; init; } = "";
|
|
|
|
public Dictionary<string, string>? MatchLabels { get; init; }
|
|
|
|
public PersistedPortForward ToPersisted() =>
|
|
new() {
|
|
Context = Cluster,
|
|
Kind = Kind,
|
|
Name = string.IsNullOrEmpty(ResourceName) ? Handle.PodName : ResourceName,
|
|
Namespace = Handle.Namespace,
|
|
PodName = Handle.PodName,
|
|
LocalPort = Handle.LocalPort,
|
|
RemotePort = Handle.RequestedPort,
|
|
MatchLabels = MatchLabels is null
|
|
? null
|
|
: new Dictionary<string, string>(MatchLabels, StringComparer.Ordinal)
|
|
};
|
|
}
|
|
|
|
public partial class DataEntryViewModel : ObservableObject {
|
|
[ObservableProperty]
|
|
private string key = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(ValuePreview))]
|
|
private string value = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private bool isBinary;
|
|
|
|
public string ValuePreview {
|
|
get {
|
|
if (string.IsNullOrEmpty(Value))
|
|
return "";
|
|
|
|
var end = Value.IndexOfAny(['\r', '\n']);
|
|
|
|
if (end < 0)
|
|
return Value;
|
|
|
|
return Value[..end] + "…";
|
|
}
|
|
}
|
|
}
|
|
|
|
public partial class CatalogItemViewModel : ObservableObject {
|
|
public required KubeContextInfo Context { get; init; }
|
|
|
|
public required Action<CatalogItemViewModel> Select { get; init; }
|
|
|
|
public required Action<CatalogItemViewModel> Close { get; init; }
|
|
|
|
public required Action<CatalogItemViewModel> UseKubectl { get; init; }
|
|
|
|
public string Name => Context.Name;
|
|
|
|
[ObservableProperty]
|
|
private bool isConnected;
|
|
|
|
[ObservableProperty]
|
|
private bool isActive;
|
|
|
|
[ObservableProperty]
|
|
private bool isKubectlCurrent;
|
|
|
|
[RelayCommand]
|
|
private void Activate() => Select(this);
|
|
|
|
[RelayCommand]
|
|
private void Disconnect() => Close(this);
|
|
|
|
[RelayCommand]
|
|
private void UseForKubectl() => UseKubectl(this);
|
|
}
|
|
|
|
public partial class MainViewModel : ObservableObject, IDisposable {
|
|
private readonly IKubeConfigService _kubeConfig;
|
|
private readonly IClusterSessionFactory _sessions;
|
|
private readonly ConfigurationFileService _configuration;
|
|
private readonly IOllamaChatClient _ollama;
|
|
private readonly Dictionary<string, ClusterPageViewModel> _pages = new(StringComparer.Ordinal);
|
|
private readonly Task _restoreTask;
|
|
|
|
public MainViewModel(
|
|
IKubeConfigService kubeConfig,
|
|
IClusterSessionFactory sessions,
|
|
ConfigurationFileService configuration,
|
|
IOllamaChatClient ollama,
|
|
ScreenshotTourOptions? tour = null) {
|
|
_kubeConfig = kubeConfig;
|
|
_sessions = sessions;
|
|
_configuration = configuration;
|
|
_ollama = ollama;
|
|
catalogCollapsed = _configuration.Current.Layout.CatalogCollapsed;
|
|
|
|
if (tour is not null) {
|
|
_configuration.Current.Layout.ClearFiltersAndSort();
|
|
_configuration.Current.UseAllNamespaces();
|
|
}
|
|
|
|
LoadCatalog();
|
|
|
|
if (tour is null)
|
|
_restoreTask = RestoreSessionsCommand.ExecuteAsync(null);
|
|
else
|
|
_restoreTask = Task.CompletedTask;
|
|
}
|
|
|
|
public async Task<string?> ConnectForCaptureAsync(string? name) {
|
|
await _restoreTask;
|
|
var target = string.IsNullOrWhiteSpace(name) ? _configuration.Current.ActiveContext : name;
|
|
|
|
if (string.IsNullOrWhiteSpace(target))
|
|
target = Catalog.FirstOrDefault()?.Name;
|
|
|
|
if (string.IsNullOrWhiteSpace(target))
|
|
return null;
|
|
|
|
await ConnectNamedAsync(target);
|
|
|
|
return string.Equals(ActivePage?.Name, target, StringComparison.Ordinal) ? target : null;
|
|
}
|
|
|
|
public ObservableCollection<CatalogItemViewModel> Catalog { get; } = [];
|
|
|
|
[ObservableProperty]
|
|
private ClusterPageViewModel? activePage;
|
|
|
|
[ObservableProperty]
|
|
private string status = "Select a cluster from the catalog.";
|
|
|
|
[ObservableProperty]
|
|
private bool catalogCollapsed;
|
|
|
|
public bool IsClusterOpen => ActivePage is not null;
|
|
|
|
public bool ShowChat => _configuration.Current.Ai.Enabled;
|
|
|
|
public string ClusterTitle => ActivePage?.Name ?? "Catalog";
|
|
|
|
public NavigatorItem? SelectedNavItem => ActivePage?.SelectedNavItem;
|
|
|
|
public ResourceDescriptor? SelectedDescriptor => ActivePage?.SelectedDescriptor;
|
|
|
|
partial void OnCatalogCollapsedChanged(bool value) {
|
|
var cfg = _configuration.Current;
|
|
|
|
if (cfg.Layout.CatalogCollapsed == value)
|
|
return;
|
|
|
|
cfg.Layout.CatalogCollapsed = value;
|
|
_configuration.Save(cfg);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void ToggleCatalog() =>
|
|
CatalogCollapsed = !CatalogCollapsed;
|
|
|
|
partial void OnActivePageChanged(ClusterPageViewModel? oldValue, ClusterPageViewModel? newValue) {
|
|
if (oldValue is not null)
|
|
oldValue.PropertyChanged -= OnActivePagePropertyChanged;
|
|
|
|
if (newValue is not null)
|
|
newValue.PropertyChanged += OnActivePagePropertyChanged;
|
|
|
|
OnPropertyChanged(nameof(IsClusterOpen));
|
|
OnPropertyChanged(nameof(ClusterTitle));
|
|
OnPropertyChanged(nameof(SelectedNavItem));
|
|
OnPropertyChanged(nameof(SelectedDescriptor));
|
|
}
|
|
|
|
private void OnActivePagePropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
|
if (e.PropertyName is nameof(ClusterPageViewModel.SelectedNavItem)) {
|
|
OnPropertyChanged(nameof(SelectedNavItem));
|
|
OnPropertyChanged(nameof(SelectedDescriptor));
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void LoadCatalog() {
|
|
var listed = _kubeConfig.ListContexts();
|
|
|
|
if (!listed.IsSuccess) {
|
|
Status = string.Join("; ", listed.Messages);
|
|
|
|
return;
|
|
}
|
|
|
|
Catalog.Clear();
|
|
|
|
foreach (var ctx in listed.Value ?? []) {
|
|
Catalog.Add(new CatalogItemViewModel {
|
|
Context = ctx,
|
|
Select = item => _ = OpenOrSwitchAsync(item),
|
|
Close = item => DisconnectNamed(item.Name),
|
|
UseKubectl = UseKubectlCurrent
|
|
});
|
|
}
|
|
|
|
DropMissingPages();
|
|
SyncCatalogFlags();
|
|
Status = Catalog.Count == 0
|
|
? "No kubeconfig contexts found. Open Connections to add one."
|
|
: $"{Catalog.Count} context(s) in kubeconfig · {_pages.Count} connected.";
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenConnections() =>
|
|
ConnectionsRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
public event EventHandler? ConnectionsRequested;
|
|
|
|
[RelayCommand]
|
|
private void OpenAiSettings() =>
|
|
AiSettingsRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
public event EventHandler? AiSettingsRequested;
|
|
|
|
[RelayCommand]
|
|
private void OpenScreenSaverSettings() =>
|
|
ScreenSaverSettingsRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
public event EventHandler? ScreenSaverSettingsRequested;
|
|
|
|
public event Action? ScreenSaverSettingsChanged;
|
|
|
|
[ObservableProperty]
|
|
private bool isFullScreen;
|
|
|
|
[RelayCommand]
|
|
private void ToggleFullScreen() =>
|
|
FullScreenToggleRequested?.Invoke(this, EventArgs.Empty);
|
|
|
|
public event EventHandler? FullScreenToggleRequested;
|
|
|
|
public event Action<VolumeFilesViewModel>? VolumeFilesRequested;
|
|
|
|
public Func<RetainReclaimViewModel, Task>? ShowRetainReclaim { get; set; }
|
|
|
|
public Func<DrainConfirmViewModel, Task<bool>>? ShowDrainPreview { get; set; }
|
|
|
|
public Func<LonghornAttachViewModel, Task<bool>>? ConfirmLonghornAttach { get; set; }
|
|
|
|
public Func<LonghornDetachViewModel, Task<bool>>? ConfirmLonghornDetach { get; set; }
|
|
|
|
public Func<LonghornBackupJobViewModel, Task<bool>>? ConfirmLonghornBackupJob { get; set; }
|
|
|
|
public ConnectionsViewModel CreateConnectionsViewModel() =>
|
|
new(_kubeConfig);
|
|
|
|
public AiSettingsViewModel CreateAiSettingsViewModel() {
|
|
var cfg = _configuration.Current;
|
|
|
|
return new AiSettingsViewModel(
|
|
cfg.Ai.Enabled,
|
|
cfg.Ai.AgentEnabled,
|
|
cfg.Ai.Endpoint,
|
|
cfg.Ai.Model,
|
|
cfg.Ai.TimeoutSeconds,
|
|
ApplyAiSettings);
|
|
}
|
|
|
|
private void ApplyAiSettings(bool enabled, bool agent, string endpoint, string model, int timeoutSeconds) {
|
|
var cfg = _configuration.Current;
|
|
cfg.Ai.Enabled = enabled;
|
|
cfg.Ai.AgentEnabled = enabled && agent;
|
|
cfg.Ai.Endpoint = endpoint;
|
|
cfg.Ai.Model = model;
|
|
cfg.Ai.TimeoutSeconds = timeoutSeconds;
|
|
cfg.EnsureDefaults();
|
|
_configuration.Save(cfg);
|
|
OnPropertyChanged(nameof(ShowChat));
|
|
|
|
foreach (var page in _pages.Values)
|
|
page.Chat.NotifySettingsChanged();
|
|
}
|
|
|
|
public ScreenSaverSettingsViewModel CreateScreenSaverSettingsViewModel() {
|
|
var cfg = _configuration.Current;
|
|
|
|
return new ScreenSaverSettingsViewModel(
|
|
cfg.ScreenSaver.Enabled,
|
|
cfg.ScreenSaver.IdleMinutes,
|
|
ApplyScreenSaverSettings);
|
|
}
|
|
|
|
private void ApplyScreenSaverSettings(bool enabled, int idleMinutes) {
|
|
var cfg = _configuration.Current;
|
|
cfg.ScreenSaver.Enabled = enabled;
|
|
cfg.ScreenSaver.IdleMinutes = idleMinutes;
|
|
cfg.EnsureDefaults();
|
|
_configuration.Save(cfg);
|
|
ScreenSaverSettingsChanged?.Invoke();
|
|
}
|
|
|
|
public Task ConnectNamedAsync(string name) {
|
|
var item = Catalog.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.Ordinal));
|
|
|
|
return OpenOrSwitchAsync(item);
|
|
}
|
|
|
|
private async Task OpenOrSwitchAsync(CatalogItemViewModel? item) {
|
|
if (item is null)
|
|
return;
|
|
|
|
if (_pages.TryGetValue(item.Name, out var existing)) {
|
|
ActivatePage(existing);
|
|
|
|
return;
|
|
}
|
|
|
|
Status = $"Connecting to {item.Name}…";
|
|
var created = _sessions.Create(item.Name);
|
|
|
|
if (!created.IsSuccess || created.Value is null) {
|
|
Status = string.Join("; ", created.Messages);
|
|
|
|
return;
|
|
}
|
|
|
|
var page = CreatePage(item.Context);
|
|
var started = await page.StartAsync(created.Value);
|
|
|
|
if (!started.IsSuccess) {
|
|
page.Dispose();
|
|
|
|
Status = string.Join("; ", started.Messages);
|
|
|
|
return;
|
|
}
|
|
|
|
_pages[item.Name] = page;
|
|
ActivatePage(page);
|
|
var restored = await page.RestorePortForwardsAsync();
|
|
|
|
if (restored.Total > 0)
|
|
Status = restored.Format();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Disconnect() {
|
|
if (ActivePage is null)
|
|
return;
|
|
|
|
DisconnectNamed(ActivePage.Name);
|
|
}
|
|
|
|
private void DisconnectNamed(string name) {
|
|
if (!_pages.TryGetValue(name, out var page))
|
|
return;
|
|
|
|
var wasActive = ActivePage == page;
|
|
|
|
if (wasActive)
|
|
page.PausePolling();
|
|
|
|
page.Dispose();
|
|
_pages.Remove(name);
|
|
|
|
if (wasActive) {
|
|
ActivePage = null;
|
|
var next = _pages.Values.FirstOrDefault();
|
|
|
|
if (next is not null)
|
|
ActivatePage(next);
|
|
else
|
|
Status = "Disconnected.";
|
|
}
|
|
|
|
SyncCatalogFlags();
|
|
PersistOpenState();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RestoreSessionsAsync() {
|
|
var names = (_configuration.Current.OpenContexts ?? [])
|
|
.Where(n => !string.IsNullOrWhiteSpace(n))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
if (names.Count == 0)
|
|
return;
|
|
|
|
var preferred = _configuration.Current.ActiveContext;
|
|
PortForwardRestoreSummary? restored = null;
|
|
|
|
foreach (var name in names) {
|
|
var item = Catalog.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.Ordinal));
|
|
|
|
if (item is null)
|
|
continue;
|
|
|
|
var created = _sessions.Create(item.Name);
|
|
|
|
if (!created.IsSuccess || created.Value is null)
|
|
continue;
|
|
|
|
var page = CreatePage(item.Context);
|
|
var started = await page.StartAsync(created.Value);
|
|
|
|
if (!started.IsSuccess) {
|
|
page.Dispose();
|
|
|
|
continue;
|
|
}
|
|
|
|
page.PausePolling();
|
|
_pages[item.Name] = page;
|
|
restored = MergeRestore(restored, await page.RestorePortForwardsAsync());
|
|
}
|
|
|
|
var active = _pages.TryGetValue(preferred ?? "", out var preferredPage)
|
|
? preferredPage
|
|
: _pages.Values.FirstOrDefault();
|
|
|
|
if (active is not null)
|
|
ActivatePage(active);
|
|
else
|
|
SyncCatalogFlags();
|
|
|
|
if (restored is not null && restored.Total > 0)
|
|
Status = restored.Format();
|
|
}
|
|
|
|
private static PortForwardRestoreSummary MergeRestore(
|
|
PortForwardRestoreSummary? current,
|
|
PortForwardRestoreSummary incoming) {
|
|
if (current is null)
|
|
return incoming;
|
|
|
|
return new PortForwardRestoreSummary(
|
|
current.Restored + incoming.Restored,
|
|
current.Failures.Concat(incoming.Failures).ToList());
|
|
}
|
|
|
|
private ClusterPageViewModel CreatePage(KubeContextInfo context) {
|
|
var page = new ClusterPageViewModel(context, new ClusterWorkspace(), _configuration, _ollama, text => Status = text);
|
|
page.VolumeFilesRequested += vm => VolumeFilesRequested?.Invoke(vm);
|
|
page.ShowRetainReclaim = viewModel => ShowRetainReclaim?.Invoke(viewModel) ?? Task.CompletedTask;
|
|
page.ConfirmDrain = dialog => ShowDrainPreview?.Invoke(dialog) ?? Task.FromResult(false);
|
|
page.ConfirmLonghornAttach = dialog => ConfirmLonghornAttach?.Invoke(dialog) ?? Task.FromResult(false);
|
|
page.ConfirmLonghornDetach = dialog => ConfirmLonghornDetach?.Invoke(dialog) ?? Task.FromResult(false);
|
|
page.ConfirmLonghornBackupJob = dialog => ConfirmLonghornBackupJob?.Invoke(dialog) ?? Task.FromResult(false);
|
|
|
|
return page;
|
|
}
|
|
|
|
private void ActivatePage(ClusterPageViewModel page) {
|
|
if (ActivePage == page) {
|
|
SyncCatalogFlags();
|
|
|
|
return;
|
|
}
|
|
|
|
ActivePage?.PausePolling();
|
|
ActivePage = page;
|
|
page.ResumePolling();
|
|
_ = page.RefreshRowsCommand.ExecuteAsync(null);
|
|
SyncCatalogFlags();
|
|
PersistOpenState();
|
|
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;
|
|
|
|
foreach (var item in Catalog) {
|
|
item.IsConnected = _pages.ContainsKey(item.Name);
|
|
item.IsActive = ActivePage?.Name == item.Name;
|
|
item.IsKubectlCurrent = string.Equals(item.Name, kubectlCurrent, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
private void DropMissingPages() {
|
|
foreach (var name in _pages.Keys.Except(Catalog.Select(c => c.Name), StringComparer.Ordinal).ToList())
|
|
DisconnectNamed(name);
|
|
}
|
|
|
|
private void PersistOpenState() {
|
|
var cfg = _configuration.Current;
|
|
cfg.OpenContexts = [.. _pages.Keys];
|
|
cfg.ActiveContext = ActivePage?.Name;
|
|
_configuration.Save(cfg);
|
|
}
|
|
|
|
public void Dispose() {
|
|
foreach (var page in _pages.Values)
|
|
page.Dispose();
|
|
|
|
_pages.Clear();
|
|
}
|
|
}
|