From 76d53918197517eca58e7b9f1bce630368367d95 Mon Sep 17 00:00:00 2001 From: Anderson Fernandes do Nascimento Date: Sun, 2 Aug 2026 02:13:46 -0300 Subject: [PATCH 1/2] =?UTF-8?q?perf:=20parser=20pr=C3=B3prio,=20filtro=20p?= =?UTF-8?q?aralelo=20e=20fim=20da=20pagina=C3=A7=C3=A3o=20de=20exibi=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reúne o trabalho de desempenho e usabilidade da sessão, mais o que já estava pendente na árvore (serviços extraídos do LogStore, entrada de caminhos, descoberta de arquivos e arrastar-e-soltar interno). DESEMPENHO — medido sobre 314.973 eventos reais (104 arquivos, 322 MB) - Parser CLEF próprio sobre Utf8JsonReader (LeitorClef), no lugar do Serilog.Formatting.Compact.Reader/Newtonsoft: 33k -> 92k eventos/s e alocação transitória de 4.412 MB -> 1.057 MB. O leitor antigo construía uma JObject inteira por linha só para descartá-la e reparseava o message template a cada evento (200 mil parses para 5 templates). Validado por um oráculo que compara evento a evento com o parser antigo: 0 divergências, inclusive nos casos de borda (BOM, CRLF, emoji, JSON truncado, número maior que long). - Busca textual paralela (máscara bool + gather sequencial, que preserva a ordem): 'VAREJO' de 138,7 ms para 17,8 ms; regex de 1.038 ms para 160 ms. O custo vinha de LogEventPropertyValue.ToString() materializar uma string por propriedade por evento só para comparar. - PoolDeTextos: nível, template e chaves de propriedade eram recriados a cada linha — 3,8 milhões de instâncias para 27 valores distintos. -181 MB. - Snapshot imutável versionado no LogStore: Snapshot() deixa de copiar o conjunto a cada consulta e de segurar o lock enquanto copia. EXIBIÇÃO - Fim da paginação: lista e tabela recebem o conjunto filtrado inteiro e virtualizam. Antes o grid recebia 50 itens, então ordenar por uma coluna reordenava a página, não o log — que era o defeito por trás de "vem menos coisa do que deveria". - Coluna Mensagem agrupa pelo template (@mt), não pelo texto formatado, e os campos do template viram colunas próprias mesmo sendo raros. - Filtro de período passa a considerar a hora. MODO AO VIVO - Corrigido o defeito que o matava à meia-noite: o tail só varria os arquivos existentes no momento da carga e nunca via o arquivo do dia seguinte. - O poll deixa de abrir todos os arquivos para ler o tamanho (2.494 ms em rede contra 2,7 ms de uma enumeração de diretório) e o merge passou a ser só na região afetada (19 ms -> 0,29 ms). UI QUE NÃO TRAVA - LogStatistics.Compute saiu da thread da UI (544 ms a cada filtragem em 1M). - Exportação com progresso throttled: eram 200 mil IProgress.Report, 3,8 s só de fila de mensagens. - Indicador de carregamento no miolo da tela com progresso e cancelamento; _isBusy só quando não há conteúdo, e corrigido o caso em que ele ficava preso. CORREÇÕES - Assets da Omni.Blazor davam 404 desde a 0.4.0: o BasePath mudou de "Omni.Blazor/" para "_content/Omni.Blazor/" e o app abria sem estilo. - Regras de CSS miravam ".omni-datagrid", classe que nunca existiu na lib — o destaque da linha selecionada e a barra de erro nunca valeram. - Agrupar por uma propriedade ausente em uns eventos e nula em outros dava dois grupos "(vazio)". - @r (renderings) não era gravado na exportação CLEF: exportar e reabrir mudava a mensagem de 38 em 314.973 eventos. USABILIDADE - Busca na árvore de arquivos e menu de contexto (mostrar no Explorer, copiar caminho, isolar seleção). Pacotes atualizados (Omni.Blazor, WebView2, Serilog, Test SDK, coverlet). TEMPORÁRIO: o app aponta para um pacote local da Omni.Blazor até a 0.5.0 ser publicada — ver NuGet.Config. O CI fica vermelho até lá. Co-Authored-By: Claude Opus 5 --- .gitignore | 3 + NuGet.Config | 60 + README.md | 9 + src/ClefExplorer.csproj | 17 +- src/Components/LogFileTree.razor | 183 ++- src/Components/LogGrid.razor | 350 +++-- src/Components/LogHeader.razor | 21 +- src/Components/LogSidebar.razor | 38 +- src/Components/LogToolbar.razor | 100 +- src/Components/LogViewer.razor | 494 +++++-- src/Helpers/FiltroArvoreArquivos.cs | 76 + src/Helpers/LogColumnDiscovery.cs | 222 ++- src/Helpers/LogStatistics.cs | 182 ++- src/Helpers/PeriodoFiltro.cs | 33 + src/MainForm.cs | 120 +- src/Models/FileTreeNode.cs | 32 + src/Program.cs | 27 +- src/Services/CaminhosEntrada.cs | 35 + src/Services/ConsultaLogs.cs | 91 ++ src/Services/DescobertaArquivosLog.cs | 96 ++ src/Services/ExploradorArquivos.cs | 65 + src/Services/FiltroArquivosLogIgnorados.cs | 67 + src/Services/LeitorArquivoLog.cs | 275 ++++ src/Services/LeitorClef.cs | 907 ++++++++++++ src/Services/LogExporter.cs | 424 +++++- src/Services/LogFilter.cs | 394 +++++- src/Services/LogStore.cs | 1236 +++++++++++------ src/Services/PoolDeTextos.cs | 36 + src/Services/WebViewDropMode.cs | 35 + src/wwwroot/css/app.css | 104 +- src/wwwroot/index.html | 10 +- src/wwwroot/js/internal-drag.js | 67 + .../CaminhosEntradaTests.cs | 26 + .../ClefExplorer.Tests.csproj | 6 +- test/ClefExplorer.Tests/ConsultaLogsTests.cs | 44 + .../ExploradorArquivosTests.cs | 45 + .../FiltroArvoreArquivosTests.cs | 138 ++ .../LeitorArquivoLogTests.cs | 271 ++++ test/ClefExplorer.Tests/LeitorClefTests.cs | 790 +++++++++++ .../LogColumnDiscoveryTests.cs | 237 ++++ test/ClefExplorer.Tests/LogExporterTests.cs | 426 ++++++ test/ClefExplorer.Tests/LogFilterTests.cs | 332 ++++- test/ClefExplorer.Tests/LogStatisticsTests.cs | 245 ++++ test/ClefExplorer.Tests/LogStoreTests.cs | 661 +++++++++ test/ClefExplorer.Tests/PeriodoFiltroTests.cs | 67 + test/ClefExplorer.Tests/PoolDeTextosTests.cs | 74 + 46 files changed, 8255 insertions(+), 916 deletions(-) create mode 100644 NuGet.Config create mode 100644 src/Helpers/FiltroArvoreArquivos.cs create mode 100644 src/Helpers/PeriodoFiltro.cs create mode 100644 src/Models/FileTreeNode.cs create mode 100644 src/Services/CaminhosEntrada.cs create mode 100644 src/Services/ConsultaLogs.cs create mode 100644 src/Services/DescobertaArquivosLog.cs create mode 100644 src/Services/ExploradorArquivos.cs create mode 100644 src/Services/FiltroArquivosLogIgnorados.cs create mode 100644 src/Services/LeitorArquivoLog.cs create mode 100644 src/Services/LeitorClef.cs create mode 100644 src/Services/PoolDeTextos.cs create mode 100644 src/Services/WebViewDropMode.cs create mode 100644 src/wwwroot/js/internal-drag.js create mode 100644 test/ClefExplorer.Tests/CaminhosEntradaTests.cs create mode 100644 test/ClefExplorer.Tests/ConsultaLogsTests.cs create mode 100644 test/ClefExplorer.Tests/ExploradorArquivosTests.cs create mode 100644 test/ClefExplorer.Tests/FiltroArvoreArquivosTests.cs create mode 100644 test/ClefExplorer.Tests/LeitorArquivoLogTests.cs create mode 100644 test/ClefExplorer.Tests/LeitorClefTests.cs create mode 100644 test/ClefExplorer.Tests/PeriodoFiltroTests.cs create mode 100644 test/ClefExplorer.Tests/PoolDeTextosTests.cs diff --git a/.gitignore b/.gitignore index ce89292..32d2304 100644 --- a/.gitignore +++ b/.gitignore @@ -416,3 +416,6 @@ FodyWeavers.xsd *.msix *.msm *.msp + +# Capturas do Playwright MCP durante a inspeção da UI — depuração, nunca fonte. +.playwright-mcp/ diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..d371f53 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 27309ad..69dd4b0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,15 @@ As versões estão disponíveis neste repositório do GitHub junto com a Windows ## 🔧 Instalação e Execução +> **Dependência local temporária.** O `NuGet.config` da raiz adiciona uma fonte de pacotes +> apontando para `..\Omni\Omni.Blazor\artifacts`: o app consome uma build local da +> **Omni.Blazor** (`0.4.1-alpha.local.1`) enquanto a correção que virtualiza o modo agrupado +> do grid — sem ela, agrupar um log grande congela a aplicação — não é publicada no nuget.org. +> Para compilar **sem** o repositório da biblioteca clonado ao lado, apague o `NuGet.config` e +> volte o `PackageReference` de `AndersonN.Omni.Blazor` para `0.4.0` (compila, mas o +> agrupamento volta a travar em logs grandes). O arquivo sai daqui quando a `0.5.0` for +> publicada. O próprio `NuGet.config` detalha como gerar o pacote local. + 1. **Clonar o repositório:** ```bash git clone https://github.com/afernandes/ClefExplorer.git diff --git a/src/ClefExplorer.csproj b/src/ClefExplorer.csproj index 3a7d63b..20b2556 100644 --- a/src/ClefExplorer.csproj +++ b/src/ClefExplorer.csproj @@ -30,12 +30,17 @@ - - - - - - + + + + + + + diff --git a/src/Components/LogFileTree.razor b/src/Components/LogFileTree.razor index dc8742d..f6e32d2 100644 --- a/src/Components/LogFileTree.razor +++ b/src/Components/LogFileTree.razor @@ -1,44 +1,75 @@ @using System.IO @using System.Text.RegularExpressions +@using ClefExplorer.Helpers -@if (RootNodes.Any()) +@inject ContextMenuService ContextMenu +@inject ExploradorArquivos Explorador +@inject NotificationService Notifications +@inject IJSRuntime JS + +@if (!RootNodes.Any()) +{ +
+ Nenhum arquivo carregado +
+} +else if (_raizesVisiveis.Count == 0) +{ +
+ Nenhum arquivo corresponde à busca +
+} +else { - } -else -{ -
- Nenhum arquivo carregado -
-} @code { [Parameter] public IEnumerable Files { get; set; } = Enumerable.Empty(); [Parameter] public IEnumerable CheckedFiles { get; set; } = Enumerable.Empty(); [Parameter] public EventCallback> OnSelectionChanged { get; set; } + /// Termo digitado no campo de busca da barra lateral; vazio mostra tudo. + [Parameter] public string? Filtro { get; set; } + private List RootNodes { get; set; } = new(); private List _checkedNodes = new(); private List? _lastFiles; + private HashSet _visiveis = new(); + private List _raizesVisiveis = new(); + private string? _ultimoFiltro; + protected override void OnParametersSet() { var currentFiles = Files?.OrderBy(f => f).ToList() ?? new List(); @@ -57,8 +88,25 @@ else BuildTree(); _checkedNodes = ComputeChecked(currentChecked); } + + if (filesChanged || Filtro != _ultimoFiltro) + { + _ultimoFiltro = Filtro; + AplicarFiltro(); + } + } + + // ─── Busca na árvore ──────────────────────────────────────────────────────── + + private void AplicarFiltro() + { + _visiveis = FiltroArvoreArquivos.Visiveis(RootNodes, Filtro); + _raizesVisiveis = RootNodes.Where(_visiveis.Contains).ToList(); } + private List FilhosVisiveis(FileTreeNode node) => + node.Children.Where(_visiveis.Contains).ToList(); + private static string NodeIcon(FileTreeNode node) { if (node.IsBackup) return "archive"; @@ -111,6 +159,103 @@ else await OnSelectionChanged.InvokeAsync(set); } + // ─── Menu de contexto ─────────────────────────────────────────────────────── + + private void AbrirMenu(MouseEventArgs e, FileTreeNode? node) + { + if (node is null) return; + ContextMenu.Open(e, ConstruirMenu(node)); + } + + private List ConstruirMenu(FileTreeNode node) + { + var caminhos = node.CaminhosDescendentes().ToList(); + var arquivo = node.FullPath; + var itens = new List(); + + // Um nó de agrupamento não tem pasta própria: a hierarquia vem do NOME do + // arquivo, então não há o que revelar no Explorer. + if (!string.IsNullOrEmpty(arquivo)) + { + itens.Add(new ContextMenuItem + { + Text = "Mostrar no Explorer", + Icon = "folder", + OnClick = () => MostrarNoExplorer(arquivo), + }); + } + + if (caminhos.Count > 0) + { + itens.Add(new ContextMenuItem + { + Text = caminhos.Count == 1 ? "Copiar caminho" : $"Copiar {caminhos.Count} caminhos", + Icon = "copy", + OnClick = () => CopiarCaminhos(caminhos), + }); + + itens.Add(ContextMenuItem.Separator()); + + // Com dezenas de arquivos carregados, isolar um deles pelo checkbox significa + // desmarcar todos os outros a mão. + itens.Add(new ContextMenuItem + { + Text = caminhos.Count == 1 ? "Exibir somente este" : $"Exibir somente estes {caminhos.Count}", + Icon = "filter", + OnClick = () => ExibirSomente(caminhos), + }); + } + + itens.Add(new ContextMenuItem + { + Text = "Exibir todos os arquivos", + Icon = "list", + OnClick = ExibirTodos, + }); + + return itens; + } + + private Task MostrarNoExplorer(string caminho) + { + if (!Explorador.Revelar(caminho)) + { + Notifications.Warning("Arquivo não encontrado", caminho); + } + return Task.CompletedTask; + } + + private async Task CopiarCaminhos(IReadOnlyList caminhos) + { + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", string.Join(Environment.NewLine, caminhos)); + Notifications.Info(caminhos.Count == 1 ? "Caminho copiado" : $"{caminhos.Count} caminhos copiados"); + } + catch (Exception ex) + { + AppLog.Warning("Não foi possível copiar para a área de transferência", ex); + Notifications.Warning("Não foi possível copiar o caminho"); + } + } + + private Task ExibirSomente(IEnumerable caminhos) => + AplicarSelecao(new HashSet(caminhos, StringComparer.OrdinalIgnoreCase)); + + private Task ExibirTodos() => + AplicarSelecao(new HashSet(_lastFiles ?? new List(), StringComparer.OrdinalIgnoreCase)); + + /// + /// Troca a marcação inteira de uma vez. É a exceção à regra de que o OmniTree é a fonte + /// única durante a interação: aqui quem manda é uma ação explícita do usuário, e o + /// estado precisa ser reescrito nos dois lados (checkboxes e arquivos visíveis). + /// + private async Task AplicarSelecao(HashSet caminhos) + { + _checkedNodes = ComputeChecked(caminhos); + await OnSelectionChanged.InvokeAsync(caminhos); + } + // ─── Construção da árvore virtual (lógica preservada da versão Bootstrap) ─── private void BuildTree() @@ -347,12 +492,4 @@ else } } - public class FileTreeNode - { - public string Name { get; set; } = ""; - public string? FullPath { get; set; } - public List Children { get; set; } = new(); - public FileTreeNode? Parent { get; set; } - public bool IsBackup { get; set; } - } } diff --git a/src/Components/LogGrid.razor b/src/Components/LogGrid.razor index 68f5e93..01b33c9 100644 --- a/src/Components/LogGrid.razor +++ b/src/Components/LogGrid.razor @@ -12,123 +12,217 @@ aplica a visibilidade no próprio OnInitialized, então toda vez que o grid é recriado — o que acontece ao abrir/fechar o painel de detalhes, já que o layout troca de splitter — as escolhas do menu interno se perdiam. Mantendo o conjunto - aqui, ele sobrevive à recriação e ainda é persistido entre execuções. *@ - -
- - - - - - - -
- @foreach (var col in TodasAsColunas) - { - var c = col; - - } -
-
-
-
- - - - - - - - - - - @* Só o nome do arquivo na célula; o caminho completo fica no title. *@ - - - - - - - - - @* Colunas derivadas do conteúdo dos logs carregados. *@ - @foreach (var coluna in Colunas) - { - var c = coluna; - - } - -
+ aqui, ele sobrevive à recriação e ainda é persistido entre execuções. + + As colunas entram e saem por @if, e NÃO pelo parâmetro Visible: na 0.2.0 a coluna + lê Visible só no OnInitialized, então alterá-lo depois não mexia em nada — marcar + e desmarcar no seletor não fazia a coluna aparecer nem sumir. Declarando de fato + só as colunas escolhidas, a que sai é descartada e se desregistra do grid. + (A biblioteca já foi corrigida para reagir a Visible; quando o app subir de versão + dá para voltar ao parâmetro, que evita recriar a coluna a cada troca.) *@ + +
+ @* O grid não recebia nenhum sinal de trabalho em andamento: com a tabela ainda vazia, + a primeira filtragem de um log grande deixava a área central em "Nenhum registro + encontrado" — que é a resposta errada para "ainda estou filtrando". *@ + @if (IsBusy) + { +
+ + Carregando logs... +
+ } + else + { + @* Virtualize + sem paginação: o grid recebe o conjunto filtrado inteiro, então + ordenar, agrupar e filtrar valem sobre TODO o log. Só as linhas visíveis vão ao + DOM, e a rolagem substitui o paginador. *@ + + + + + + + + @* Em três seções: as fixas, as propriedades recorrentes do log e os + campos de message template. Numa lista única, os campos de template + (que podem ser dezenas) afogariam o resto. *@ +
+ @foreach (var col in LogGridColumns.Fixed) + { + @ItemDeColuna(col.Key, col.Title) + } + + @if (ColunasDePropriedade.Count > 0) + { +
Propriedades
+ @foreach (var col in ColunasDePropriedade) + { + @ItemDeColuna(col.Key, col.Title) + } + } + + @if (ColunasDeTemplate.Count > 0) + { +
Campos da mensagem
+ @foreach (var col in ColunasDeTemplate) + { + @ItemDeColuna(col.Key, col.Title) + } + } +
+
+
+
+ + + @* Agrupa em Ano › Mês › Dia. Sem a hierarquia a chave seria o INSTANTE EXATO, + que tem milissegundos e não se repete: cada evento viraria um grupo de um + item — medido sobre os 314.973 eventos de C:\TOTVSPDV\Logs, o resultado é o + teto do MaxGroups (10.000 grupos) e um aviso de corte no lugar da árvore. + Ver a página /showcase/datagrid-date-grouping do Forneria.Demo. + + MaxGroups e AutoCollapseGroupsThreshold ficam nos defaults da biblioteca + (10.000 e 100). Não é omissão: no mesmo conjunto, agrupar por data custa + 147 ms e achata 314.988 linhas, das quais ~110 chegam ao DOM — a lista + achatada é entregue à virtualização, e é o teto do MaxGroups que segura o + caso degenerado de uma coluna quase-única. Baixar o auto-colapso não teria + efeito aqui: ele conta os grupos de PRIMEIRO nível, e esta hierarquia começa + pelo ano, quase sempre um só. *@ + @if (VisibleColumns.Contains(LogGridColumns.Timestamp)) + { + + } + + @if (VisibleColumns.Contains(LogGridColumns.Level)) + { + + + + } + + @* O VALOR da coluna é o template (@mt), não a mensagem formatada: agrupar pelo + texto renderizado daria um grupo por linha, já que os parâmetros mudam a cada + evento. Pelo template, "Intervalo otimizado para {ProviderKey}" junta todas as + ocorrências — e os parâmetros viram colunas próprias (ver + LogColumnDiscovery.ColumnSource.TemplateField) para agrupar e filtrar por eles. + + Os três seletores têm papéis distintos aqui: + Property → chave do agrupamento, ordenação e filtro (o template); + TextSelector → rótulo do cabeçalho de grupo, que o grid tira do texto da + célula do PRIMEIRO item — com a mensagem formatada, o grupo + se chamaria por uma de suas linhas; + Template → o que a célula mostra de fato: a mensagem renderizada. + + Efeito colateral assumido: o filtro DESTA coluna procura no template; + procurar por um valor ("RacClient") se faz na coluna do campo ou na busca + global. Sem @mt (mensagem literal), tudo cai na própria mensagem. *@ + @if (VisibleColumns.Contains(LogGridColumns.Message)) + { + + + + } + + @* Só o nome do arquivo na célula; o caminho completo fica no title. *@ + @if (VisibleColumns.Contains(LogGridColumns.SourceFile)) + { + + + + } + + @if (VisibleColumns.Contains(LogGridColumns.Exception)) + { + + + + } + + @* Colunas derivadas do conteúdo dos logs carregados. + + Property entrega o valor TIPADO (número como número) e TextSelector cuida do + que aparece na célula. Com o texto nos dois papéis, a ordenação comparava + string e "591" vinha antes de "78". *@ + @foreach (var coluna in Colunas.Where(c => VisibleColumns.Contains(c.Key))) + { + var c = coluna; + + } + +
+ }
@code { + /// Filtragem em andamento sem nada ainda para mostrar. + [Parameter] public bool IsBusy { get; set; } + [Parameter] public IReadOnlyList Eventos { get; set; } = Array.Empty(); [Parameter] public ClefEvent? SelectedEvent { get; set; } [Parameter] public EventCallback OnSelect { get; set; } @@ -143,9 +237,23 @@ private bool _colunasOpen; - /// Fixas primeiro, depois as descobertas — a mesma ordem da tabela. - private IEnumerable<(string Key, string Title)> TodasAsColunas => - LogGridColumns.Fixed.Concat(Colunas.Select(c => (c.Key, c.Title))); + /// Propriedades recorrentes do log (SourceContext, Application…). + private IReadOnlyList ColunasDePropriedade => + Colunas.Where(c => c.Source == ColumnSource.Property).ToList(); + + /// Parâmetros dos message templates ({ProviderKey}, {Interval}…). + private IReadOnlyList ColunasDeTemplate => + Colunas.Where(c => c.Source == ColumnSource.TemplateField).ToList(); + + private RenderFragment ItemDeColuna(string key, string title) => __builder => + { + + }; private async Task Alternar(string key, bool visivel) { @@ -159,6 +267,14 @@ private static string NomeArquivo(ClefEvent e) => string.IsNullOrEmpty(e.SourceFile) ? string.Empty : Path.GetFileName(e.SourceFile); + /// + /// Identidade da mensagem para agrupar, ordenar e filtrar: o template quando existe, + /// senão a própria mensagem (logs sem @mt, ou linhas em que o template É a + /// mensagem por não ter parâmetro). + /// + private static string TemplateOuMensagem(ClefEvent e) => + !string.IsNullOrEmpty(e.MessageTemplate) ? e.MessageTemplate : e.Message ?? string.Empty; + /// Destaca a linha selecionada e as de erro, como na lista. private string LinhaCss(ClefEvent e) { diff --git a/src/Components/LogHeader.razor b/src/Components/LogHeader.razor index af99720..577dfdb 100644 --- a/src/Components/LogHeader.razor +++ b/src/Components/LogHeader.razor @@ -17,12 +17,9 @@ @onclick="ToggleRegex">.*
+ @* O seletor de "itens por página" saiu junto com a paginação: lista e tabela + virtualizam o conjunto filtrado inteiro, e a rolagem faz a navegação. *@
-
@@ -31,9 +28,6 @@ [Parameter] public string TextoPesquisa { get; set; } = string.Empty; [Parameter] public EventCallback TextoPesquisaChanged { get; set; } - [Parameter] public int QuantidadeRegistros { get; set; } - [Parameter] public EventCallback QuantidadeRegistrosChanged { get; set; } - [Parameter] public bool UsarRegex { get; set; } [Parameter] public EventCallback UsarRegexChanged { get; set; } @@ -42,12 +36,6 @@ [Parameter] public EventCallback OnReload { get; set; } - // Páginas grandes só passaram a fazer sentido depois que a lista virou virtualizada: - // antes, 1000 itens significavam 1000 nós no DOM. - private readonly int[] _qtdOptions = { 30, 50, 100, 250, 500, 1000 }; - - private static string FormatQtd(int v) => $"{v} itens"; - private async Task OnSearchChanged(string? v) { TextoPesquisa = v ?? string.Empty; @@ -60,9 +48,4 @@ await UsarRegexChanged.InvokeAsync(UsarRegex); } - private async Task OnQtdChanged(int v) - { - QuantidadeRegistros = v; - await QuantidadeRegistrosChanged.InvokeAsync(v); - } } diff --git a/src/Components/LogSidebar.razor b/src/Components/LogSidebar.razor index 7598a90..b02226a 100644 --- a/src/Components/LogSidebar.razor +++ b/src/Components/LogSidebar.razor @@ -1,4 +1,5 @@ @using System.IO +@using ClefExplorer.Helpers @using ClefExplorer.Models