Keep severity badge cells as table cells on both overviews

.counts is display: flex. On the <td> itself it made the cell a flex box
with its own height, so the row divider jumped in the findings column of
SQLInsight Web's overview and the hub portal's customer overview. The
class now sits on a <div> inside the cell; the CSS rule is unchanged.

TableMarkupTests checks Web, Hub and Community: no <td>/<th> may carry a
class whose rule sets display flex or grid.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Søren Eggert Lundsteen Olsen 2026-10-01 15:39:36 +02:00
parent e1d45e1734
commit 2c49a1e1b1
3 changed files with 61 additions and 4 deletions

View file

@ -28,11 +28,11 @@
<td>@(c.Plan ?? "–")</td>
<td class="nowrap @(c.LicenseExpires < Queries.Now.AddDays(30) ? "bad" : "")">@(c.LicenseExpires?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) ?? "–")</td>
<td class="num">@c.Instances</td>
<td class="counts">
<td><div class="counts">
@if (c.Critical > 0) { <Sev Severity="4" Count="c.Critical" /> }
@if (c.High > 0) { <Sev Severity="3" Count="c.High" /> }
@if (c.Critical == 0 && c.High == 0) { <span class="muted">ingen kritiske/høje</span> }
</td>
</div></td>
<td class="nowrap @(c.LastSubmission < Queries.Now.AddHours(-1) ? "bad" : "")">@(c.LastSubmission?.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) ?? "aldrig") UTC</td>
</tr>
}

View file

@ -45,12 +45,12 @@ else
</td>
<td><a href="/instanser/@Uri.EscapeDataString(i.Name)"><strong>@i.Name</strong></a><div class="muted">@i.ServerName</div></td>
<td>@(i.ProductVersion is null ? "–" : Display.Release(i.ProductVersion, i.EngineEdition))<div class="muted">@i.ProductVersion @i.ProductLevel @i.ProductUpdateLevel</div></td>
<td class="counts">
<td><div class="counts">
@if (i.Critical > 0) { <SeverityBadge Severity="Severity.Critical" Count="i.Critical" /> }
@if (i.High > 0) { <SeverityBadge Severity="Severity.High" Count="i.High" /> }
@if (i.Medium > 0) { <SeverityBadge Severity="Severity.Medium" Count="i.Medium" /> }
@if (i.Low > 0) { <SeverityBadge Severity="Severity.Low" Count="i.Low" /> }
</td>
</div></td>
<td>@i.NotMeasurable</td>
<td title="@Display.Time(i.LastSeenAt)">@Display.Ago(i.LastSeenAt)</td>
</tr>

View file

@ -0,0 +1,57 @@
using System.Text.RegularExpressions;
namespace SQLInsight.Community.Tests;
/// <summary>
/// A table cell must stay a table cell. A class that sets display: flex or grid turns a &lt;td&gt;/&lt;th&gt; into a flex box,
/// so it gets its own height and border and the row's divider jumps (the severity badges on the overviews did that).
/// Such classes belong on an element inside the cell. Checked for every site's stylesheet against its own pages.
/// </summary>
public sealed partial class TableMarkupTests
{
[Theory]
[InlineData("SQLInsight.Web")]
[InlineData("SQLInsight.Hub")]
[InlineData("SQLInsight.Community")]
public void No_table_cell_has_a_flex_or_grid_class(string project)
{
var folder = Path.Combine(RepositoryRoot(), "src", project);
var layoutClasses = Directory.EnumerateFiles(Path.Combine(folder, "wwwroot"), "*.css", SearchOption.AllDirectories)
.SelectMany(css => FlexOrGridRule().Matches(File.ReadAllText(css)))
.SelectMany(rule => ClassName().Matches(rule.Groups["selectors"].Value).Select(c => c.Groups[1].Value))
.ToHashSet(StringComparer.Ordinal);
var offenders = Directory.EnumerateFiles(folder, "*.razor", SearchOption.AllDirectories)
.Where(file => !file.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !file.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
.SelectMany(file => CellClass().Matches(File.ReadAllText(file))
.SelectMany(cell => cell.Groups["classes"].Value.Split(' ', StringSplitOptions.RemoveEmptyEntries))
.Where(layoutClasses.Contains)
.Select(c => $"{Path.GetFileName(file)}: <td/th class=\"{c}\">"))
.ToList();
Assert.True(offenders.Count == 0, "Flyt klassen ind på et element i cellen: " + string.Join(", ", offenders));
}
private static string RepositoryRoot()
{
var root = AppContext.BaseDirectory;
while (!File.Exists(Path.Combine(root, "SQLInsight.slnx")))
{
root = Path.GetDirectoryName(root) ?? throw new InvalidOperationException("SQLInsight.slnx ikke fundet.");
}
return root;
}
// A rule whose declarations set display to (inline-)flex or (inline-)grid; media queries are fine, their inner rules match.
[GeneratedRegex(@"(?<selectors>[^{}]+)\{[^{}]*\bdisplay\s*:\s*(inline-)?(flex|grid)\b")]
private static partial Regex FlexOrGridRule();
[GeneratedRegex(@"\.([A-Za-z][\w-]*)")]
private static partial Regex ClassName();
// Literal class names only; Razor expressions (@…) in the attribute are skipped by splitting on spaces.
[GeneratedRegex(@"<t[dh]\b[^>]*\bclass=""(?<classes>[^""]*)""")]
private static partial Regex CellClass();
}