Skip to content

Commit a701b3f

Browse files
fix: ui bugs (#1293)
1 parent 4f7966f commit a701b3f

6 files changed

Lines changed: 164 additions & 12 deletions

File tree

src/KubeUI.Avalonia/Features/Resources/Properties/Controls/ResourceEventsView.axaml.cs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ namespace KubeUI.Avalonia.Features.Resources.Properties.Controls;
1414
public sealed partial class ResourceEventsView : UserControl, IInitializeCluster
1515
{
1616
private readonly DispatcherTimer _timer = new(DispatcherPriority.Background);
17+
private readonly ObservableCollection<ResourceEventItem> _items = [];
18+
private readonly ReadOnlyObservableCollection<ResourceEventItem> _readOnlyItems;
1719
private readonly ReadOnlyObservableCollection<Corev1Event> _emptyEvents = new([]);
1820

1921
private ClusterWorkspaceViewModel? _cluster;
2022
private ISourceCache<Corev1Event, string>? _eventCache;
2123
private IDisposable? _eventCacheSubscription;
2224
private ReadOnlyObservableCollection<Corev1Event> _matchedEvents;
2325
private bool _isDetached;
26+
private bool _refreshPending;
2427

2528
private IKubernetesObject<V1ObjectMeta>? _resource;
2629

@@ -33,6 +36,8 @@ public sealed partial class ResourceEventsView : UserControl, IInitializeCluster
3336
public ResourceEventsView()
3437
{
3538
InitializeComponent();
39+
_readOnlyItems = new ReadOnlyObservableCollection<ResourceEventItem>(_items);
40+
Items = _readOnlyItems;
3641
_matchedEvents = _emptyEvents;
3742
_timer.Interval = TimeSpan.FromSeconds(1);
3843
_timer.Tick += Timer_Tick;
@@ -43,7 +48,7 @@ protected override void OnDataContextChanged(EventArgs e)
4348
base.OnDataContextChanged(e);
4449
_resource = DataContext as IKubernetesObject<V1ObjectMeta>;
4550
RebuildEventSubscription();
46-
Refresh();
51+
RequestRefresh();
4752
}
4853

4954
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
@@ -87,12 +92,27 @@ public void Initialize(ClusterWorkspaceViewModel cluster)
8792
}
8893

8994
RebuildEventSubscription();
90-
Refresh();
95+
RequestRefresh();
9196
}
9297

9398
private void Timer_Tick(object? sender, EventArgs e)
9499
{
95-
Refresh();
100+
RequestRefresh();
101+
}
102+
103+
private void RequestRefresh()
104+
{
105+
if (_isDetached || _refreshPending)
106+
{
107+
return;
108+
}
109+
110+
_refreshPending = true;
111+
Dispatcher.UIThread.Post(() =>
112+
{
113+
_refreshPending = false;
114+
Refresh();
115+
}, DispatcherPriority.Background);
96116
}
97117

98118
private void Refresh()
@@ -134,25 +154,36 @@ private void RebuildEventSubscription()
134154
.SortAndBind(
135155
out _matchedEvents,
136156
SortExpressionComparer<Corev1Event>.Descending(@event => ResourceEventsSelector.GetSortTimestamp(@event)))
137-
.Subscribe(_ => Refresh());
157+
.Subscribe(_ => RequestRefresh());
138158
}
139159

140160
private void DisposeEventSubscription()
141161
{
142162
_eventCacheSubscription?.Dispose();
143163
_eventCacheSubscription = null;
144164
_matchedEvents = _emptyEvents;
165+
_refreshPending = false;
145166
}
146167

147168
private void Clear()
148169
{
149-
Items = [];
170+
if (_items.Count > 0)
171+
{
172+
_items.Clear();
173+
}
174+
150175
HasItems = false;
151176
}
152177

153178
private void UpdateItems(ResourceEventItem[] items)
154179
{
155-
Items = items;
180+
_items.Clear();
181+
182+
for (var index = 0; index < items.Length; index++)
183+
{
184+
_items.Add(items[index]);
185+
}
186+
156187
HasItems = items.Length > 0;
157188
}
158189
}

src/KubeUI.Avalonia/Features/Resources/Yaml/Behaviors/YamlEditorScrollBehavior.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ namespace KubeUI.Avalonia.Features.Resources.Yaml.Behaviors;
1515

1616
public sealed class YamlEditorScrollBehavior : Behavior<TextEditor>
1717
{
18+
private const double ScrollOffsetTolerance = 0.5;
19+
1820
private IFactory? _factory;
1921
private ScrollViewer? _scrollViewer;
2022
private IDisposable? _visibilitySubscription;
@@ -288,6 +290,13 @@ private void RestoreScrollOffset()
288290
}
289291

290292
var targetOffset = _pendingRestoreOffset ?? vm.ScrollOffset;
293+
var currentOffset = _scrollViewer.Offset;
294+
295+
if (AreOffsetsClose(currentOffset, targetOffset))
296+
{
297+
_pendingRestoreOffset = null;
298+
return;
299+
}
291300

292301
// Wait until the scroll extent is ready before applying a non-zero restore.
293302
if (targetOffset != default
@@ -312,6 +321,12 @@ private void RestoreScrollOffset()
312321
}
313322
}
314323

324+
private static bool AreOffsetsClose(Vector left, Vector right)
325+
{
326+
return Math.Abs(left.X - right.X) <= ScrollOffsetTolerance
327+
&& Math.Abs(left.Y - right.Y) <= ScrollOffsetTolerance;
328+
}
329+
315330
private void AttachScrollViewer()
316331
{
317332
if (AssociatedObject?.GetScrollViewer() is not ScrollViewer scrollViewer)
@@ -355,7 +370,7 @@ private void ScrollViewerOnPropertyChanged(object? sender, AvaloniaPropertyChang
355370
if (_pendingRestoreOffset is Vector pendingRestoreOffset
356371
&& pendingRestoreOffset != default
357372
&& sender is ScrollViewer scrollViewer
358-
&& scrollViewer.Offset != pendingRestoreOffset)
373+
&& !AreOffsetsClose(scrollViewer.Offset, pendingRestoreOffset))
359374
{
360375
return;
361376
}

src/KubeUI.Avalonia/Resources/Workloads/v1/Pod/Controls/PodContainerCell.axaml.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public partial class PodContainerCell : UserControl, IInitializeCluster
1717
private ClusterWorkspaceViewModel? _cluster;
1818

1919
private V1Pod? _viewModel;
20+
private readonly ObservableCollection<ViewModel> _containerStatuses = [];
2021

2122
private GroupApiVersionKind _groupApiVersionKind = GroupApiVersionKind.From<V1Pod>();
2223

@@ -26,6 +27,7 @@ public partial class PodContainerCell : UserControl, IInitializeCluster
2627
public PodContainerCell()
2728
{
2829
InitializeComponent();
30+
ContainerStatuses = _containerStatuses;
2931

3032
#if DEBUG
3133
if (Design.IsDesignMode)
@@ -96,14 +98,15 @@ protected override void OnUnloaded(RoutedEventArgs e)
9698

9799
private void PopulateData()
98100
{
101+
_containerStatuses.Clear();
102+
99103
if (DataContext is V1Pod pod)
100104
{
101105
_viewModel = pod;
102106

103-
var coll = new ObservableCollection<ViewModel>();
104107
if (pod?.Status?.ContainerStatuses != null)
105108
{
106-
coll.AddRange(pod.Status.ContainerStatuses.Select(x =>
109+
_containerStatuses.AddRange(pod.Status.ContainerStatuses.Select(x =>
107110
{
108111
var vm = new ViewModel()
109112
{
@@ -125,7 +128,7 @@ private void PopulateData()
125128
}
126129
if (pod?.Status?.InitContainerStatuses != null)
127130
{
128-
coll.AddRange(pod.Status.InitContainerStatuses.Select(x =>
131+
_containerStatuses.AddRange(pod.Status.InitContainerStatuses.Select(x =>
129132
{
130133
var vm = new ViewModel()
131134
{
@@ -148,7 +151,7 @@ private void PopulateData()
148151
}
149152
if (pod?.Status?.EphemeralContainerStatuses != null)
150153
{
151-
coll.AddRange(pod.Status.EphemeralContainerStatuses.Select(x =>
154+
_containerStatuses.AddRange(pod.Status.EphemeralContainerStatuses.Select(x =>
152155
{
153156
var vm = new ViewModel()
154157
{
@@ -170,7 +173,6 @@ private void PopulateData()
170173
return vm;
171174
}));
172175
}
173-
ContainerStatuses = coll;
174176
}
175177
}
176178

tests/KubeUI.Avalonia.Tests/Features/Resources/Properties/ResourceEventsViewTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
using System.Reflection;
12
using Avalonia.Controls;
23
using Avalonia.Headless.XUnit;
34
using Avalonia.Threading;
45
using k8s.Models;
56
using KubeUI.Avalonia.Features.Resources.Properties.Controls;
67
using KubeUI.Avalonia.Tests.Infra;
8+
using Shouldly;
79

810
namespace KubeUI.Avalonia.Tests.Features.Resources.Properties;
911

@@ -50,4 +52,45 @@ public async Task detached_resource_events_view_does_not_throw_when_data_context
5052

5153
Dispatcher.UIThread.RunJobs();
5254
}
55+
56+
[AvaloniaFact]
57+
public async Task refresh_keeps_a_stable_items_source_instance()
58+
{
59+
var workspace = new TestCluster().CreateWorkspace();
60+
await workspace.EnsureWorkspaceStateInitializedAsync();
61+
_ = TestApp.CurrentServices ?? throw new InvalidOperationException("Test services are not initialized.");
62+
63+
var view = new ResourceEventsView();
64+
view.Initialize(workspace);
65+
66+
var window = new Window
67+
{
68+
Content = view,
69+
};
70+
71+
window.Show();
72+
view.DataContext = new V1Pod
73+
{
74+
Metadata = new V1ObjectMeta
75+
{
76+
Name = "pod-1",
77+
NamespaceProperty = "default",
78+
}
79+
};
80+
81+
Dispatcher.UIThread.RunJobs();
82+
83+
var itemsBeforeRefresh = view.Items;
84+
85+
var refreshMethod = typeof(ResourceEventsView).GetMethod("Refresh", BindingFlags.Instance | BindingFlags.NonPublic);
86+
refreshMethod.ShouldNotBeNull();
87+
refreshMethod.Invoke(view, null);
88+
89+
Dispatcher.UIThread.RunJobs();
90+
91+
view.Items.ShouldBeSameAs(itemsBeforeRefresh);
92+
93+
window.Content = null;
94+
window.Close();
95+
}
5396
}

tests/KubeUI.Avalonia.Tests/Features/Resources/Yaml/ResourceYamlViewModelTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ public void YamlFoldingStrategy_DoesNotCreateFoldingsForFlatMappings()
189189
foldings.Count.ShouldBe(0);
190190
}
191191

192+
[AvaloniaFact]
193+
public void YamlEditorScrollBehavior_TreatsNearlyEqualOffsetsAsEqual()
194+
{
195+
var method = typeof(YamlEditorScrollBehavior).GetMethod("AreOffsetsClose", BindingFlags.Static | BindingFlags.NonPublic);
196+
method.ShouldNotBeNull();
197+
198+
var closeResult = (bool)method.Invoke(null, [new Vector(10, 20), new Vector(10.25, 20.25)])!;
199+
closeResult.ShouldBeTrue();
200+
201+
var farResult = (bool)method.Invoke(null, [new Vector(10, 20), new Vector(11, 20)])!;
202+
farResult.ShouldBeFalse();
203+
}
204+
192205
[AvaloniaFact]
193206
public void YamlFoldingStrategy_DoesNotCreateFoldingsForListItemsWithoutChildren()
194207
{

tests/KubeUI.Avalonia.Tests/Features/Workloads/Pod/PodContainerCellTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,52 @@ public async Task Tooltip_viewmodel_contains_type_status_restarts_and_image()
8282
eph.Restarts.ShouldBe(0);
8383
eph.Image.ShouldBe("ephemeral:image");
8484
}
85+
86+
[AvaloniaFact]
87+
public async Task refresh_keeps_a_stable_container_statuses_collection_instance()
88+
{
89+
var firstPod = new V1Pod
90+
{
91+
Status = new V1PodStatus
92+
{
93+
ContainerStatuses = new List<V1ContainerStatus>
94+
{
95+
new() { Name = "normal1", Ready = true, Started = true, RestartCount = 1 }
96+
}
97+
}
98+
};
99+
100+
var secondPod = new V1Pod
101+
{
102+
Status = new V1PodStatus
103+
{
104+
ContainerStatuses = new List<V1ContainerStatus>
105+
{
106+
new() { Name = "normal2", Ready = false, Started = false, RestartCount = 0 }
107+
}
108+
}
109+
};
110+
111+
var view = new PodContainerCell
112+
{
113+
DataContext = firstPod
114+
};
115+
116+
var window = new Window { Content = view };
117+
window.Show();
118+
Dispatcher.UIThread.RunJobs();
119+
120+
var statusesBeforeRefresh = view.ContainerStatuses;
121+
statusesBeforeRefresh.ShouldNotBeNull();
122+
123+
view.DataContext = secondPod;
124+
Dispatcher.UIThread.RunJobs();
125+
126+
view.ContainerStatuses.ShouldBeSameAs(statusesBeforeRefresh);
127+
view.ContainerStatuses.Count.ShouldBe(1);
128+
view.ContainerStatuses[0].Name.ShouldBe("normal2");
129+
130+
window.Content = null;
131+
window.Close();
132+
}
85133
}

0 commit comments

Comments
 (0)