+
-
+
@if (ShouldShowBadge)
{
@BadgeLabel
}
-
+
+ @if (ShowPendingIndicator)
+ {
+ (pending toggle, unsaved)
+ }
+
@switch (PrimaryAction)
{
case ActionKind.Toggle:
case ActionKind.DisabledToggle:
-
break;
case ActionKind.Spinner:
-
-
- Upgrading…
-
+ @if (UpgradeProgress is { } progress)
+ {
+ var verb = PhaseVerb(progress.CurrentPhase);
+ var hasFile = !string.IsNullOrEmpty(progress.CurrentEntryName);
+ var hasPosition = progress.CurrentBatchSize > 1;
+
+ var displayText = hasFile
+ ? (hasPosition ? $"{verb} {progress.CurrentBatchPosition} of {progress.CurrentBatchSize}" : verb)
+ : $"Preparing upgrade of {progress.CurrentBatchSize} {(progress.CurrentBatchSize == 1 ? "database" : "databases")}\u2026";
+
+
+
+
+ @displayText
+ @if (hasFile)
+ {
+ for @Entry.FileName
+ }
+
+
+
+ }
+ else
+ {
+
+
+ Upgrading…
+
+ }
+
break;
case ActionKind.Upgrade:
-
break;
case ActionKind.Retry:
-
break;
+
+ case ActionKind.RestoreFromBackup:
+
+ break;
+
+ case ActionKind.RetryClassification:
+
+ break;
}
+
+
diff --git a/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.cs b/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.cs
index 4bed2816..c746aa64 100644
--- a/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.cs
+++ b/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.cs
@@ -1,20 +1,23 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.
+using EventLogExpert.Logging.Abstractions;
+using EventLogExpert.Runtime.Banner;
using EventLogExpert.Runtime.Database;
+using EventLogExpert.Runtime.Database.Upgrade;
using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
namespace EventLogExpert.UI.Database;
public sealed partial class DatabaseEntryRow : ComponentBase
{
- ///
- /// Click-driven reveal flag for the trash strip on the left of the row. Set true when the name button is clicked,
- /// cleared when the cursor leaves the row -- so re-entering the row without re-clicking does not re-open the slide,
- /// even though the name button may still hold DOM focus. Keyboard navigation drives the reveal via :focus-visible in
- /// CSS instead.
- ///
- private bool _isMouseRevealed;
+ private readonly string _nameButtonId = $"db-row-{Guid.NewGuid():N}-name";
+ private readonly string _pendingStatusId = $"db-row-{Guid.NewGuid():N}-pending";
+
+ private ElementReference _nameButtonRef;
+ private ElementReference _removeButtonRef;
+ private bool _shouldFocusNameAfterRender;
private enum ActionKind
{
@@ -23,7 +26,9 @@ private enum ActionKind
DisabledToggle,
Upgrade,
Retry,
- Spinner
+ Spinner,
+ RestoreFromBackup,
+ RetryClassification
}
[Parameter] public bool EffectiveEnabled { get; set; }
@@ -32,27 +37,41 @@ private enum ActionKind
[Parameter] public bool IsClassificationPending { get; set; }
+ [Parameter] public bool IsSelected { get; set; }
+
+ [Parameter] public bool IsTogglePending { get; set; }
+
[Parameter] public bool IsUpgradeBlocked { get; set; }
[Parameter] public bool IsUpgrading { get; set; }
[Parameter] public EventCallback OnRemove { get; set; }
+ [Parameter] public EventCallback OnRestoreFromBackup { get; set; }
+
+ [Parameter] public EventCallback OnRetryClassification { get; set; }
+
+ [Parameter] public EventCallback OnSelectionToggle { get; set; }
+
[Parameter] public EventCallback OnToggle { get; set; }
[Parameter] public EventCallback OnUpgrade { get; set; }
+ [Parameter] public BannerProgressEntry? UpgradeProgress { get; set; }
+
private string BadgeKind => Entry.BackupExists ? "Recovery" : Entry.Status.ToString();
private string BadgeLabel => DatabaseStatusLabels.GetRowBadgeLabel(Entry);
+ private bool IsRestoreBlocked => IsUpgradeBlocked || IsUpgrading || UpgradeProgress is not null;
+
private ActionKind PrimaryAction
{
get
{
- if (Entry.BackupExists) { return ActionKind.None; }
+ if (Entry.BackupExists) { return ActionKind.RestoreFromBackup; }
- if (IsUpgrading) { return ActionKind.Spinner; }
+ if (IsUpgrading || UpgradeProgress is not null) { return ActionKind.Spinner; }
return Entry.Status switch
{
@@ -63,7 +82,7 @@ private ActionKind PrimaryAction
DatabaseStatus.UpgradeFailed => ActionKind.Retry,
DatabaseStatus.UnrecognizedSchema => ActionKind.None,
DatabaseStatus.ObsoleteSchema => ActionKind.None,
- DatabaseStatus.ClassificationFailed => ActionKind.None,
+ DatabaseStatus.ClassificationFailed => ActionKind.RetryClassification,
_ => ActionKind.None
};
}
@@ -71,10 +90,47 @@ private ActionKind PrimaryAction
private bool ShouldShowBadge => Entry.BackupExists ||
(!IsUpgrading &&
+ UpgradeProgress is null &&
Entry.Status != DatabaseStatus.Ready &&
Entry.Status != DatabaseStatus.UpgradeRequired);
- private void HandleNameClick() => _isMouseRevealed = true;
+ private bool ShowPendingIndicator => IsTogglePending && PrimaryAction != ActionKind.DisabledToggle;
+
+ [Inject] private ITraceLogger TraceLogger { get; init; } = null!;
+
+ public ValueTask FocusRemoveButtonAsync() => _removeButtonRef.FocusAsync(preventScroll: true);
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!_shouldFocusNameAfterRender) { return; }
+
+ _shouldFocusNameAfterRender = false;
+
+ try { await _nameButtonRef.FocusAsync(preventScroll: true); }
+ catch (ObjectDisposedException) { }
+ catch (JSException) { }
+ }
+
+ private static string PhaseVerb(UpgradePhase phase) => phase switch
+ {
+ UpgradePhase.BackingUp => "Backing up",
+ UpgradePhase.MigratingSchema => "Migrating schema",
+ UpgradePhase.Verifying => "Verifying",
+ _ => "Upgrading"
+ };
+
+ private void OnCancelClick()
+ {
+ _shouldFocusNameAfterRender = true;
+
+ try { UpgradeProgress?.Cancel(); }
+ catch (OperationCanceledException) { }
+ catch (Exception ex)
+ {
+ TraceLogger.Warning(
+ $"{nameof(DatabaseEntryRow)}.{nameof(OnCancelClick)}: cancel threw: {ex}");
+ }
+ }
- private void HandleRowMouseLeave() => _isMouseRevealed = false;
+ private async Task OnRemoveClick() => await OnRemove.InvokeAsync();
}
diff --git a/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.css b/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.css
index 5c6ca977..2f17af33 100644
--- a/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.css
+++ b/src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.css
@@ -1,46 +1,28 @@
.db-entry-row {
- --db-entry-trash-reveal: 2.5rem;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: center;
+ gap: .5rem;
- position: relative;
- overflow: hidden;
+ padding: .25rem .5rem;
+ background-color: var(--background-dark);
+
+ border-left: 3px solid transparent;
+
+ transition: background-color 180ms ease, border-left-color 180ms ease;
}
-.db-entry-row-content {
- position: relative;
+.db-entry-row--selected {
+ border-left-color: var(--clr-lightblue);
+}
+.db-entry-row-content {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 1rem;
- /* No left padding: the visible left indent lives on .db-entry-info so the
- content layer's left edge butts up against the trash strip when slid.
- Background matches the modal chrome (var(--background-dark)) so rows
- blend visually but the opaque fill is still required to cover the
- absolutely-positioned trash button underneath at rest. The trash
- button is rendered earlier in the DOM than this element, so this
- element paints above it via source order without needing z-index --
- which also keeps rows below dialog-level overlays (e.g. inline alerts). */
- padding: .15rem .5rem .15rem 0;
- background-color: var(--background-dark);
-
- transition: transform 200ms ease;
-}
-
-/* Reveal logic:
- - Mouse: clicking the name button sets the .db-entry-row--revealed flag in
- code-behind, which keeps the slide open until the cursor leaves the row
- (@onmouseleave clears the flag). Re-entering the row without re-clicking
- does NOT re-open the slide, even if the name button still has DOM focus.
- - Keyboard: :focus-visible on the name button (matched only when focus
- came from a key press, not a mouse click) opens the slide independently
- of hover/state, so keyboard users can see what's there.
- - Trash :focus keeps the strip open during the click (mouse focus or
- keyboard focus), so the click reliably lands. */
-.db-entry-row.db-entry-row--revealed .db-entry-row-content,
-.db-entry-row:has(.db-entry-name:focus-visible) .db-entry-row-content,
-.db-entry-row:has(.db-entry-remove-btn:focus) .db-entry-row-content {
- transform: translateX(var(--db-entry-trash-reveal));
+ padding: .25rem 0;
}
.db-entry-info {
@@ -48,8 +30,6 @@
align-items: center;
gap: .5rem;
min-width: 0;
-
- padding-left: .5rem;
}
.db-entry-name {
@@ -108,10 +88,29 @@
.db-entry-upgrading {
display: inline-flex;
align-items: center;
- gap: .35rem;
+ gap: .5rem;
color: var(--clr-lightblue);
font-style: italic;
+
+ animation: db-entry-upgrading-fade-in 200ms ease-out;
+}
+
+.db-entry-upgrading-status {
+ display: inline-flex;
+ align-items: center;
+ gap: .35rem;
+ min-width: 0;
+}
+
+.db-entry-upgrading-text {
+ display: inline-block;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ animation: db-entry-upgrading-text-fade 180ms ease-out;
}
.db-entry-spinner {
@@ -119,32 +118,35 @@
animation: db-entry-spin 1.5s linear infinite;
}
-.db-entry-remove-btn {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: var(--db-entry-trash-reveal);
+.db-entry-cancel-btn {
+ flex: 0 0 auto;
+ padding: .15rem .65rem;
- display: flex;
- align-items: center;
- justify-content: center;
+ background: transparent;
+ color: inherit;
+ border: 1px solid currentColor;
+ border-radius: .25rem;
- /* Recessed-well treatment: darker fill plus a soft inner shadow makes the
- exposed trash strip read as a depression cut into the row, rather than
- a button floating on top. */
- background-color: var(--surface-inset);
- box-shadow: inset 0 0 .35rem rgba(0, 0, 0, .55);
+ font-style: normal;
+
+ transition: background-color 150ms ease;
+
+ &:hover { background-color: rgba(255, 255, 255, 0.12); }
}
-@media (hover: none) {
- .db-entry-row-content {
- transform: translateX(var(--db-entry-trash-reveal));
- }
+.db-entry-remove-btn {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
@media (prefers-reduced-motion: reduce) {
- .db-entry-row-content {
+ .db-entry-row,
+ .db-entry-upgrading,
+ .db-entry-upgrading-text,
+ .db-entry-cancel-btn {
+ animation: none;
transition: none;
}
}
@@ -153,3 +155,31 @@
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
+
+@keyframes db-entry-upgrading-fade-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes db-entry-upgrading-text-fade {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+
+.db-entry-actions ::deep .option-select {
+ border-radius: 4px;
+}
+
+
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
+}
\ No newline at end of file
diff --git a/src/EventLogExpert.UI/DatabaseTools/DatabaseToolsModal.razor b/src/EventLogExpert.UI/DatabaseTools/DatabaseToolsModal.razor
index 6eae4113..b9b1ac52 100644
--- a/src/EventLogExpert.UI/DatabaseTools/DatabaseToolsModal.razor
+++ b/src/EventLogExpert.UI/DatabaseTools/DatabaseToolsModal.razor
@@ -1,7 +1,7 @@
@using EventLogExpert.UI.DatabaseTools.Tabs
@inherits ModalBase
-
-
-