-
-
- @ev.Timestamp?.ToString("HH:mm:ss")
-
-
@ev.Message
-
- }
+
+
}
else
{
@@ -47,6 +58,35 @@
[Parameter] public ClefEvent? SelectedEvent { get; set; }
[Parameter] public EventCallback
OnSelect { get; set; }
+ ///
+ /// O OmniVirtualize exige ICollection: materializa uma vez por mudança de
+ /// parâmetros, em vez de a cada render.
+ ///
+ private ICollection _itens = Array.Empty();
+
+ protected override void OnParametersSet() =>
+ _itens = Eventos as ICollection ?? Eventos?.ToList() ?? (ICollection)Array.Empty();
+
+ /// Enter/Espaço ativam o item, como num botão.
+ private async Task AoTeclar(KeyboardEventArgs e, ClefEvent ev)
+ {
+ if (e.Key is "Enter" or " ")
+ {
+ await OnSelect.InvokeAsync(ev);
+ }
+ }
+
+ ///
+ /// Nome anunciado pelo leitor de tela. Sem ele, o item seria lido como um amontoado de
+ /// textos soltos — e o nível, que visualmente é um badge colorido, se perderia.
+ ///
+ private static string RotuloAcessivel(ClefEvent e)
+ {
+ var hora = e.Timestamp?.ToString("HH:mm:ss") ?? "";
+ var excecao = string.IsNullOrEmpty(e.Exception) ? "" : ", com exceção";
+ return $"{e.Level} às {hora}: {e.Message}{excecao}";
+ }
+
private string ObterResumoProps(ClefEvent e)
{
if (e.Properties == null) return "";
diff --git a/src/Components/LogToolbar.razor b/src/Components/LogToolbar.razor
index eade214..f5bb27e 100644
--- a/src/Components/LogToolbar.razor
+++ b/src/Components/LogToolbar.razor
@@ -23,8 +23,30 @@
Disabled="@(TotalCount == 0)"
Title="Exporta os eventos filtrados (não apenas a página atual)"
OnClick="OnExport" />
-
+
+ @* Só faz sentido quando há um detalhe aberto para reposicionar. O ícone aponta
+ para ONDE o painel vai, e não onde ele está — a lib não tem ícones de painel,
+ e a seta remove a ambiguidade. *@
+ @if (DetailVisible)
+ {
+
+ }
+ @* OmniPagination é zero-based; o app conta a partir de 1 (é o que aparece no
+ "X-Y de Z"). Sem a conversão, a página 1 era exibida como "2" e a última
+ ficava inalcançável. A tradução fica aqui, na fronteira com o componente. *@
+
@@ -42,4 +64,13 @@
[Parameter] public EventCallback OnToggleTail { get; set; }
[Parameter] public bool IsLoading { get; set; }
[Parameter] public EventCallback OnCancelLoad { get; set; }
+
+ /// Exibindo em tabela (em vez da lista).
+ [Parameter] public bool GridMode { get; set; }
+ [Parameter] public EventCallback OnToggleViewMode { get; set; }
+
+ /// Há um evento selecionado (o painel de detalhes está aberto).
+ [Parameter] public bool DetailVisible { get; set; }
+ [Parameter] public bool DetailAtBottom { get; set; }
+ [Parameter] public EventCallback OnToggleDetailPosition { get; set; }
}
diff --git a/src/Components/LogViewer.razor b/src/Components/LogViewer.razor
index 5f4adf0..c1e4820 100644
--- a/src/Components/LogViewer.razor
+++ b/src/Components/LogViewer.razor
@@ -1,6 +1,7 @@
@using System.Threading
@using ClefExplorer.Models
@using ClefExplorer.Services
+@using ClefExplorer.Helpers
@implements IDisposable
@inject LogStore Store
@@ -10,6 +11,7 @@
@inject SettingsService SettingsService
@inject DialogService Dialog
@inject NotificationService Notifications
+@inject UiPreferencesService UiPreferences
@inject IJSRuntime JS
@@ -60,25 +62,33 @@
TailEnabled="@Store.TailEnabled"
OnToggleTail="AlternarAoVivo"
IsLoading="@Store.IsLoading"
- OnCancelLoad="CancelarCarregamento" />
+ OnCancelLoad="CancelarCarregamento"
+ DetailVisible="@(_selected != null)"
+ DetailAtBottom="@DetalheAbaixo"
+ OnToggleDetailPosition="AlternarPosicaoDetalhe"
+ GridMode="@ModoTabela"
+ OnToggleViewMode="AlternarModoVisualizacao" />
- @* Sem seleção: lista ocupa todo o espaço (sem splitter, evitando a
- race de medição do OmniSplitter aninhado no estado de 1 pane).
- Com seleção: splitter lista|detalhe — criado quando o pane pai já
- tem largura, então a medição funciona. *@
+ @* Sem seleção: o miolo ocupa todo o espaço, SEM splitter. Manter o
+ splitter sempre montado (com o pane do detalhe colapsado) foi
+ tentado e reintroduz a race de medição: o splitter aninhado mede
+ 0px e o conteúdo some.
+
+ O @key na orientação: o splitter mede e fixa os tamanhos dos panes
+ na inicialização, então reaproveitar a instância deixaria as
+ medidas da disposição anterior aplicadas na nova. *@
@if (_selected != null)
{
-
-
+
+
-
+ @RenderEventos()
-
+
@@ -88,10 +98,7 @@
else
{
-
+ @RenderEventos()
}
@@ -101,7 +108,29 @@
+@* Lista ou tabela conforme a preferência — o resto do layout (splitter, detalhe) é o
+ mesmo nos dois modos, então só o miolo troca. *@
@code {
+ private RenderFragment RenderEventos() => __builder =>
+ {
+ if (ModoTabela)
+ {
+
+ }
+ };
+
private ClefEvent? _selected;
private bool _isBusy;
private List