mirror of
https://github.com/MAKS-IT-COM/maksit-cluster-console.git
synced 2026-11-13 20:26:10 +01:00
(feature): show overview chat and let the details pane collapse
This commit is contained in:
parent
665fa99125
commit
9e556c6048
13
CHANGELOG.md
13
CHANGELOG.md
@ -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
|
||||||
|
|||||||
12
README.md
12
README.md
@ -1,8 +1,8 @@
|
|||||||
# MaksIT.ClusterConsole
|
# MaksIT.ClusterConsole
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@ -39,7 +39,7 @@ Click a catalog row to open that context. A green dot is a live session in this
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
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 -->
|
||||||

|

|
||||||
@ -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`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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` |
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
120
src/MaksIT.ClusterConsole.UI/Services/ShoulderPane.cs
Normal file
120
src/MaksIT.ClusterConsole.UI/Services/ShoulderPane.cs
Normal 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;
|
||||||
|
}
|
||||||
@ -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" />
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
AcceptsReturn="False" />
|
AcceptsReturn="False" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<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="Ask about selection" Command="{Binding ActivePage.Chat.AskAboutSelectionCommand}" IsVisible="{Binding ActivePage.HasSelectedRow}" />
|
||||||
<Button Content="Clear" Command="{Binding ActivePage.Chat.ClearChatCommand}" />
|
<Button Content="Clear" Command="{Binding ActivePage.Chat.ClearChatCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
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"
|
||||||
|
xmlns:views="using:MaksIT.ClusterConsole.UI.Views.Cluster"
|
||||||
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ClusterDashboardView"
|
x:Class="MaksIT.ClusterConsole.UI.Views.Cluster.ClusterDashboardView"
|
||||||
x:DataType="vm:MainViewModel">
|
x:DataType="vm:MainViewModel">
|
||||||
<Grid Margin="16" RowDefinitions="Auto,*">
|
<Grid Margin="16" RowDefinitions="Auto,*">
|
||||||
@ -24,7 +25,8 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid Grid.Row="1" Margin="0,16,0,0" RowDefinitions="*,Auto">
|
<Grid Grid.Row="1" Margin="0,16,0,0" ColumnDefinitions="*,Auto">
|
||||||
|
<Grid RowDefinitions="*,Auto">
|
||||||
<Grid IsVisible="{Binding !ActivePage.Overview.OverviewPerNode}"
|
<Grid IsVisible="{Binding !ActivePage.Overview.OverviewPerNode}"
|
||||||
ColumnDefinitions="*,*"
|
ColumnDefinitions="*,*"
|
||||||
RowDefinitions="Auto,Auto">
|
RowDefinitions="Auto,Auto">
|
||||||
@ -360,5 +362,25 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -6,7 +6,9 @@
|
|||||||
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"
|
||||||
|
ColumnDefinitions="*,4,420"
|
||||||
|
layout:ShoulderPane.Collapsed="{Binding ActivePage.ShoulderCollapsed}">
|
||||||
<DataGrid Name="ResourceGrid"
|
<DataGrid Name="ResourceGrid"
|
||||||
layout:LayoutMemento.ResourceTable="True"
|
layout:LayoutMemento.ResourceTable="True"
|
||||||
grid:ResourceTable.Page="{Binding ActivePage}"
|
grid:ResourceTable.Page="{Binding ActivePage}"
|
||||||
@ -19,7 +21,13 @@
|
|||||||
CanUserResizeColumns="True"
|
CanUserResizeColumns="True"
|
||||||
CanUserSortColumns="True"
|
CanUserSortColumns="True"
|
||||||
ClipboardCopyMode="IncludeHeader" />
|
ClipboardCopyMode="IncludeHeader" />
|
||||||
<GridSplitter Grid.Column="1" Classes="shell-split" ResizeDirection="Columns" />
|
<GridSplitter Grid.Column="1"
|
||||||
<views:ResourceDetailsView Grid.Column="2" />
|
Classes="shell-split"
|
||||||
|
ResizeDirection="Columns"
|
||||||
|
IsVisible="{Binding !ActivePage.ShoulderCollapsed}" />
|
||||||
|
<Grid Grid.Column="2">
|
||||||
|
<views:ResourceDetailsView IsVisible="{Binding !ActivePage.ShoulderCollapsed}" />
|
||||||
|
<views:ShoulderRail />
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.ClusterConsole.UI.Views.Cluster;
|
||||||
|
|
||||||
|
public partial class ShoulderRail : UserControl {
|
||||||
|
public ShoulderRail() =>
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user