Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/EventLogExpert.Runtime/Banner/BannerViewSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static IReadOnlyList<BannerCycleItem> BuildCycle(
IReadOnlyList<ErrorBannerEntry> errorBanners,
IReadOnlyList<DatabaseEntry> attentionEntries,
bool attentionDismissed,
bool attentionSuppressedByModalContext,
BannerProgressEntry? backgroundProgress,
IReadOnlyList<BannerInfoEntry> infoBanners)
{
Expand All @@ -36,16 +37,20 @@ public static IReadOnlyList<BannerCycleItem> BuildCycle(
return [new BannerCycleItem(BannerView.Critical, 0, null)];
}

bool includeAttention = attentionEntries.Count > 0
&& !attentionDismissed
&& !attentionSuppressedByModalContext;

var items = new List<BannerCycleItem>(
errorBanners.Count + (attentionEntries.Count > 0 && !attentionDismissed ? 1 : 0)
errorBanners.Count + (includeAttention ? 1 : 0)
+ (backgroundProgress is not null ? 1 : 0) + infoBanners.Count);

for (int i = 0; i < errorBanners.Count; i++)
{
items.Add(new BannerCycleItem(BannerView.Error, i, errorBanners[i].Id));
}

if (attentionEntries.Count > 0 && !attentionDismissed)
if (includeAttention)
{
items.Add(new BannerCycleItem(BannerView.Attention, 0, null));
}
Expand Down
230 changes: 230 additions & 0 deletions src/EventLogExpert.UI/Banner/BannerCycleStateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using EventLogExpert.Runtime.Banner;
using EventLogExpert.Runtime.Modal;
using EventLogExpert.UI.DatabaseTools;

namespace EventLogExpert.UI.Banner;

public sealed class BannerCycleStateService : IBannerCycleStateService, IDisposable
{
private readonly IAttentionBannerService _attention;
private readonly ICriticalErrorService _critical;
private readonly IErrorBannerService _errors;
private readonly IInfoBannerService _infos;
private readonly IModalCoordinator _modalCoordinator;
private readonly IProgressBannerService _progress;
private readonly Lock _stateLock = new();

private BannerView _currentView;
private int _displayedIndex;
private IReadOnlyList<BannerCycleItem> _items = [];
private bool _modalContentDisplayed;
private BannerCycleItem? _pendingOverrideItem;
private BannerCycleItem? _selectedItem;

public BannerCycleStateService(
IAttentionBannerService attention,
IErrorBannerService errors,
IInfoBannerService infos,
IProgressBannerService progress,
ICriticalErrorService critical,
IModalCoordinator modalCoordinator)
{
_attention = attention;
_errors = errors;
_infos = infos;
_progress = progress;
_critical = critical;
_modalCoordinator = modalCoordinator;

_attention.StateChanged += OnFacetChanged;
_errors.StateChanged += OnFacetChanged;
_infos.StateChanged += OnFacetChanged;
_progress.StateChanged += OnFacetChanged;
_critical.StateChanged += OnFacetChanged;
_modalCoordinator.StateChanged += OnFacetChanged;

RebuildAndReselect();
}

public event Action? StateChanged;

public BannerView CurrentView
{
get { using (_stateLock.EnterScope()) { return _currentView; } }
}

public int DisplayedIndex
{
get { using (_stateLock.EnterScope()) { return _displayedIndex; } }
}

public IReadOnlyList<BannerCycleItem> Items
{
get { using (_stateLock.EnterScope()) { return _items; } }
}

public bool ModalContentDisplayed
{
get { using (_stateLock.EnterScope()) { return _modalContentDisplayed; } }
}

public BannerCycleItem? SelectedItem
{
get { using (_stateLock.EnterScope()) { return _selectedItem; } }
}

public void Dispose()
{
_attention.StateChanged -= OnFacetChanged;
_errors.StateChanged -= OnFacetChanged;
_infos.StateChanged -= OnFacetChanged;
_progress.StateChanged -= OnFacetChanged;
_critical.StateChanged -= OnFacetChanged;
_modalCoordinator.StateChanged -= OnFacetChanged;
}

public void MoveNext()
{
using (_stateLock.EnterScope())
{
var items = _items;
if (_displayedIndex >= items.Count - 1) { return; }

_displayedIndex++;
_selectedItem = items[_displayedIndex];
_currentView = _selectedItem.View;
}

StateChanged?.Invoke();
}

public void MovePrev()
{
using (_stateLock.EnterScope())
{
var items = _items;
if (_displayedIndex <= 0 || items.Count == 0) { return; }

_displayedIndex--;
_selectedItem = items[_displayedIndex];
_currentView = _selectedItem.View;
}

StateChanged?.Invoke();
Comment thread
jschick04 marked this conversation as resolved.
}

public void RegisterFallbackError(BannerCycleItem newCycleItem)
{
using (_stateLock.EnterScope())
{
_pendingOverrideItem = newCycleItem;
RebuildAndReselectLocked();
}

StateChanged?.Invoke();
}

public void SetModalContentDisplayed(bool displayed)
{
using (_stateLock.EnterScope())
{
if (_modalContentDisplayed == displayed) { return; }

_modalContentDisplayed = displayed;
}

StateChanged?.Invoke();
}

private static bool ItemMatches(BannerCycleItem selected, BannerCycleItem candidate)
{
if (selected.View != candidate.View) { return false; }

return selected.EntryId == candidate.EntryId;
}

private void OnFacetChanged()
{
using (_stateLock.EnterScope())
{
RebuildAndReselectLocked();
}

StateChanged?.Invoke();
}

private void RebuildAndReselect()
{
using (_stateLock.EnterScope())
{
RebuildAndReselectLocked();
}
}

private void RebuildAndReselectLocked()
{
if (_modalCoordinator.ActiveSession is null)
{
_modalContentDisplayed = false;
}

bool attentionSuppressed =
_modalCoordinator.ActiveSession?.ComponentType == typeof(DatabaseToolsModal);

IReadOnlyList<BannerCycleItem> items = BannerViewSelector.BuildCycle(
_critical.CurrentCritical,
_errors.ErrorBanners,
_attention.AttentionEntries,
_attention.AttentionDismissed,
attentionSuppressed,
_progress.BackgroundProgress,
_infos.InfoBanners);

_items = items;

if (items.Count == 0)
{
_selectedItem = null;
_displayedIndex = 0;
_currentView = BannerView.None;
_pendingOverrideItem = null;
return;
}

if (_pendingOverrideItem is not null)
{
for (int i = 0; i < items.Count; i++)
{
if (!ItemMatches(_pendingOverrideItem, items[i])) { continue; }

_displayedIndex = i;
_selectedItem = items[i];
_currentView = _selectedItem.View;
_pendingOverrideItem = null;
return;
}

_pendingOverrideItem = null;
}

if (_selectedItem is not null)
{
for (int i = 0; i < items.Count; i++)
{
if (!ItemMatches(_selectedItem, items[i])) { continue; }

_displayedIndex = i;
_selectedItem = items[i];
_currentView = _selectedItem.View;
return;
}
}

_displayedIndex = Math.Clamp(_displayedIndex, 0, items.Count - 1);
_selectedItem = items[_displayedIndex];
_currentView = _selectedItem.View;
}
}
70 changes: 33 additions & 37 deletions src/EventLogExpert.UI/Banner/BannerHost.razor
Original file line number Diff line number Diff line change
@@ -1,70 +1,66 @@
@{
@if (RendersContent)
{
BannerCycleItem? selected = CycleState.SelectedItem;
IReadOnlyList<BannerCycleItem> items = CycleState.Items;
BannerView view = CycleState.CurrentView;
int displayedIndex = CycleState.DisplayedIndex;

Exception? currentCritical = CriticalErrorService.CurrentCritical;
IReadOnlyList<ErrorBannerEntry> errors = ErrorBannerService.ErrorBanners;
IReadOnlyList<BannerInfoEntry> infos = InfoBannerService.InfoBanners;
IReadOnlyList<DatabaseEntry> attentionEntries = AttentionBannerService.AttentionEntries;
bool attentionDismissed = AttentionBannerService.AttentionDismissed;
BannerProgressEntry? backgroundProgress = ProgressBannerService.BackgroundProgress;

(BannerCycleItem? selected, BannerView view) = RebuildItemsAndPickSelected(
currentCritical,
errors,
attentionEntries,
attentionDismissed,
backgroundProgress,
infos);

_currentView = view;
bool showCyclePagination = _items.Count > 1 && view != BannerView.Critical;
bool showCyclePagination = items.Count > 1 && view != BannerView.Critical;

RenderFragment cycleNav = @<text>
@if (showCyclePagination)
{
<span class="banner-pagination">@(_displayedIndex + 1) of @_items.Count</span>
<span class="banner-pagination">@(displayedIndex + 1) of @items.Count</span>
<button aria-label="Previous banner"
class="banner-cycle-prev button"
disabled="@(_displayedIndex == 0)"
@onclick="OnCyclePrev"
disabled="@(displayedIndex == 0)"
@onclick="CycleState.MovePrev"
type="button">
<i aria-hidden="true" class="bi bi-chevron-left"></i>
</button>
<button aria-label="Next banner"
class="banner-cycle-next button"
disabled="@(_displayedIndex == _items.Count - 1)"
@onclick="OnCycleNext"
disabled="@(displayedIndex == items.Count - 1)"
@onclick="CycleState.MoveNext"
type="button">
<i aria-hidden="true" class="bi bi-chevron-right"></i>
</button>
}
</text>;
}

@switch (view)
{
case BannerView.Critical when currentCritical != null:
<CriticalBanner Critical="currentCritical" />
@switch (view)
{
case BannerView.Critical when currentCritical != null:
<CriticalBanner Critical="currentCritical" />

break;
break;

case BannerView.Error when selected != null && selected.IndexWithinSlice < errors.Count:
<ErrorBanner CycleNav="cycleNav" Entry="errors[selected.IndexWithinSlice]" />
case BannerView.Error when selected != null && selected.IndexWithinSlice < errors.Count:
<ErrorBanner CycleNav="cycleNav" Entry="errors[selected.IndexWithinSlice]" />

break;
break;

case BannerView.Attention when attentionEntries.Count > 0:
<AttentionBanner AttentionCount="attentionEntries.Count"
CycleNav="cycleNav"
OnFallbackErrorPosted="HandleFallbackErrorPosted" />
case BannerView.Attention when attentionEntries.Count > 0:
<AttentionBanner AttentionCount="attentionEntries.Count"
CycleNav="cycleNav"
OnFallbackErrorPosted="HandleFallbackErrorPosted" />

break;
break;

case BannerView.UpgradeProgress when backgroundProgress is { } progress:
<UpgradeProgressBanner CycleNav="cycleNav" Progress="progress" />
case BannerView.UpgradeProgress when backgroundProgress is { } progress:
<UpgradeProgressBanner CycleNav="cycleNav" Progress="progress" />

break;
break;

case BannerView.Info when selected != null && selected.IndexWithinSlice < infos.Count:
<InfoBanner CycleNav="cycleNav" Entry="infos[selected.IndexWithinSlice]" />
case BannerView.Info when selected != null && selected.IndexWithinSlice < infos.Count:
<InfoBanner CycleNav="cycleNav" Entry="infos[selected.IndexWithinSlice]" />

break;
break;
}
}
Loading
Loading