Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ name: CI
on:
push:
branches: [main]
# Sem filtro de base: roda em PR para QUALQUER branch. Com "branches: [main]" os
# PRs empilhados (que apontam para outra branch de feature) ficavam sem CI, e o
# gate de testes só valia para o último PR da fila.
pull_request:
branches: [main]
workflow_dispatch:

permissions:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ jobs:
/p:AppxBundlePlatforms=x64
/p:UapAppxPackageBuildMode=StoreUpload
/p:AppxPackageSigningEnabled=false
/p:Version=${{ steps.ver.outputs.version }}

- name: Locate upload package
id: pkg
Expand Down
27 changes: 27 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project>

<!--
Fonte única da versão do produto.

Antes o número vivia solto no ClefExplorer.csproj e ficou em 1.0.0 enquanto a Store
já estava em 1.0.2 — o título da janela, que lê a versão do assembly, mostrava um
número diferente do publicado.

Quem consome:
- ClefExplorer.csproj → Version/FileVersion/AssemblyVersion (título da janela);
- publish-store-package.ps1 → padrão quando -Version não é informado;
- .github/workflows/release.yml → sobrescreve com a versão da tag (a tag manda no release).

A <Identity Version> do Package.appxmanifest NÃO é lida daqui: é XML carimbado em
tempo de empacotamento pelo script e pelo workflow, ambos a partir deste valor.
-->
<PropertyGroup>
<ClefVersion>1.1.0</ClefVersion>

<Version>$(ClefVersion)</Version>
<!-- A Store reserva a 4ª parte (revisão) e só aceita 0. -->
<FileVersion>$(ClefVersion).0</FileVersion>
<AssemblyVersion>$(ClefVersion).0</AssemblyVersion>
</PropertyGroup>

</Project>
10 changes: 5 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ O app está publicado na Store (product `9MVZN1HVJ230`, gratuito) e o CI está v

| # | Item | Tipo | Esforço | Status |
|---|---|---|---|---|
| 18 | Virtualização da lista (alternativa/complemento à paginação) | ⚡ Perf | 🟡 | |
| 18 | Virtualização da lista (alternativa/complemento à paginação) | ⚡ Perf | 🟡 | ✅ (+ páginas de até 1000) |
| 19 | Painel de estatísticas / timeline | ✨ Feature | 🔴 | ⬜ |
| 20 | Acessibilidade (teclado, ARIA, foco) | ♿ A11y | 🟡 | |
| 21 | Versionamento coerente + auto-update da versão standalone | 🧱 Débito | 🟡 | |
| 22 | Internacionalização (i18n) | ✨ Feature | 🔴 | ⬜ |
| 23 | Higiene geral do código e nomenclatura | 🧱 Débito | 🟢 | 🟨 parcial (avisos `CS*` zerados) |
| 20 | Acessibilidade (teclado, ARIA, foco) | ♿ A11y | 🟡 | ✅ (anel de foco não verificado na tela) |
| 21 | Versionamento coerente + auto-update da versão standalone | 🧱 Débito | 🟡 | 🟨 versão unificada; auto-update pendente |
| 22 | Internacionalização (i18n) | ✨ Feature | 🔴 | ⬜ (depende de decisão de alcance) |
| 23 | Higiene geral do código e nomenclatura | 🧱 Débito | 🟢 | |

---

Expand Down
14 changes: 13 additions & 1 deletion publish-store-package.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Version,
[string]$Version = '',

[string]$Platform = 'x64',
[string]$Configuration = 'Release',
Expand All @@ -60,6 +60,18 @@ foreach ($p in @($srcProj, $pkgProj, $manifest)) {
if (-not (Test-Path $p)) { throw "Não encontrei: $p (rode o script da raiz do repositório)." }
}

# ---------- Versão: parâmetro, ou a do Directory.Build.props ----------
if ([string]::IsNullOrWhiteSpace($Version)) {
$propsPath = Join-Path $root 'Directory.Build.props'
if (Test-Path $propsPath) {
$Version = ([xml](Get-Content $propsPath)).Project.PropertyGroup.ClefVersion
}
if ([string]::IsNullOrWhiteSpace($Version)) {
throw "Informe -Version ou defina <ClefVersion> em Directory.Build.props."
}
Write-Host "Versão herdada do Directory.Build.props: $Version" -ForegroundColor DarkGray
}

# ---------- Normaliza a versão para x.y.z.0 ----------
$v = $Version.Trim().TrimStart('v', 'V')
if ($v -notmatch '^\d+\.\d+(\.\d+){0,2}$') {
Expand Down
8 changes: 4 additions & 4 deletions src/ClefExplorer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<!-- Single-file publish defaults (can be overridden via CLI) -->
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
<!-- Só x64: é o que o CI publica, o que o pacote da Store contém e o que o README
anuncia como requisito. Declarar win-x86 sugeria um suporte que não existe. -->
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<InvariantGlobalization>true</InvariantGlobalization>
<ApplicationIcon>app.ico</ApplicationIcon>
Expand All @@ -24,9 +26,7 @@
<PackageProjectUrl>https://github.com/afernandes/ClefExplorer</PackageProjectUrl>
<RepositoryUrl>https://github.com/afernandes/ClefExplorer.git</RepositoryUrl>
<PackageTags>log;serilog;clef;viewer;blazor;winforms;structured-logging</PackageTags>
<Version>1.0.0</Version>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<!-- Version/FileVersion/AssemblyVersion vêm do Directory.Build.props (fonte única). -->
</PropertyGroup>

<ItemGroup>
Expand Down
177 changes: 177 additions & 0 deletions src/Components/LogGrid.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
@using ClefExplorer.Models
@using ClefExplorer.Helpers

@* Visão em tabela dos eventos: ordenar, agrupar (arrastando a coluna para a faixa
acima), filtrar por coluna e redimensionar.

Além das colunas fixas, as colunas de propriedade são DESCOBERTAS a partir do
conteúdo dos logs carregados — cada aplicação emite um conjunto próprio
(SourceContext, RequestId, MachineName…), então fixá-las na mão não funcionaria.

O seletor de colunas é NOSSO, e não o embutido (AllowColumnVisibility): a coluna
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. *@

<div class="clef-grid">
<OmniDataGrid TItem="ClefEvent"
Data="@Eventos"
Virtualize="true"
Height="100%"
AllowSorting="true"
AllowGrouping="true"
AllowColumnResize="true"
AllowColumnVisibility="false"
AllowColumnFilter="true"
HideGroupedColumn="true"
GroupPanelText="Arraste uma coluna aqui para agrupar"
EmptyText="Nenhum registro encontrado"
RowClass="@LinhaCss"
OnRowClick="OnSelect">
<ToolbarContent>
<OmniPopover Open="_colunasOpen" OpenChanged="@(v => _colunasOpen = v)" ShowArrow="false" Offset="4" Width="240px">
<Trigger>
<OmniButton Text="Colunas" Icon="sliders" Size="ComponentSize.Sm" Variant="ButtonVariant.Ghost" />
</Trigger>
<ChildContent>
<div class="clef-col-chooser">
@foreach (var col in TodasAsColunas)
{
var c = col;
<label class="clef-col-item">
<input type="checkbox"
checked="@VisibleColumns.Contains(c.Key)"
@onchange="@(e => Alternar(c.Key, (bool?)e.Value == true))" />
<span>@c.Title</span>
</label>
}
</div>
</ChildContent>
</OmniPopover>
</ToolbarContent>

<Columns>
<OmniDataGridColumn TItem="ClefEvent"
Title="Data/hora"
PropertyName="Timestamp"
Property="@(e => e.Timestamp ?? default)"
TextSelector="@(e => e.Timestamp?.ToString("dd/MM/yyyy HH:mm:ss.fff") ?? "")"
FilterType="ColumnFilterType.Date"
Filterable="true"
Visible="@VisibleColumns.Contains(LogGridColumns.Timestamp)"
Width="165px" />

<OmniDataGridColumn TItem="ClefEvent"
Title="Nível"
PropertyName="Level"
Property="@(e => e.Level ?? "")"
Groupable="true"
Filterable="true"
Visible="@VisibleColumns.Contains(LogGridColumns.Level)"
Width="120px">
<Template Context="ev">
<OmniBadge Variant="@LogLevelStyles.Variant(ev.Level)" Text="@ev.Level" Size="ComponentSize.Sm" />
</Template>
</OmniDataGridColumn>

<OmniDataGridColumn TItem="ClefEvent"
Title="Mensagem"
PropertyName="Message"
Property="@(e => e.Message ?? "")"
Filterable="true"
Visible="@VisibleColumns.Contains(LogGridColumns.Message)" />

@* Só o nome do arquivo na célula; o caminho completo fica no title. *@
<OmniDataGridColumn TItem="ClefEvent"
Title="Arquivo"
PropertyName="SourceFile"
Property="@(e => NomeArquivo(e))"
Groupable="true"
Filterable="true"
Visible="@VisibleColumns.Contains(LogGridColumns.SourceFile)"
Width="180px">
<Template Context="ev">
<span title="@ev.SourceFile">@NomeArquivo(ev)</span>
</Template>
</OmniDataGridColumn>

<OmniDataGridColumn TItem="ClefEvent"
Title="Exceção"
PropertyName="Exception"
Property="@(e => string.IsNullOrEmpty(e.Exception) ? "" : "sim")"
Groupable="true"
Visible="@VisibleColumns.Contains(LogGridColumns.Exception)"
Width="90px">
<Template Context="ev">
@if (!string.IsNullOrEmpty(ev.Exception))
{
<OmniIcon Name="zap" Size="ComponentSize.Sm" Class="clef-grid-exc" />
}
</Template>
</OmniDataGridColumn>

@* Colunas derivadas do conteúdo dos logs carregados. *@
@foreach (var coluna in Colunas)
{
var c = coluna;
<OmniDataGridColumn TItem="ClefEvent"
Title="@c.Title"
PropertyName="@c.Key"
Property="@(e => LogColumnDiscovery.FormatValue(e, c.Key))"
Groupable="true"
Filterable="true"
Visible="@VisibleColumns.Contains(c.Key)"
Width="160px" />
}
</Columns>
</OmniDataGrid>
</div>

@code {
[Parameter] public IReadOnlyList<ClefEvent> Eventos { get; set; } = Array.Empty<ClefEvent>();
[Parameter] public ClefEvent? SelectedEvent { get; set; }
[Parameter] public EventCallback<ClefEvent> OnSelect { get; set; }

/// <summary>Colunas descobertas no conteúdo dos logs carregados.</summary>
[Parameter] public IReadOnlyList<DiscoveredColumn> Colunas { get; set; } = Array.Empty<DiscoveredColumn>();

/// <summary>Chaves das colunas visíveis (fixas e descobertas).</summary>
[Parameter] public HashSet<string> VisibleColumns { get; set; } = new(StringComparer.OrdinalIgnoreCase);

[Parameter] public EventCallback<HashSet<string>> VisibleColumnsChanged { get; set; }

private bool _colunasOpen;

/// <summary>Fixas primeiro, depois as descobertas — a mesma ordem da tabela.</summary>
private IEnumerable<(string Key, string Title)> TodasAsColunas =>
LogGridColumns.Fixed.Concat(Colunas.Select(c => (c.Key, c.Title)));

private async Task Alternar(string key, bool visivel)
{
var novo = new HashSet<string>(VisibleColumns, StringComparer.OrdinalIgnoreCase);
if (visivel) novo.Add(key); else novo.Remove(key);

VisibleColumns = novo;
await VisibleColumnsChanged.InvokeAsync(novo);
}

private static string NomeArquivo(ClefEvent e) =>
string.IsNullOrEmpty(e.SourceFile) ? string.Empty : Path.GetFileName(e.SourceFile);

/// <summary>Destaca a linha selecionada e as de erro, como na lista.</summary>
private string LinhaCss(ClefEvent e)
{
var classes = new List<string>(2);

if (ReferenceEquals(e, SelectedEvent)) classes.Add("is-selected");

if (string.Equals(e.Level, "Error", StringComparison.OrdinalIgnoreCase)
|| string.Equals(e.Level, "Fatal", StringComparison.OrdinalIgnoreCase))
{
classes.Add("clef-grid-row-error");
}

return string.Join(' ', classes);
}
}
10 changes: 8 additions & 2 deletions src/Components/LogGroupManager.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
@foreach (var group in GroupService.Groups)
{
var g = group;
<div class="clef-group-item @(_editingGroup?.Id == g.Id ? "is-active" : "")" @key="g.Id" @onclick="@(() => EditGroup(g))">
@* <button> em vez de <div @onclick>: recebe foco, ativa por Enter/Espaço e é
anunciado como botão. Não há nada interativo aninhado, então a troca é segura. *@
<button type="button"
class="clef-group-item @(_editingGroup?.Id == g.Id ? "is-active" : "")"
@key="g.Id"
aria-pressed="@((_editingGroup?.Id == g.Id).ToString().ToLowerInvariant())"
@onclick="@(() => EditGroup(g))">
<span class="clef-mono" style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">@g.Name</span>
<OmniIcon Name="chevron-right" Size="ComponentSize.Sm" />
</div>
</button>
}
</div>

Expand Down
4 changes: 3 additions & 1 deletion src/Components/LogHeader.razor
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@

[Parameter] public EventCallback OnReload { get; set; }

private readonly int[] _qtdOptions = { 30, 50, 100 };
// 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";

Expand Down
Loading
Loading