(feature): show overview chat and let the details pane collapse

This commit is contained in:
Maksym Sadovnychyy 2026-10-05 21:02:46 +02:00
parent 665fa99125
commit 9e556c6048
26 changed files with 785 additions and 472 deletions

View File

@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]
## [0.8.7] - 2026-10-05
### Added
- When Settings → AI is on, Chat sits on the cluster overview beside Errors and Warnings. Analyze issues asks which objects are still failing and which events are leftover after the pods recovered. Those warnings and errors go with the question.
### Changed
- The details pane, and the overview chat, can collapse to a narrow rail. The rail reads Chat on the overview and the current kind elsewhere. Opening it restores the previous width. The choice is saved with the layout.
- The Linux Flatpak sandbox no longer requests pulseaudio. The app has no audio. `--share=network` still uses this computer's network, including localhost, so a local cluster API, local Ollama, and a port-forward opened in the browser stay reachable. A kubeconfig login that starts `aws`, `gcloud`, or `kubelogin` cannot start that program from the sandbox. A certificate or token in the kubeconfig is enough. The README and AppStream say so.
- AppStream screenshots stay on tag `v0.8.6`, where those images are stored. The `0.8.7` release entry points at that version's changelog.
- Flathub verification uses the token from the developer portal. The well-known file does not list the application id.
## [0.8.6] - 2026-10-03
### Added

View File

@ -1,8 +1,8 @@
# MaksIT.ClusterConsole
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-58.7%25-yellowgreen)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-48.1%25-yellowgreen)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-67.5%25-green)
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-59.2%25-yellowgreen)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-48.7%25-yellowgreen)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-68.1%25-green)
![.NET](https://img.shields.io/badge/.NET-10-512BD4)
![License](https://img.shields.io/badge/License-Apache%202.0-blue)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-0078D6)
@ -39,7 +39,7 @@ Click a catalog row to open that context. A green dot is a live session in this
![Cluster catalog](assets/screenshots/welcome.png)
Overview shows CPU, memory, and pod counts from metrics-server. **Resource limits** can patch container CPU/MEM against node capacity. **Cluster** and **Nodes** switch those charts. Errors and Warnings list node, pod, and event problems, a LoadBalancer Service that is Pending or Unreachable, and a PersistentVolumeClaim that is Pending.
Overview shows CPU, memory, and pod counts from metrics-server. **Resource limits** can patch container CPU/MEM against node capacity. **Cluster** and **Nodes** switch those charts. Errors and Warnings list node, pod, and event problems, a LoadBalancer Service that is Pending or Unreachable, and a PersistentVolumeClaim that is Pending. When Settings → AI is on, Chat sits beside those lists and can explain the current warnings and errors.
<!-- microsoft-store 1 -->
![Cluster overview](assets/screenshots/overview.png)
@ -65,7 +65,7 @@ Ready, Restarts, Status, Node, CPU, and Memory. Filters and sort persist per clu
### Chat
Settings → AI turns Chat on. The endpoint and model are any local [Ollama](https://ollama.com) model you already pulled. The default is `qwen3:8b`. Reading the cluster is enough with that model. Repairs work better with a larger one: `qwen3:14b`, `qwen3:32b`, or `qwen3-coder:30b`. Put that name in the model box after `ollama pull`.
Settings → AI turns Chat on, on a resource and on the cluster overview. The endpoint and model are any local [Ollama](https://ollama.com) model you already pulled. The default is `qwen3:8b`. Reading the cluster is enough with that model. Repairs work better with a larger one: `qwen3:14b`, `qwen3:32b`, or `qwen3-coder:30b`. Put that name in the model box after `ollama pull`.
![AI settings](assets/screenshots/ai-settings.png)
@ -244,7 +244,7 @@ The previous id `com.maks_it.ClusterConsole` is replaced by this lowercase id. U
If GNOME or KDE does not show a launcher icon, `flatpak run` may warn that `/var/lib/flatpak/exports/share` and `~/.local/share/flatpak/exports/share` are not on `XDG_DATA_DIRS`. Log out and back in once so the session picks up those paths.
Linux uses X11/XWayland (Avalonia native Wayland still hangs on GNOME). The sandbox grants `--filesystem=home` for kubeconfig (`~/.kube`). AppStream and the desktop file live in [`data/`](data/).
Linux uses X11/XWayland (Avalonia native Wayland still hangs on GNOME). The sandbox grants `--filesystem=home` for a kubeconfig under the home directory and the certificate files it names. `--share=network` uses this computer's network, including localhost, so a local cluster API, local Ollama, and a port-forward opened in the browser stay reachable. A kubeconfig login that starts a program on this computer (`aws`, `gcloud`, `kubelogin`) cannot start that program from the sandbox. A certificate or token in the kubeconfig is enough. AppStream and the desktop file live in [`data/`](data/).
## Configuration

View File

@ -2,6 +2,11 @@
Short notes shown in the app after an upgrade. The full history, including maintainer notes, is [CHANGELOG.md](CHANGELOG.md).
## [0.8.7] - 2026-10-05
- When AI is on, Chat sits on the cluster overview. Analyze issues explains the current warnings and errors.
- The details pane, and the overview chat, can collapse to a rail and open again. The choice is kept.
## [0.8.6] - 2026-10-03
- The drain dialog can show a short AI note on whether it is safe to continue, which pods will move, and which stay.

View File

@ -10,6 +10,7 @@
<project_license>Apache-2.0</project_license>
<description>
<p>**After installation, point the app at a kubeconfig that can reach a running cluster (KUBECONFIG or ~/.kube/config).**</p>
<p>**A login that starts a program on this computer, such as aws, gcloud, or kubelogin, cannot start that program from the sandbox. A certificate or token in the kubeconfig is enough. Localhost on this computer stays reachable, including a local cluster API, local Ollama, and a port-forward opened in the browser.**</p>
<p>Browse a Kubernetes cluster from the desktop using your kubeconfig. Apply YAML, follow logs, exec into pods, and manage port-forwards without a browser.</p>
<p>Settings stay on this computer. Cluster access uses the network and files under the home folder (typically ~/.kube).</p>
<ul>
@ -35,35 +36,41 @@
</branding>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/main/assets/screenshots/overview.png</image>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/v0.8.6/assets/screenshots/overview.png</image>
<caption>Cluster overview</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/main/assets/screenshots/applications.png</image>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/v0.8.6/assets/screenshots/applications.png</image>
<caption>Applications table</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/main/assets/screenshots/pods.png</image>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/v0.8.6/assets/screenshots/pods.png</image>
<caption>Pods table</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/main/assets/screenshots/pods-chat.png</image>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/v0.8.6/assets/screenshots/pods-chat.png</image>
<caption>Chat on a selected pod</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/main/assets/screenshots/persistentvolumeclaims.png</image>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/v0.8.6/assets/screenshots/persistentvolumeclaims.png</image>
<caption>Persistent volume claims</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/main/assets/screenshots/dapr-control-plane.png</image>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/v0.8.6/assets/screenshots/dapr-control-plane.png</image>
<caption>Dapr control plane</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/main/assets/screenshots/port-forwarding.png</image>
<image>https://raw.githubusercontent.com/MAKS-IT-COM/maksit-cluster-console/v0.8.6/assets/screenshots/port-forwarding.png</image>
<caption>Port forwarding</caption>
</screenshot>
</screenshots>
<releases>
<release version="0.8.7" date="2026-10-05">
<url type="details">https://github.com/MAKS-IT-COM/maksit-cluster-console/blob/v0.8.7/CHANGELOG.md</url>
<description>
<p>Chat on the cluster overview when AI is on. The details pane can collapse to a rail.</p>
</description>
</release>
<release version="0.8.6" date="2026-10-03">
<url type="details">https://github.com/MAKS-IT-COM/maksit-cluster-console/blob/v0.8.6/CHANGELOG.md</url>
<description>

View File

@ -9,7 +9,7 @@ Values for `com.maks_it.clusterconsole.yml` (or `.json`) at the top of the Flath
| Homepage | [https://maks-it.com/](https://maks-it.com/) |
| Project | `src/MaksIT.ClusterConsole.UI/MaksIT.ClusterConsole.UI.csproj` |
| Target | `net10.0` |
| Source | Release tag on `https://github.com/MAKS-IT-COM/maksit-cluster-console` (currently `v0.8.6`) |
| Source | Release tag on `https://github.com/MAKS-IT-COM/maksit-cluster-console` (currently `v0.8.7`) |
| Runtime | `org.freedesktop.Platform` |
| SDK | `org.freedesktop.Sdk` |
| SDK extension | `org.freedesktop.Sdk.Extension.dotnet10` |

View File

@ -7,13 +7,12 @@ Finish args for the manifest. The GitHub bundle uses this set. Keep the Flathub
--share=network
--socket=wayland
--socket=x11
--socket=pulseaudio
--device=dri
--filesystem=home
```
Network reaches the cluster API. Home covers `~/.kube`, a `KUBECONFIG` path under the home directory, settings under `~/.config`, and volume download and upload. X11 and Wayland are both present because Linux runs through X11/XWayland; Avalonia's native Wayland path hangs on GNOME. DRI is for GL rendering. The app has no audio feature; include pulseaudio only when a local run needs it.
Network reaches the cluster API. Settings stay in the sandbox config directory. Home covers a kubeconfig anywhere under the home directory, including `~/.kube` and certificate files the kubeconfig names, plus files chosen in the open and save dialogs. X11 and Wayland stay together until Avalonia's native Wayland path no longer hangs on GNOME. Linux runs through X11/XWayland. DRI is for GL rendering. The app has no audio, so the sandbox does not take pulseaudio.
A kubeconfig and a reachable cluster are required after install. The metainfo says so. The submission is from the project that maintains this repository.
A kubeconfig and a reachable cluster are required after install. `--share=network` uses this computer's network, including localhost, so a local cluster API, local Ollama, and a port-forward opened in the browser stay reachable. A kubeconfig login that starts `aws`, `gcloud`, or `kubelogin` cannot start that program from the sandbox. A certificate or token in the kubeconfig is enough. The metainfo says so. The submission is from the project that maintains this repository.
The pull request steps are in [submission.md](submission.md). Manifest values are in [manifest.md](manifest.md). Verification is in [verification.md](verification.md).

View File

@ -9,7 +9,7 @@ Upstream files stay in this repository. The Flathub build installs them from the
| [src/MaksIT.ClusterConsole.UI/Assets/icon.svg](../../src/MaksIT.ClusterConsole.UI/Assets/icon.svg) | Icon, installed as `share/icons/hicolor/scalable/apps/com.maks_it.clusterconsole.svg`. |
| [LICENSE.md](../../LICENSE.md) | Apache-2.0. Install to `$FLATPAK_DEST/share/licenses/com.maks_it.clusterconsole/` because the filename is `LICENSE.md`. |
Screenshot URLs in the metainfo use tag `v0.8.3`. The `0.8.6` release entry points at tag `v0.8.6`. On a newer release, point `<image>` and `<release>` at that tag or at a commit.
Screenshot URLs in the metainfo use tag `v0.8.6`, where those images are stored. The `0.8.7` release entry points at tag `v0.8.7`. On a newer release, point `<image>` at the tag that contains the screenshots and `<release>` at that version.
## Pull request

View File

@ -1,9 +1,9 @@
# Flathub verification
After the app is published, place this line in `https://maks-it.com/.well-known/org.flathub.VerifiedApps.txt`.
After the app is published, open the Flathub developer portal and copy the verification token it shows for this app. Place that token on its own line in `https://maks-it.com/.well-known/org.flathub.VerifiedApps.txt`.
```text
com.maks_it.clusterconsole
```
The portal generates the token. The file does not list the application id. HTTPS is required. A line that starts with `#` is a comment.
Login verification does not apply to `com.maks_it`. The domain from the id is `maks-it.com`.
The pull request steps are in [submission.md](submission.md). Manifest values are in [manifest.md](manifest.md). Finish args are in [permissions.md](permissions.md).

View File

@ -3,7 +3,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.8.6</Version>
<Version>0.8.7</Version>
<Product>Cluster Console</Product>
<AssemblyTitle>Cluster Console</AssemblyTitle>
<Company>MaksIT</Company>

View File

@ -90,6 +90,23 @@ public static class ClusterIssues {
return $"{noun}: {active} ({resolved} resolved)";
}
public static string Format(ClusterIssueSet issues) {
var lines = new List<string>();
foreach (var error in issues.Errors)
lines.Add(Line("ERROR", error));
foreach (var warning in issues.Warnings)
lines.Add(Line("WARN", warning));
return lines.Count == 0
? "No overview warnings or errors."
: string.Join(Environment.NewLine, lines);
}
private static string Line(string severity, ClusterIssue issue) =>
$"{severity} {issue.State} {issue.Kind} {issue.ObjectName} ({issue.Age}): {issue.Message}";
private static List<ClusterIssue> Rank(IEnumerable<ClusterIssue> issues) =>
issues
.OrderBy(i => i.State == Resolved ? 1 : 0)

View File

@ -49,6 +49,8 @@ public sealed class LayoutSettings {
public double DetailsWidth { get; set; } = 380;
public bool ShoulderCollapsed { get; set; }
public double HelmHistoryHeight { get; set; } = 220;
public string? SelectedNavId { get; set; }

View File

@ -248,6 +248,16 @@ public class ClusterIssuesTests {
]));
}
[Fact]
public void Format_lists_severity_state_object_and_message() {
var at = DateTimeOffset.Parse("2026-10-04T21:00:00Z");
var text = ClusterIssues.Format(new ClusterIssueSet(
[Issue("e1", ClusterIssues.Active, at)],
[]));
Assert.Contains("WARN Active Event obj (1m): msg", text, StringComparison.Ordinal);
}
[Fact]
public void Age_formats_hours_and_minutes() {
var now = DateTimeOffset.Parse("2026-08-19T15:00:00Z");

View File

@ -145,6 +145,7 @@ public class ConfigurationFileServiceTests {
cfg.Layout.CatalogWidth = 260;
cfg.Layout.NavigatorWidth = 200;
cfg.Layout.DetailsWidth = 420;
cfg.Layout.ShoulderCollapsed = true;
cfg.Layout.HelmHistoryHeight = 280;
cfg.Layout.SetColumns("resources/pods", new Dictionary<string, double> {
["Name"] = 220,
@ -162,6 +163,7 @@ public class ConfigurationFileServiceTests {
Assert.Equal(260, reloaded.Current.Layout.CatalogWidth);
Assert.Equal(200, reloaded.Current.Layout.NavigatorWidth);
Assert.Equal(420, reloaded.Current.Layout.DetailsWidth);
Assert.True(reloaded.Current.Layout.ShoulderCollapsed);
Assert.Equal(280, reloaded.Current.Layout.HelmHistoryHeight);
var columns = reloaded.Current.Layout.ColumnsFor("resources/pods");
Assert.NotNull(columns);

View File

@ -54,11 +54,12 @@ public class ReleaseNotesTests {
[Fact]
public void Shipped_notes_keep_older_work_out_of_the_current_version() {
var notes = ReleaseNotes.AddedSince(ReleaseNotes.Text(), seenVersion: null, currentVersion: "0.8.6");
var notes = ReleaseNotes.AddedSince(ReleaseNotes.Text(), seenVersion: null, currentVersion: "0.8.7");
var note = Assert.Single(notes);
Assert.Equal("0.8.6", note.Version);
Assert.Contains(note.Added, line => line.Contains("drain dialog", StringComparison.Ordinal));
Assert.Equal("0.8.7", note.Version);
Assert.Contains(note.Added, line => line.Contains("Analyze issues", StringComparison.Ordinal));
Assert.DoesNotContain(note.Added, line => line.Contains("drain dialog", StringComparison.Ordinal));
Assert.DoesNotContain(note.Added, line => line.Contains("Helm Charts", StringComparison.Ordinal));
Assert.DoesNotContain(note.Added, line => line.Contains("footer", StringComparison.OrdinalIgnoreCase));
}

View File

@ -0,0 +1,120 @@
using Avalonia;
using Avalonia.Controls;
namespace MaksIT.ClusterConsole.UI.Services;
/// <summary>
/// Shrinks a grid column to a rail while the right shoulder is collapsed, and restores the previous width.
/// </summary>
public static class ShoulderPane {
public const double RailWidth = 36;
public static readonly AttachedProperty<bool> CollapsedProperty =
AvaloniaProperty.RegisterAttached<Grid, bool>("Collapsed", typeof(ShoulderPane));
public static readonly AttachedProperty<int> ContentColumnProperty =
AvaloniaProperty.RegisterAttached<Grid, int>("ContentColumn", typeof(ShoulderPane), 2);
public static readonly AttachedProperty<int> SplitterColumnProperty =
AvaloniaProperty.RegisterAttached<Grid, int>("SplitterColumn", typeof(ShoulderPane), 1);
private static readonly AttachedProperty<double> ExpandedWidthProperty =
AvaloniaProperty.RegisterAttached<Grid, double>("ExpandedWidth", typeof(ShoulderPane));
private static readonly AttachedProperty<bool> WatchingProperty =
AvaloniaProperty.RegisterAttached<Grid, bool>("Watching", typeof(ShoulderPane));
static ShoulderPane() {
CollapsedProperty.Changed.AddClassHandler<Grid>((grid, _) => Apply(grid));
}
public static bool GetCollapsed(Grid grid) =>
grid.GetValue(CollapsedProperty);
public static void SetCollapsed(Grid grid, bool value) =>
grid.SetValue(CollapsedProperty, value);
public static int GetContentColumn(Grid grid) =>
grid.GetValue(ContentColumnProperty);
public static void SetContentColumn(Grid grid, int value) =>
grid.SetValue(ContentColumnProperty, value);
public static int GetSplitterColumn(Grid grid) =>
grid.GetValue(SplitterColumnProperty);
public static void SetSplitterColumn(Grid grid, int value) =>
grid.SetValue(SplitterColumnProperty, value);
private static void Apply(Grid grid) {
if (grid.ColumnDefinitions.Count == 0) {
grid.AttachedToVisualTree -= OnAttached;
grid.AttachedToVisualTree += OnAttached;
return;
}
Watch(grid);
var content = Column(grid, grid.GetValue(ContentColumnProperty));
if (content is null)
return;
var splitter = Column(grid, grid.GetValue(SplitterColumnProperty));
if (!grid.GetValue(CollapsedProperty)) {
var expanded = grid.GetValue(ExpandedWidthProperty);
if (expanded < 180)
return;
content.Width = new GridLength(expanded);
if (splitter is not null)
splitter.Width = new GridLength(4);
return;
}
if (content.Width.IsAbsolute && content.Width.Value > RailWidth + 1)
grid.SetValue(ExpandedWidthProperty, content.Width.Value);
content.Width = new GridLength(RailWidth);
if (splitter is not null)
splitter.Width = new GridLength(0);
}
private static void OnAttached(object? sender, VisualTreeAttachmentEventArgs e) {
if (sender is Grid grid)
Apply(grid);
}
private static void Watch(Grid grid) {
if (grid.GetValue(WatchingProperty))
return;
grid.SetValue(WatchingProperty, true);
var content = Column(grid, grid.GetValue(ContentColumnProperty));
if (content is null)
return;
content.PropertyChanged += (_, e) => {
if (e.Property != ColumnDefinition.WidthProperty || !grid.GetValue(CollapsedProperty))
return;
if (content.Width.IsAbsolute && Math.Abs(content.Width.Value - RailWidth) < 0.5)
return;
if (content.Width.IsAbsolute && content.Width.Value > RailWidth + 1)
grid.SetValue(ExpandedWidthProperty, content.Width.Value);
content.Width = new GridLength(RailWidth);
};
}
private static ColumnDefinition? Column(Grid grid, int index) =>
index >= 0 && index < grid.ColumnDefinitions.Count ? grid.ColumnDefinitions[index] : null;
}

View File

@ -212,6 +212,29 @@
<Style Selector="GridSplitter.shell-split-row:pointerover">
<Setter Property="Background" Value="#006199" />
</Style>
<Style Selector="Button.shoulder-rail">
<Setter Property="Background" Value="#16181b" />
<Setter Property="Foreground" Value="#E8F4FC" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Padding" Value="4,12" />
<Setter Property="CornerRadius" Value="0" />
</Style>
<Style Selector="Button.shoulder-rail:pointerover">
<Setter Property="Background" Value="#006199" />
</Style>
<Style Selector="Button.shoulder-toggle">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#9aa0a6" />
<Setter Property="Padding" Value="6,2" />
<Setter Property="MinWidth" Value="28" />
<Setter Property="FontSize" Value="16" />
</Style>
<Style Selector="Button.shoulder-toggle:pointerover">
<Setter Property="Foreground" Value="#E8F4FC" />
</Style>
<Style Selector="Border.details-toolbar">
<Setter Property="Background" Value="#1a1d20" />
<Setter Property="BorderBrush" Value="#2a2e33" />

View File

@ -150,6 +150,10 @@ public partial class ClusterChatViewModel : ObservableObject {
ChatStatus = ModelStatus();
}
[RelayCommand]
private void AskAboutIssues() =>
ChatInput = "Explain the overview warnings and errors. Which objects are still failing, and which events are leftover after the pods recovered?";
[RelayCommand]
private void AskAboutSelection() {
var selection = _selection();

View File

@ -94,6 +94,9 @@ public partial class ClusterOverviewViewModel : ObservableObject {
public bool HasOverviewErrors => OverviewErrors.Count > 0;
public string IssueSummary() =>
ClusterIssues.Format(new ClusterIssueSet([.. OverviewWarnings], [.. OverviewErrors]));
public bool HasContainerLimits => LimitRows.Count > 0;
public bool HasSelectedLimit => SelectedLimitRow is not null;

View File

@ -76,6 +76,7 @@ public partial class ClusterPageViewModel : ObservableObject, IDisposable {
text => EventsText = text,
SetYaml);
Chat = new ClusterChatViewModel(_configuration, _chat, ChatSelection);
shoulderCollapsed = _configuration.Current.Layout.ShoulderCollapsed;
Terminal = new PodTerminalViewModel(_workspace, CurrentShellTarget, () => SelectedTab == DetailTab.Terminal);
Logs = new PodLogsViewModel(_workspace, CurrentLogTarget);
}
@ -337,6 +338,32 @@ public partial class ClusterPageViewModel : ObservableObject, IDisposable {
public string ShoulderTitle => SelectedNavItem?.Title ?? "";
public string ShoulderRailText {
get {
if (IsClusterDashboard)
return "Chat";
return ShoulderTitle.Length > 0 ? ShoulderTitle : "Details";
}
}
[ObservableProperty]
private bool shoulderCollapsed;
partial void OnShoulderCollapsedChanged(bool value) {
var cfg = _configuration.Current;
if (cfg.Layout.ShoulderCollapsed == value)
return;
cfg.Layout.ShoulderCollapsed = value;
_configuration.Save(cfg);
}
[RelayCommand]
private void ToggleShoulder() =>
ShoulderCollapsed = !ShoulderCollapsed;
public string ShoulderHint =>
ShoulderHints.Text(SelectedNavItem?.Id, SelectedDescriptor);
@ -531,6 +558,7 @@ public partial class ClusterPageViewModel : ObservableObject, IDisposable {
OnPropertyChanged(nameof(IsWorkloadsDashboard));
OnPropertyChanged(nameof(IsResourceTable));
OnPropertyChanged(nameof(ShoulderTitle));
OnPropertyChanged(nameof(ShoulderRailText));
OnPropertyChanged(nameof(ShoulderHint));
NotifyDetailsUi();
_listedRows.Clear();
@ -2207,6 +2235,18 @@ public partial class ClusterPageViewModel : ObservableObject, IDisposable {
|| SelectedNavItem?.Id is ResourceCatalog.DaprSidecarsId or ResourceCatalog.DaprControlPlaneId
|| string.Equals(SelectedRow?.Document["kind"]?.GetValue<string>(), "Pod", StringComparison.OrdinalIgnoreCase);
private string ChatOverview() {
if (!IsClusterDashboard)
return OverviewText;
var issues = Overview.IssueSummary();
if (string.IsNullOrWhiteSpace(OverviewText))
return issues;
return OverviewText + Environment.NewLine + Environment.NewLine + issues;
}
private ClusterChatSelection ChatSelection() =>
new() {
ContextName = Name,
@ -2215,7 +2255,7 @@ public partial class ClusterPageViewModel : ObservableObject, IDisposable {
ResourceName = SelectedRow?.Name,
PodName = TargetPodName,
ContainerName = SelectedContainer?.Name,
Overview = OverviewText,
Overview = ChatOverview(),
Events = EventsText,
LogText = Logs.LogsText,
Targets = ActionTargets,

View File

@ -1,54 +1,55 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MaksIT.ClusterConsole.UI.ViewModels.Shell"
xmlns:cluster="using:MaksIT.ClusterConsole.UI.ViewModels.Cluster"
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ClusterChatView"
x:DataType="vm:MainViewModel">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Spacing="6" Margin="0,8,0,0">
<TextBlock Text="{Binding ActivePage.Chat.ChatStatus}" Classes="hint" TextWrapping="Wrap" />
<DockPanel>
<Button DockPanel.Dock="Right"
Content="Send"
Command="{Binding ActivePage.Chat.SendChatCommand}"
Margin="6,0,0,0" />
<TextBox Text="{Binding ActivePage.Chat.ChatInput}"
PlaceholderText="Ask about this cluster or the selected resource"
AcceptsReturn="False" />
</DockPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Content="Ask about selection" Command="{Binding ActivePage.Chat.AskAboutSelectionCommand}" IsVisible="{Binding ActivePage.HasSelectedRow}" />
<Button Content="Clear" Command="{Binding ActivePage.Chat.ClearChatCommand}" />
</StackPanel>
</StackPanel>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding ActivePage.Chat.ChatMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="cluster:ChatMessageViewModel">
<Border Classes="chat-bubble"
Classes.user="{Binding IsUser}"
Classes.assistant="{Binding IsAssistant}"
Classes.tool="{Binding IsTool}"
Classes.confirm="{Binding IsConfirm}">
<StackPanel Spacing="8">
<TextBlock Text="{Binding Text}" TextWrapping="Wrap" />
<ScrollViewer IsVisible="{Binding HasChange}"
MaxHeight="220"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding Change}"
FontFamily="Consolas, monospace"
TextWrapping="NoWrap" />
</ScrollViewer>
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding IsConfirm}">
<Button Content="Approve" Command="{Binding ApproveCommand}" IsEnabled="{Binding IsPending}" />
<Button Content="Reject" Command="{Binding RejectCommand}" IsEnabled="{Binding IsPending}" />
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MaksIT.ClusterConsole.UI.ViewModels.Shell"
xmlns:cluster="using:MaksIT.ClusterConsole.UI.ViewModels.Cluster"
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ClusterChatView"
x:DataType="vm:MainViewModel">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Spacing="6" Margin="0,8,0,0">
<TextBlock Text="{Binding ActivePage.Chat.ChatStatus}" Classes="hint" TextWrapping="Wrap" />
<DockPanel>
<Button DockPanel.Dock="Right"
Content="Send"
Command="{Binding ActivePage.Chat.SendChatCommand}"
Margin="6,0,0,0" />
<TextBox Text="{Binding ActivePage.Chat.ChatInput}"
PlaceholderText="Ask about this cluster or the selected resource"
AcceptsReturn="False" />
</DockPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Content="Analyze issues" Command="{Binding ActivePage.Chat.AskAboutIssuesCommand}" IsVisible="{Binding ActivePage.IsClusterDashboard}" />
<Button Content="Ask about selection" Command="{Binding ActivePage.Chat.AskAboutSelectionCommand}" IsVisible="{Binding ActivePage.HasSelectedRow}" />
<Button Content="Clear" Command="{Binding ActivePage.Chat.ClearChatCommand}" />
</StackPanel>
</StackPanel>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding ActivePage.Chat.ChatMessages}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="cluster:ChatMessageViewModel">
<Border Classes="chat-bubble"
Classes.user="{Binding IsUser}"
Classes.assistant="{Binding IsAssistant}"
Classes.tool="{Binding IsTool}"
Classes.confirm="{Binding IsConfirm}">
<StackPanel Spacing="8">
<TextBlock Text="{Binding Text}" TextWrapping="Wrap" />
<ScrollViewer IsVisible="{Binding HasChange}"
MaxHeight="220"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding Change}"
FontFamily="Consolas, monospace"
TextWrapping="NoWrap" />
</ScrollViewer>
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding IsConfirm}">
<Button Content="Approve" Command="{Binding ApproveCommand}" IsEnabled="{Binding IsPending}" />
<Button Content="Reject" Command="{Binding RejectCommand}" IsEnabled="{Binding IsPending}" />
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</UserControl>

View File

@ -1,364 +1,386 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MaksIT.ClusterConsole.UI.ViewModels.Shell"
xmlns:charts="using:MaksIT.ClusterConsole.UI.Controls.Charts"
xmlns:converters="using:MaksIT.ClusterConsole.UI.Converters"
xmlns:layout="using:MaksIT.ClusterConsole.UI.Services"
xmlns:shared="using:MaksIT.ClusterConsole.Shared"
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ClusterDashboardView"
x:DataType="vm:MainViewModel">
<Grid Margin="16" RowDefinitions="Auto,*">
<StackPanel Spacing="6">
<TextBlock Text="{Binding ClusterTitle}" FontSize="22" FontWeight="SemiBold" />
<TextBlock Text="{Binding ActivePage.OverviewText}" Classes="hint" />
<TextBlock Text="{Binding ActivePage.Overview.MetricsHint}" Classes="hint" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,8,0,0">
<Button Content="Cluster"
Classes="scope-toggle"
Classes.selected="{Binding !ActivePage.Overview.OverviewPerNode}"
Command="{Binding ActivePage.Overview.ShowClusterOverviewCommand}" />
<Button Content="Nodes"
Classes="scope-toggle"
Classes.selected="{Binding ActivePage.Overview.OverviewPerNode}"
Command="{Binding ActivePage.Overview.ShowNodesOverviewCommand}" />
</StackPanel>
</StackPanel>
<Grid Grid.Row="1" Margin="0,16,0,0" RowDefinitions="*,Auto">
<Grid IsVisible="{Binding !ActivePage.Overview.OverviewPerNode}"
ColumnDefinitions="*,*"
RowDefinitions="Auto,Auto">
<Border Background="#25282c" Padding="16" CornerRadius="6" Margin="0,0,8,8" ClipToBounds="True">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="CPU" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding ActivePage.Overview.CpuCaption}" Classes="hint" />
</Grid>
<ProgressBar Grid.Row="1" Minimum="0" Maximum="100" Value="{Binding ActivePage.Overview.CpuPercent}" Height="8" Margin="0,8,0,8" />
<charts:Sparkline Grid.Row="2"
Height="72"
HorizontalAlignment="Stretch"
Values="{Binding ActivePage.Overview.CpuHistory}"
LineBrush="#00a7a0" />
<charts:ResourceBar Grid.Row="3"
Height="20"
Margin="0,8,0,8"
HorizontalAlignment="Stretch"
Used="{Binding ActivePage.Overview.CpuSlice.Used}"
Requests="{Binding ActivePage.Overview.CpuSlice.Requests}"
Limits="{Binding ActivePage.Overview.CpuSlice.Limits}"
Allocatable="{Binding ActivePage.Overview.CpuSlice.Allocatable}"
Capacity="{Binding ActivePage.Overview.CpuSlice.Capacity}" />
<WrapPanel Grid.Row="4" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#00a7a0" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.UsageLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff9f0a" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.RequestsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff4d6d" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.LimitsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#6b7280" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.AllocatableLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#3f454c" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.CapacityLine}" />
</StackPanel>
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.LimitsWarning}"
Classes="warning"
IsVisible="{Binding ActivePage.Overview.CpuSlice.LimitsExceedCapacity}" />
</WrapPanel>
</Grid>
</Border>
<Border Grid.Column="1" Background="#25282c" Padding="16" CornerRadius="6" Margin="8,0,0,8" ClipToBounds="True">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Memory" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding ActivePage.Overview.MemoryCaption}" Classes="hint" />
</Grid>
<ProgressBar Grid.Row="1" Minimum="0" Maximum="100" Value="{Binding ActivePage.Overview.MemoryPercent}" Height="8" Margin="0,8,0,8" />
<charts:Sparkline Grid.Row="2"
Height="72"
HorizontalAlignment="Stretch"
Values="{Binding ActivePage.Overview.MemoryHistory}"
LineBrush="#C93DCE" />
<charts:ResourceBar Grid.Row="3"
Height="20"
Margin="0,8,0,8"
HorizontalAlignment="Stretch"
Used="{Binding ActivePage.Overview.MemorySlice.Used}"
Requests="{Binding ActivePage.Overview.MemorySlice.Requests}"
Limits="{Binding ActivePage.Overview.MemorySlice.Limits}"
Allocatable="{Binding ActivePage.Overview.MemorySlice.Allocatable}"
Capacity="{Binding ActivePage.Overview.MemorySlice.Capacity}" />
<WrapPanel Grid.Row="4" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#00a7a0" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.UsageLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff9f0a" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.RequestsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff4d6d" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.LimitsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#6b7280" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.AllocatableLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#3f454c" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.CapacityLine}" />
</StackPanel>
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.LimitsWarning}"
Classes="warning"
IsVisible="{Binding ActivePage.Overview.MemorySlice.LimitsExceedCapacity}" />
</WrapPanel>
</Grid>
</Border>
<Border Grid.Row="1" Grid.ColumnSpan="2" Background="#25282c" Padding="16" CornerRadius="6" Margin="0,8,0,0">
<Grid RowDefinitions="Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Pods" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding ActivePage.Overview.PodCaption}" Classes="hint" />
</Grid>
<ProgressBar Grid.Row="1" Minimum="0" Maximum="100" Value="{Binding ActivePage.Overview.PodPercent}" Height="8" Margin="0,8,0,8" />
<charts:ResourceBar Grid.Row="2"
Height="20"
HorizontalAlignment="Stretch"
Used="{Binding ActivePage.Overview.PodSlice.Used}"
Allocatable="{Binding ActivePage.Overview.PodSlice.Allocatable}"
Capacity="{Binding ActivePage.Overview.PodSlice.Capacity}"
ShowRequests="False" />
<WrapPanel Grid.Row="3" Margin="0,8,0,0" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,0">
<Border Width="8" Height="8" Background="#00a7a0" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.PodSlice.UsageLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,0">
<Border Width="8" Height="8" Background="#6b7280" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.PodSlice.AllocatableLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Width="8" Height="8" Background="#3f454c" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.PodSlice.CapacityLine}" />
</StackPanel>
</WrapPanel>
</Grid>
</Border>
</Grid>
<ScrollViewer IsVisible="{Binding ActivePage.Overview.OverviewPerNode}"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding ActivePage.Overview.NodeUsages}" HorizontalAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:NodeUsageViewModel">
<Border Background="#25282c" Padding="16" CornerRadius="6" Margin="0,0,0,16" HorizontalAlignment="Stretch">
<Grid RowDefinitions="Auto,*">
<TextBlock Text="{Binding Name}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,12" />
<Grid Grid.Row="1" ColumnDefinitions="*,*" RowDefinitions="*,Auto">
<StackPanel Spacing="6" Margin="0,0,8,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="CPU" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding Cpu.Caption}" Classes="hint" />
</Grid>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding CpuPercent}" Height="8" />
<charts:Sparkline Height="72" HorizontalAlignment="Stretch" Values="{Binding CpuHistory}" LineBrush="#00a7a0" />
<charts:ResourceBar Height="16"
HorizontalAlignment="Stretch"
Used="{Binding Cpu.Used}"
Requests="{Binding Cpu.Requests}"
Limits="{Binding Cpu.Limits}"
Allocatable="{Binding Cpu.Allocatable}"
Capacity="{Binding Cpu.Capacity}" />
<TextBlock Text="{Binding Cpu.UsageLine}" Classes="hint" />
<TextBlock Text="{Binding Cpu.RequestsLine}" Classes="hint" />
<TextBlock Text="{Binding Cpu.LimitsLine}" Classes="hint" />
<TextBlock Text="{Binding Cpu.LimitsWarning}" Classes="warning" IsVisible="{Binding Cpu.LimitsExceedCapacity}" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="6" Margin="8,0,0,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Memory" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding Memory.Caption}" Classes="hint" />
</Grid>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding MemoryPercent}" Height="8" />
<charts:Sparkline Height="72" HorizontalAlignment="Stretch" Values="{Binding MemoryHistory}" LineBrush="#C93DCE" />
<charts:ResourceBar Height="16"
HorizontalAlignment="Stretch"
Used="{Binding Memory.Used}"
Requests="{Binding Memory.Requests}"
Limits="{Binding Memory.Limits}"
Allocatable="{Binding Memory.Allocatable}"
Capacity="{Binding Memory.Capacity}" />
<TextBlock Text="{Binding Memory.UsageLine}" Classes="hint" />
<TextBlock Text="{Binding Memory.RequestsLine}" Classes="hint" />
<TextBlock Text="{Binding Memory.LimitsLine}" Classes="hint" />
<TextBlock Text="{Binding Memory.LimitsWarning}" Classes="warning" IsVisible="{Binding Memory.LimitsExceedCapacity}" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Spacing="6" Margin="0,12,0,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Pods" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding Pods.Caption}" Classes="hint" />
</Grid>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding PodPercent}" Height="8" />
<charts:ResourceBar Height="16"
HorizontalAlignment="Stretch"
Used="{Binding Pods.Used}"
Allocatable="{Binding Pods.Allocatable}"
Capacity="{Binding Pods.Capacity}"
ShowRequests="False" />
<TextBlock Text="{Binding Pods.UsageLine}" Classes="hint" />
</StackPanel>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel Grid.Row="1" Margin="0,8,0,0" Spacing="8">
<Border Background="#25282c"
Padding="12,10"
CornerRadius="6"
MaxHeight="220"
IsVisible="{Binding ActivePage.Overview.HasContainerLimits}">
<Grid RowDefinitions="Auto,*">
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,8">
<TextBlock Text="{Binding ActivePage.Overview.LimitsCaption}"
FontWeight="SemiBold"
Classes.warning="{Binding ActivePage.Overview.HasLimitOvercommit}" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
<Button Content="Apply selected"
Command="{Binding ActivePage.Overview.ApplySelectedLimitCommand}"
IsVisible="{Binding ActivePage.Overview.HasSelectedLimit}" />
<Button Content="Apply edits"
Command="{Binding ActivePage.Overview.ApplyChangedLimitsCommand}"
IsVisible="{Binding ActivePage.Overview.HasDirtyLimits}" />
</StackPanel>
</Grid>
<DataGrid Name="OverviewLimitsGrid"
layout:LayoutMemento.Table="{x:Static shared:LayoutSettings.OverviewLimitsTable}"
Grid.Row="1"
MinHeight="120"
ItemsSource="{Binding ActivePage.Overview.LimitRows}"
SelectedItem="{Binding ActivePage.Overview.SelectedLimitRow}"
AutoGenerateColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="True"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
x:CompileBindings="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Workload" Binding="{Binding Workload}" IsReadOnly="True" Width="180" />
<DataGridTextColumn Header="Namespace" Binding="{Binding Namespace}" IsReadOnly="True" Width="120" />
<DataGridTextColumn Header="Container" Binding="{Binding Container}" IsReadOnly="True" Width="120" />
<DataGridTextColumn Header="Pods" Binding="{Binding Pods}" IsReadOnly="True" Width="60" />
<DataGridTextColumn Header="CPU req" Binding="{Binding CpuRequest}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="CPU limit" Binding="{Binding CpuLimit}" Width="90" />
<DataGridTextColumn Header="CPU share" Binding="{Binding CpuShare}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="MEM req" Binding="{Binding MemoryRequest}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="MEM limit" Binding="{Binding MemoryLimit}" Width="90" />
<DataGridTextColumn Header="MEM share" Binding="{Binding MemoryShare}" IsReadOnly="True" Width="90" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
<Border Background="#25282c" Padding="12,10,12,0" CornerRadius="6"
MaxHeight="180"
IsVisible="{Binding ActivePage.Overview.HasOverviewErrors}">
<Grid RowDefinitions="Auto,*">
<TextBlock Text="{Binding ActivePage.Overview.OverviewErrorsCaption}" Classes="issue-errors" Margin="0,0,0,8" />
<DataGrid Name="OverviewErrorsGrid"
layout:LayoutMemento.Table="{x:Static shared:LayoutSettings.OverviewErrorsTable}"
Grid.Row="1"
MinHeight="100"
ItemsSource="{Binding ActivePage.Overview.OverviewErrors}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
x:CompileBindings="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="*" />
<DataGridTextColumn Header="Object" Binding="{Binding ObjectName}" Width="180" />
<DataGridTextColumn Header="Type" Binding="{Binding Kind}" Width="80" />
<DataGridTemplateColumn Header="Status" Width="90" SortMemberPath="State">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding State}"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="6,0"
Foreground="{Binding State, Converter={x:Static converters:IssueStateBrushConverter.Instance}, ConverterParameter=Error}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Age" Binding="{Binding Age}" SortMemberPath="OccurredAt" Width="80" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
<Border Background="#25282c" Padding="12,10" CornerRadius="6">
<StackPanel>
<TextBlock Text="{Binding ActivePage.Overview.OverviewWarningsCaption}"
Classes="issue-warnings"
Margin="0,0,0,8"
IsVisible="{Binding ActivePage.Overview.HasOverviewWarnings}" />
<DataGrid Name="OverviewWarningsGrid"
layout:LayoutMemento.Table="{x:Static shared:LayoutSettings.OverviewWarningsTable}"
MinHeight="100"
MaxHeight="160"
ItemsSource="{Binding ActivePage.Overview.OverviewWarnings}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
IsVisible="{Binding ActivePage.Overview.HasOverviewWarnings}"
x:CompileBindings="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="*" />
<DataGridTextColumn Header="Object" Binding="{Binding ObjectName}" Width="180" />
<DataGridTextColumn Header="Type" Binding="{Binding Kind}" Width="80" />
<DataGridTemplateColumn Header="Status" Width="90" SortMemberPath="State">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding State}"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="6,0"
Foreground="{Binding State, Converter={x:Static converters:IssueStateBrushConverter.Instance}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Age" Binding="{Binding Age}" SortMemberPath="OccurredAt" Width="80" />
</DataGrid.Columns>
</DataGrid>
<StackPanel HorizontalAlignment="Center"
IsVisible="{Binding !ActivePage.Overview.HasOverviewWarnings}">
<TextBlock Text="No issues found" FontWeight="SemiBold" HorizontalAlignment="Center" />
<TextBlock Text="Everything is fine in the Cluster" Classes="hint" HorizontalAlignment="Center" Margin="0,4,0,0" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Grid>
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MaksIT.ClusterConsole.UI.ViewModels.Shell"
xmlns:charts="using:MaksIT.ClusterConsole.UI.Controls.Charts"
xmlns:converters="using:MaksIT.ClusterConsole.UI.Converters"
xmlns:layout="using:MaksIT.ClusterConsole.UI.Services"
xmlns:shared="using:MaksIT.ClusterConsole.Shared"
xmlns:views="using:MaksIT.ClusterConsole.UI.Views.Cluster"
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ClusterDashboardView"
x:DataType="vm:MainViewModel">
<Grid Margin="16" RowDefinitions="Auto,*">
<StackPanel Spacing="6">
<TextBlock Text="{Binding ClusterTitle}" FontSize="22" FontWeight="SemiBold" />
<TextBlock Text="{Binding ActivePage.OverviewText}" Classes="hint" />
<TextBlock Text="{Binding ActivePage.Overview.MetricsHint}" Classes="hint" TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,8,0,0">
<Button Content="Cluster"
Classes="scope-toggle"
Classes.selected="{Binding !ActivePage.Overview.OverviewPerNode}"
Command="{Binding ActivePage.Overview.ShowClusterOverviewCommand}" />
<Button Content="Nodes"
Classes="scope-toggle"
Classes.selected="{Binding ActivePage.Overview.OverviewPerNode}"
Command="{Binding ActivePage.Overview.ShowNodesOverviewCommand}" />
</StackPanel>
</StackPanel>
<Grid Grid.Row="1" Margin="0,16,0,0" ColumnDefinitions="*,Auto">
<Grid RowDefinitions="*,Auto">
<Grid IsVisible="{Binding !ActivePage.Overview.OverviewPerNode}"
ColumnDefinitions="*,*"
RowDefinitions="Auto,Auto">
<Border Background="#25282c" Padding="16" CornerRadius="6" Margin="0,0,8,8" ClipToBounds="True">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="CPU" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding ActivePage.Overview.CpuCaption}" Classes="hint" />
</Grid>
<ProgressBar Grid.Row="1" Minimum="0" Maximum="100" Value="{Binding ActivePage.Overview.CpuPercent}" Height="8" Margin="0,8,0,8" />
<charts:Sparkline Grid.Row="2"
Height="72"
HorizontalAlignment="Stretch"
Values="{Binding ActivePage.Overview.CpuHistory}"
LineBrush="#00a7a0" />
<charts:ResourceBar Grid.Row="3"
Height="20"
Margin="0,8,0,8"
HorizontalAlignment="Stretch"
Used="{Binding ActivePage.Overview.CpuSlice.Used}"
Requests="{Binding ActivePage.Overview.CpuSlice.Requests}"
Limits="{Binding ActivePage.Overview.CpuSlice.Limits}"
Allocatable="{Binding ActivePage.Overview.CpuSlice.Allocatable}"
Capacity="{Binding ActivePage.Overview.CpuSlice.Capacity}" />
<WrapPanel Grid.Row="4" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#00a7a0" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.UsageLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff9f0a" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.RequestsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff4d6d" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.LimitsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#6b7280" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.AllocatableLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#3f454c" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.CapacityLine}" />
</StackPanel>
<TextBlock Text="{Binding ActivePage.Overview.CpuSlice.LimitsWarning}"
Classes="warning"
IsVisible="{Binding ActivePage.Overview.CpuSlice.LimitsExceedCapacity}" />
</WrapPanel>
</Grid>
</Border>
<Border Grid.Column="1" Background="#25282c" Padding="16" CornerRadius="6" Margin="8,0,0,8" ClipToBounds="True">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Memory" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding ActivePage.Overview.MemoryCaption}" Classes="hint" />
</Grid>
<ProgressBar Grid.Row="1" Minimum="0" Maximum="100" Value="{Binding ActivePage.Overview.MemoryPercent}" Height="8" Margin="0,8,0,8" />
<charts:Sparkline Grid.Row="2"
Height="72"
HorizontalAlignment="Stretch"
Values="{Binding ActivePage.Overview.MemoryHistory}"
LineBrush="#C93DCE" />
<charts:ResourceBar Grid.Row="3"
Height="20"
Margin="0,8,0,8"
HorizontalAlignment="Stretch"
Used="{Binding ActivePage.Overview.MemorySlice.Used}"
Requests="{Binding ActivePage.Overview.MemorySlice.Requests}"
Limits="{Binding ActivePage.Overview.MemorySlice.Limits}"
Allocatable="{Binding ActivePage.Overview.MemorySlice.Allocatable}"
Capacity="{Binding ActivePage.Overview.MemorySlice.Capacity}" />
<WrapPanel Grid.Row="4" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#00a7a0" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.UsageLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff9f0a" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.RequestsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#ff4d6d" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.LimitsLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#6b7280" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.AllocatableLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,4">
<Border Width="8" Height="8" Background="#3f454c" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.CapacityLine}" />
</StackPanel>
<TextBlock Text="{Binding ActivePage.Overview.MemorySlice.LimitsWarning}"
Classes="warning"
IsVisible="{Binding ActivePage.Overview.MemorySlice.LimitsExceedCapacity}" />
</WrapPanel>
</Grid>
</Border>
<Border Grid.Row="1" Grid.ColumnSpan="2" Background="#25282c" Padding="16" CornerRadius="6" Margin="0,8,0,0">
<Grid RowDefinitions="Auto,Auto,Auto,Auto">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Pods" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding ActivePage.Overview.PodCaption}" Classes="hint" />
</Grid>
<ProgressBar Grid.Row="1" Minimum="0" Maximum="100" Value="{Binding ActivePage.Overview.PodPercent}" Height="8" Margin="0,8,0,8" />
<charts:ResourceBar Grid.Row="2"
Height="20"
HorizontalAlignment="Stretch"
Used="{Binding ActivePage.Overview.PodSlice.Used}"
Allocatable="{Binding ActivePage.Overview.PodSlice.Allocatable}"
Capacity="{Binding ActivePage.Overview.PodSlice.Capacity}"
ShowRequests="False" />
<WrapPanel Grid.Row="3" Margin="0,8,0,0" Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,0">
<Border Width="8" Height="8" Background="#00a7a0" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.PodSlice.UsageLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,16,0">
<Border Width="8" Height="8" Background="#6b7280" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.PodSlice.AllocatableLine}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Width="8" Height="8" Background="#3f454c" VerticalAlignment="Center" />
<TextBlock Text="{Binding ActivePage.Overview.PodSlice.CapacityLine}" />
</StackPanel>
</WrapPanel>
</Grid>
</Border>
</Grid>
<ScrollViewer IsVisible="{Binding ActivePage.Overview.OverviewPerNode}"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding ActivePage.Overview.NodeUsages}" HorizontalAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:NodeUsageViewModel">
<Border Background="#25282c" Padding="16" CornerRadius="6" Margin="0,0,0,16" HorizontalAlignment="Stretch">
<Grid RowDefinitions="Auto,*">
<TextBlock Text="{Binding Name}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,12" />
<Grid Grid.Row="1" ColumnDefinitions="*,*" RowDefinitions="*,Auto">
<StackPanel Spacing="6" Margin="0,0,8,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="CPU" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding Cpu.Caption}" Classes="hint" />
</Grid>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding CpuPercent}" Height="8" />
<charts:Sparkline Height="72" HorizontalAlignment="Stretch" Values="{Binding CpuHistory}" LineBrush="#00a7a0" />
<charts:ResourceBar Height="16"
HorizontalAlignment="Stretch"
Used="{Binding Cpu.Used}"
Requests="{Binding Cpu.Requests}"
Limits="{Binding Cpu.Limits}"
Allocatable="{Binding Cpu.Allocatable}"
Capacity="{Binding Cpu.Capacity}" />
<TextBlock Text="{Binding Cpu.UsageLine}" Classes="hint" />
<TextBlock Text="{Binding Cpu.RequestsLine}" Classes="hint" />
<TextBlock Text="{Binding Cpu.LimitsLine}" Classes="hint" />
<TextBlock Text="{Binding Cpu.LimitsWarning}" Classes="warning" IsVisible="{Binding Cpu.LimitsExceedCapacity}" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="6" Margin="8,0,0,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Memory" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding Memory.Caption}" Classes="hint" />
</Grid>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding MemoryPercent}" Height="8" />
<charts:Sparkline Height="72" HorizontalAlignment="Stretch" Values="{Binding MemoryHistory}" LineBrush="#C93DCE" />
<charts:ResourceBar Height="16"
HorizontalAlignment="Stretch"
Used="{Binding Memory.Used}"
Requests="{Binding Memory.Requests}"
Limits="{Binding Memory.Limits}"
Allocatable="{Binding Memory.Allocatable}"
Capacity="{Binding Memory.Capacity}" />
<TextBlock Text="{Binding Memory.UsageLine}" Classes="hint" />
<TextBlock Text="{Binding Memory.RequestsLine}" Classes="hint" />
<TextBlock Text="{Binding Memory.LimitsLine}" Classes="hint" />
<TextBlock Text="{Binding Memory.LimitsWarning}" Classes="warning" IsVisible="{Binding Memory.LimitsExceedCapacity}" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Spacing="6" Margin="0,12,0,0">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Pods" FontWeight="SemiBold" />
<TextBlock Grid.Column="1" Text="{Binding Pods.Caption}" Classes="hint" />
</Grid>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding PodPercent}" Height="8" />
<charts:ResourceBar Height="16"
HorizontalAlignment="Stretch"
Used="{Binding Pods.Used}"
Allocatable="{Binding Pods.Allocatable}"
Capacity="{Binding Pods.Capacity}"
ShowRequests="False" />
<TextBlock Text="{Binding Pods.UsageLine}" Classes="hint" />
</StackPanel>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel Grid.Row="1" Margin="0,8,0,0" Spacing="8">
<Border Background="#25282c"
Padding="12,10"
CornerRadius="6"
MaxHeight="220"
IsVisible="{Binding ActivePage.Overview.HasContainerLimits}">
<Grid RowDefinitions="Auto,*">
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,8">
<TextBlock Text="{Binding ActivePage.Overview.LimitsCaption}"
FontWeight="SemiBold"
Classes.warning="{Binding ActivePage.Overview.HasLimitOvercommit}" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
<Button Content="Apply selected"
Command="{Binding ActivePage.Overview.ApplySelectedLimitCommand}"
IsVisible="{Binding ActivePage.Overview.HasSelectedLimit}" />
<Button Content="Apply edits"
Command="{Binding ActivePage.Overview.ApplyChangedLimitsCommand}"
IsVisible="{Binding ActivePage.Overview.HasDirtyLimits}" />
</StackPanel>
</Grid>
<DataGrid Name="OverviewLimitsGrid"
layout:LayoutMemento.Table="{x:Static shared:LayoutSettings.OverviewLimitsTable}"
Grid.Row="1"
MinHeight="120"
ItemsSource="{Binding ActivePage.Overview.LimitRows}"
SelectedItem="{Binding ActivePage.Overview.SelectedLimitRow}"
AutoGenerateColumns="False"
CanUserResizeColumns="True"
CanUserSortColumns="True"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
x:CompileBindings="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Workload" Binding="{Binding Workload}" IsReadOnly="True" Width="180" />
<DataGridTextColumn Header="Namespace" Binding="{Binding Namespace}" IsReadOnly="True" Width="120" />
<DataGridTextColumn Header="Container" Binding="{Binding Container}" IsReadOnly="True" Width="120" />
<DataGridTextColumn Header="Pods" Binding="{Binding Pods}" IsReadOnly="True" Width="60" />
<DataGridTextColumn Header="CPU req" Binding="{Binding CpuRequest}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="CPU limit" Binding="{Binding CpuLimit}" Width="90" />
<DataGridTextColumn Header="CPU share" Binding="{Binding CpuShare}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="MEM req" Binding="{Binding MemoryRequest}" IsReadOnly="True" Width="80" />
<DataGridTextColumn Header="MEM limit" Binding="{Binding MemoryLimit}" Width="90" />
<DataGridTextColumn Header="MEM share" Binding="{Binding MemoryShare}" IsReadOnly="True" Width="90" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
<Border Background="#25282c" Padding="12,10,12,0" CornerRadius="6"
MaxHeight="180"
IsVisible="{Binding ActivePage.Overview.HasOverviewErrors}">
<Grid RowDefinitions="Auto,*">
<TextBlock Text="{Binding ActivePage.Overview.OverviewErrorsCaption}" Classes="issue-errors" Margin="0,0,0,8" />
<DataGrid Name="OverviewErrorsGrid"
layout:LayoutMemento.Table="{x:Static shared:LayoutSettings.OverviewErrorsTable}"
Grid.Row="1"
MinHeight="100"
ItemsSource="{Binding ActivePage.Overview.OverviewErrors}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
x:CompileBindings="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="*" />
<DataGridTextColumn Header="Object" Binding="{Binding ObjectName}" Width="180" />
<DataGridTextColumn Header="Type" Binding="{Binding Kind}" Width="80" />
<DataGridTemplateColumn Header="Status" Width="90" SortMemberPath="State">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding State}"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="6,0"
Foreground="{Binding State, Converter={x:Static converters:IssueStateBrushConverter.Instance}, ConverterParameter=Error}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Age" Binding="{Binding Age}" SortMemberPath="OccurredAt" Width="80" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
<Border Background="#25282c" Padding="12,10" CornerRadius="6">
<StackPanel>
<TextBlock Text="{Binding ActivePage.Overview.OverviewWarningsCaption}"
Classes="issue-warnings"
Margin="0,0,0,8"
IsVisible="{Binding ActivePage.Overview.HasOverviewWarnings}" />
<DataGrid Name="OverviewWarningsGrid"
layout:LayoutMemento.Table="{x:Static shared:LayoutSettings.OverviewWarningsTable}"
MinHeight="100"
MaxHeight="160"
ItemsSource="{Binding ActivePage.Overview.OverviewWarnings}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
IsVisible="{Binding ActivePage.Overview.HasOverviewWarnings}"
x:CompileBindings="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Message" Binding="{Binding Message}" Width="*" />
<DataGridTextColumn Header="Object" Binding="{Binding ObjectName}" Width="180" />
<DataGridTextColumn Header="Type" Binding="{Binding Kind}" Width="80" />
<DataGridTemplateColumn Header="Status" Width="90" SortMemberPath="State">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding State}"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="6,0"
Foreground="{Binding State, Converter={x:Static converters:IssueStateBrushConverter.Instance}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Age" Binding="{Binding Age}" SortMemberPath="OccurredAt" Width="80" />
</DataGrid.Columns>
</DataGrid>
<StackPanel HorizontalAlignment="Center"
IsVisible="{Binding !ActivePage.Overview.HasOverviewWarnings}">
<TextBlock Text="No issues found" FontWeight="SemiBold" HorizontalAlignment="Center" />
<TextBlock Text="Everything is fine in the Cluster" Classes="hint" HorizontalAlignment="Center" Margin="0,4,0,0" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</Grid>
<Grid Grid.Column="1" Margin="16,0,0,0" IsVisible="{Binding ShowChat}">
<Border Width="380"
Padding="12"
Background="#25282c"
CornerRadius="6"
IsVisible="{Binding !ActivePage.ShoulderCollapsed}">
<DockPanel>
<Button DockPanel.Dock="Top"
Classes="shoulder-toggle"
Content="›"
HorizontalAlignment="Left"
Margin="0,0,0,8"
Command="{Binding ActivePage.ToggleShoulderCommand}"
ToolTip.Tip="Collapse" />
<views:ClusterChatView />
</DockPanel>
</Border>
<views:ShoulderRail />
</Grid>
</Grid>
</Grid>
</UserControl>

View File

@ -1,25 +1,33 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MaksIT.ClusterConsole.UI.ViewModels.Shell"
xmlns:grid="using:MaksIT.ClusterConsole.UI.Controls.Grid"
xmlns:layout="using:MaksIT.ClusterConsole.UI.Services"
xmlns:views="using:MaksIT.ClusterConsole.UI.Views.Cluster"
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ResourceBrowserView"
x:DataType="vm:MainViewModel">
<Grid Name="ResourceTableGrid" ColumnDefinitions="*,4,420">
<DataGrid Name="ResourceGrid"
layout:LayoutMemento.ResourceTable="True"
grid:ResourceTable.Page="{Binding ActivePage}"
grid:ResourceTable.Descriptor="{Binding SelectedDescriptor}"
ItemsSource="{Binding ActivePage.Rows}"
SelectedItem="{Binding ActivePage.SelectedRow}"
SelectionMode="Extended"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
ClipboardCopyMode="IncludeHeader" />
<GridSplitter Grid.Column="1" Classes="shell-split" ResizeDirection="Columns" />
<views:ResourceDetailsView Grid.Column="2" />
</Grid>
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MaksIT.ClusterConsole.UI.ViewModels.Shell"
xmlns:grid="using:MaksIT.ClusterConsole.UI.Controls.Grid"
xmlns:layout="using:MaksIT.ClusterConsole.UI.Services"
xmlns:views="using:MaksIT.ClusterConsole.UI.Views.Cluster"
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ResourceBrowserView"
x:DataType="vm:MainViewModel">
<Grid Name="ResourceTableGrid"
ColumnDefinitions="*,4,420"
layout:ShoulderPane.Collapsed="{Binding ActivePage.ShoulderCollapsed}">
<DataGrid Name="ResourceGrid"
layout:LayoutMemento.ResourceTable="True"
grid:ResourceTable.Page="{Binding ActivePage}"
grid:ResourceTable.Descriptor="{Binding SelectedDescriptor}"
ItemsSource="{Binding ActivePage.Rows}"
SelectedItem="{Binding ActivePage.SelectedRow}"
SelectionMode="Extended"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
ClipboardCopyMode="IncludeHeader" />
<GridSplitter Grid.Column="1"
Classes="shell-split"
ResizeDirection="Columns"
IsVisible="{Binding !ActivePage.ShoulderCollapsed}" />
<Grid Grid.Column="2">
<views:ResourceDetailsView IsVisible="{Binding !ActivePage.ShoulderCollapsed}" />
<views:ShoulderRail />
</Grid>
</Grid>
</UserControl>

View File

@ -13,13 +13,18 @@
<layout:LayoutBand Slot="DetailsWidth" Min="180" Max="1600" Fallback="380" />
</layout:LayoutMemento.Band>
<Border DockPanel.Dock="Top" Classes="details-toolbar">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding ActivePage.ShoulderTitle}"
<Grid ColumnDefinitions="Auto,*,Auto">
<Button Classes="shoulder-toggle"
Content="›"
Command="{Binding ActivePage.ToggleShoulderCommand}"
ToolTip.Tip="Collapse" />
<TextBlock Grid.Column="1"
Text="{Binding ActivePage.ShoulderTitle}"
FontWeight="SemiBold"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Margin="0,0,8,0" />
<Button Grid.Column="1"
Margin="4,0,8,0" />
<Button Grid.Column="2"
Content="Hint"
ToolTip.Tip="How to read this screen">
<Button.Flyout>

View File

@ -0,0 +1,23 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MaksIT.ClusterConsole.UI.ViewModels.Shell"
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ShoulderRail"
x:DataType="vm:MainViewModel"
IsVisible="{Binding ActivePage.ShoulderCollapsed}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Width="36">
<Button Classes="shoulder-rail"
Command="{Binding ActivePage.ToggleShoulderCommand}"
ToolTip.Tip="Show">
<StackPanel Spacing="10" HorizontalAlignment="Center">
<TextBlock Text="‹" FontSize="16" HorizontalAlignment="Center" />
<LayoutTransformControl>
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="90" />
</LayoutTransformControl.LayoutTransform>
<TextBlock Text="{Binding ActivePage.ShoulderRailText}" />
</LayoutTransformControl>
</StackPanel>
</Button>
</UserControl>

View File

@ -0,0 +1,9 @@
using Avalonia.Controls;
namespace MaksIT.ClusterConsole.UI.Views.Cluster;
public partial class ShoulderRail : UserControl {
public ShoulderRail() =>
InitializeComponent();
}

View File

@ -98,7 +98,6 @@
"--share=network",
"--socket=wayland",
"--socket=x11",
"--socket=pulseaudio",
"--device=dri",
"--filesystem=home"
],