(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] ## [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 ## [0.8.6] - 2026-10-03
### Added ### Added

View File

@ -1,8 +1,8 @@
# MaksIT.ClusterConsole # MaksIT.ClusterConsole
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-58.7%25-yellowgreen) ![Line Coverage](https://img.shields.io/badge/Line%20Coverage-59.2%25-yellowgreen)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-48.1%25-yellowgreen) ![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-48.7%25-yellowgreen)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-67.5%25-green) ![Method Coverage](https://img.shields.io/badge/Method%20Coverage-68.1%25-green)
![.NET](https://img.shields.io/badge/.NET-10-512BD4) ![.NET](https://img.shields.io/badge/.NET-10-512BD4)
![License](https://img.shields.io/badge/License-Apache%202.0-blue) ![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) ![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) ![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 --> <!-- microsoft-store 1 -->
![Cluster overview](assets/screenshots/overview.png) ![Cluster overview](assets/screenshots/overview.png)
@ -65,7 +65,7 @@ Ready, Restarts, Status, Node, CPU, and Memory. Filters and sort persist per clu
### Chat ### 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) ![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. 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 ## 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). 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 ## [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. - 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> <project_license>Apache-2.0</project_license>
<description> <description>
<p>**After installation, point the app at a kubeconfig that can reach a running cluster (KUBECONFIG or ~/.kube/config).**</p> <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>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> <p>Settings stay on this computer. Cluster access uses the network and files under the home folder (typically ~/.kube).</p>
<ul> <ul>
@ -35,35 +36,41 @@
</branding> </branding>
<screenshots> <screenshots>
<screenshot type="default"> <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> <caption>Cluster overview</caption>
</screenshot> </screenshot>
<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> <caption>Applications table</caption>
</screenshot> </screenshot>
<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> <caption>Pods table</caption>
</screenshot> </screenshot>
<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> <caption>Chat on a selected pod</caption>
</screenshot> </screenshot>
<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> <caption>Persistent volume claims</caption>
</screenshot> </screenshot>
<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> <caption>Dapr control plane</caption>
</screenshot> </screenshot>
<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> <caption>Port forwarding</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <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"> <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> <url type="details">https://github.com/MAKS-IT-COM/maksit-cluster-console/blob/v0.8.6/CHANGELOG.md</url>
<description> <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/) | | Homepage | [https://maks-it.com/](https://maks-it.com/) |
| Project | `src/MaksIT.ClusterConsole.UI/MaksIT.ClusterConsole.UI.csproj` | | Project | `src/MaksIT.ClusterConsole.UI/MaksIT.ClusterConsole.UI.csproj` |
| Target | `net10.0` | | 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` | | Runtime | `org.freedesktop.Platform` |
| SDK | `org.freedesktop.Sdk` | | SDK | `org.freedesktop.Sdk` |
| SDK extension | `org.freedesktop.Sdk.Extension.dotnet10` | | 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 --share=network
--socket=wayland --socket=wayland
--socket=x11 --socket=x11
--socket=pulseaudio
--device=dri --device=dri
--filesystem=home --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). 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`. | | [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`. | | [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 ## Pull request

View File

@ -1,9 +1,9 @@
# Flathub verification # 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 The portal generates the token. The file does not list the application id. HTTPS is required. A line that starts with `#` is a comment.
com.maks_it.clusterconsole
``` 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). 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> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Version>0.8.6</Version> <Version>0.8.7</Version>
<Product>Cluster Console</Product> <Product>Cluster Console</Product>
<AssemblyTitle>Cluster Console</AssemblyTitle> <AssemblyTitle>Cluster Console</AssemblyTitle>
<Company>MaksIT</Company> <Company>MaksIT</Company>

View File

@ -90,6 +90,23 @@ public static class ClusterIssues {
return $"{noun}: {active} ({resolved} resolved)"; 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) => private static List<ClusterIssue> Rank(IEnumerable<ClusterIssue> issues) =>
issues issues
.OrderBy(i => i.State == Resolved ? 1 : 0) .OrderBy(i => i.State == Resolved ? 1 : 0)

View File

@ -49,6 +49,8 @@ public sealed class LayoutSettings {
public double DetailsWidth { get; set; } = 380; public double DetailsWidth { get; set; } = 380;
public bool ShoulderCollapsed { get; set; }
public double HelmHistoryHeight { get; set; } = 220; public double HelmHistoryHeight { get; set; } = 220;
public string? SelectedNavId { get; set; } 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] [Fact]
public void Age_formats_hours_and_minutes() { public void Age_formats_hours_and_minutes() {
var now = DateTimeOffset.Parse("2026-08-19T15:00:00Z"); var now = DateTimeOffset.Parse("2026-08-19T15:00:00Z");

View File

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

View File

@ -54,11 +54,12 @@ public class ReleaseNotesTests {
[Fact] [Fact]
public void Shipped_notes_keep_older_work_out_of_the_current_version() { 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); var note = Assert.Single(notes);
Assert.Equal("0.8.6", note.Version); Assert.Equal("0.8.7", note.Version);
Assert.Contains(note.Added, line => line.Contains("drain dialog", StringComparison.Ordinal)); 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("Helm Charts", StringComparison.Ordinal));
Assert.DoesNotContain(note.Added, line => line.Contains("footer", StringComparison.OrdinalIgnoreCase)); 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"> <Style Selector="GridSplitter.shell-split-row:pointerover">
<Setter Property="Background" Value="#006199" /> <Setter Property="Background" Value="#006199" />
</Style> </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"> <Style Selector="Border.details-toolbar">
<Setter Property="Background" Value="#1a1d20" /> <Setter Property="Background" Value="#1a1d20" />
<Setter Property="BorderBrush" Value="#2a2e33" /> <Setter Property="BorderBrush" Value="#2a2e33" />

View File

@ -150,6 +150,10 @@ public partial class ClusterChatViewModel : ObservableObject {
ChatStatus = ModelStatus(); 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] [RelayCommand]
private void AskAboutSelection() { private void AskAboutSelection() {
var selection = _selection(); var selection = _selection();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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