From 1e925d9e1a0eba5dbc96e779917796cdd5f132d7 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 19 Aug 2026 18:02:51 +0200 Subject: [PATCH 1/5] replaced rules report table with fanxy table --- roles/ui/files/FWO.UI/FWO.Ui.csproj | 1 + .../Pages/Reporting/Reports/RulesReport.razor | 638 ++++++++++-------- .../Reporting/Reports/RulesReport.razor.css | 9 - roles/ui/files/FWO.UI/_Imports.razor | 3 +- roles/ui/files/FWO.UI/wwwroot/css/site.css | 8 + 5 files changed, 363 insertions(+), 296 deletions(-) delete mode 100644 roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor.css diff --git a/roles/ui/files/FWO.UI/FWO.Ui.csproj b/roles/ui/files/FWO.UI/FWO.Ui.csproj index bad7210cea..ded2cece78 100644 --- a/roles/ui/files/FWO.UI/FWO.Ui.csproj +++ b/roles/ui/files/FWO.UI/FWO.Ui.csproj @@ -36,5 +36,6 @@ + diff --git a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor index 9c3d6e9f55..df6c5c89b9 100644 --- a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor +++ b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor @@ -2,7 +2,6 @@ @using FWO.Report.Data @using FWO.Ui.Display @using FWO.Services.RuleTreeBuilder - @inject UserConfig userConfig @inject IRuleTreeBuilder ruleTreeBuilder @@ -29,226 +28,18 @@ { @foreach (var device in management.Devices) { - @if (device.ContainsRules() && _ruleDisplay != null) + @if (device.ContainsRules()) { - -
- - -
- - - - @if (_emptyColumns[0] == false) - { - - - - } - - @if (SelectedReportType == ReportType.UnusedRules || SelectedReportType == ReportType.AppRules) - { - - - - @if (_showLastModifiedAfterLastHit && _emptyColumns[13] == false) - { - - - - } - } - - @if (SelectedReportType.IsComplianceReport()) - { - - - - - - - } - - @if (_emptyColumns[1] == false) - { - - } - - @if (_emptyColumns[2] == false) - { - - - - } - - @if (_emptyColumns[3] == false) - { - - - - } - - @if (_emptyColumns[4] == false) - { - - - - } - - @if (_emptyColumns[5] == false) - { - - - - } - - @if (_emptyColumns[6] == false) - { - - - - } - - @if (SelectedReportType == ReportType.NatRules) - { - - - - - - - - - - } - - @if (_emptyColumns[7] == false && SelectedReportType != ReportType.NatRules) - { - - } - - @if (_emptyColumns[8] == false && SelectedReportType != ReportType.NatRules) - { - - } - - @if (_emptyColumns[9] == false) - { - - - - } - - @if (_emptyColumns[10] == false) - { - - - - } - - @if (_emptyColumns[11] == false) - { - - } - - @if (_emptyColumns[12] == false) - { - - } - - @if (_emptyColumns[13] == false) - { - @if (!_showLastModifiedAfterLastHit) - { - - - - } - } - - @if (_emptyColumns[14] == false && ShowLastHitColumn) - { - - - - } - - - - - - - - - - - - - - -
-
- @((MarkupString)GetToggleArrow(context.Data!, management, device))@((MarkupString)RuleDisplayHtml.DisplayHierarchicalNumber(context.Data!)) -
- -
-
@(context.Data!.SectionHeader)
-
+
} } @@ -259,6 +50,36 @@ @code { + private RenderFragment> RuleTreeActionsTemplate => context => + @
+ + +
; + + private RenderFragment> SectionHeaderTemplate => context => @ + + + @context.Item.SectionHeaderTitle + (@CountRealRuleDescendants(context.Item) Regeln) + + ; + #region Parameters [Parameter] @@ -292,20 +113,27 @@ #region Consts - Private - private const int ColumnsCount = 15; + private const string LastModifiedTitle = "LastModified"; + private const string ComplianceTitle = "Compliance"; + private const string ViolationDetailsTitle = "Violation Details"; #endregion #region Fields - private ITable? _reportTable; - private readonly bool[] _emptyColumns = new bool[ColumnsCount]; - private NatRuleDisplayHtml? _ruleDisplay; - private bool _showLastModifiedAfterLastHit => SelectedReportType == ReportType.UnusedRules || SelectedReportType == ReportType.AppRules; - private bool ShowLastHitColumn => ReportTypeGroups.IsRuleReport(SelectedReportType); + private IReadOnlyList> _ruleColumns = []; + private readonly Dictionary<(int managementId, int deviceId), IReadOnlyList> _ruleRowsByDevice = []; + private ReportType _cachedReportType = ReportType.Undefined; + private int _cachedManagementsHash; - private Rule? _lastClickedRule = null; + #endregion + + #region Properties - Private + + private IReadOnlyList> RuleColumns => _ruleColumns; + private bool ShowLastModifiedAfterLastHit => SelectedReportType == ReportType.UnusedRules || SelectedReportType == ReportType.AppRules; + private bool ShowLastHitColumn => ReportTypeGroups.IsRuleReport(SelectedReportType); #endregion @@ -314,119 +142,357 @@ protected override void OnInitialized() { _ruleDisplay = new NatRuleDisplayHtml(userConfig); - // TODO: are we using NatRuleDisplayHtml for non-NAT rules? + RefreshColumns(); + } + + protected override void OnParametersSet() + { + if (_cachedReportType != SelectedReportType) + { + RefreshColumns(); + } + + int managementsHash = CalculateManagementsHash(); + if (_cachedManagementsHash != managementsHash) + { + RefreshRuleRows(); + } } #endregion #region Methods - Private - private string GetTableRowClasses(Rule rule) + private IReadOnlyList> BuildRuleColumns() { - string classes = ""; - if (rule.SectionHeader != null) + List> columns = + [ + CreateTextColumn( + "number", + userConfig.GetText("number"), + row => row.Rule.DisplayOrderNumberString, + row => row.Rule.DisplayOrderNumberString), + ]; + + if (SelectedReportType == ReportType.UnusedRules || SelectedReportType == ReportType.AppRules) { - classes = "hide-all-but-second-child second-child-full-width "; + columns.Add(CreateMarkupColumn( + "last_hit_pre", + userConfig.GetText("last_hit"), + row => RuleDisplayHtml.DisplayLastHit(row.Rule.Metadata), + row => row.Rule.Metadata.LastHit?.ToString("O") ?? string.Empty)); + + if (ShowLastModifiedAfterLastHit) + { + columns.Add(CreateMarkupColumn( + "last_modified_pre", + LastModifiedTitle, + row => RuleDisplayBase.DisplayLastModified(row.Rule), + row => row.Rule.LastModified?.ToString("O") ?? string.Empty)); + } } - return classes; + + if (SelectedReportType.IsComplianceReport()) + { + columns.Add(CreateMarkupColumn( + "compliance", + ComplianceTitle, + row => RuleDisplayHtml.DisplayIsCompliant(row.Rule, GetActLocation()), + row => row.Rule.Compliance.ToString())); + + columns.Add(CreateMarkupColumn( + "violation_details", + ViolationDetailsTitle, + row => RuleDisplayHtml.DisplayViolationDetails(row.Rule), + row => row.Rule.ViolationDetails)); + } + + columns.Add(CreateTextColumn( + "name", + userConfig.GetText("name"), + row => row.DisplayName, + row => row.DisplayName)); + + columns.Add(CreateMarkupColumn( + "source_zone", + userConfig.GetText("source_zone"), + row => RuleDisplayHtml.DisplaySourceZones(row.Rule), + row => string.Join(", ", row.Rule.RuleFromZones.Select(zone => zone.Content.Name)))); + + columns.Add(CreateMarkupColumn( + "source", + userConfig.GetText("source"), + row => _ruleDisplay?.DisplaySource(row.Rule, GetActLocation(), SelectedReportType) ?? string.Empty, + row => row.Rule.Source)); + + columns.Add(CreateMarkupColumn( + "destination_zone", + userConfig.GetText("destination_zone"), + row => RuleDisplayHtml.DisplayDestinationZones(row.Rule), + row => string.Join(", ", row.Rule.RuleToZones.Select(zone => zone.Content.Name)))); + + columns.Add(CreateMarkupColumn( + "destination", + userConfig.GetText("destination"), + row => _ruleDisplay?.DisplayDestination(row.Rule, GetActLocation(), SelectedReportType) ?? string.Empty, + row => row.Rule.Destination)); + + columns.Add(CreateMarkupColumn( + "services", + userConfig.GetText("services"), + row => _ruleDisplay?.DisplayServices(row.Rule, GetActLocation(), SelectedReportType) ?? string.Empty, + row => row.Rule.Service)); + + if (SelectedReportType == ReportType.NatRules) + { + columns.Add(CreateMarkupColumn( + "trans_source", + userConfig.GetText("trans_source"), + row => _ruleDisplay?.DisplayTranslatedSource(row.Rule, GetActLocation()) ?? string.Empty, + row => row.Rule.NatData.TranslatedSource ?? string.Empty, + "table-active")); + + columns.Add(CreateMarkupColumn( + "trans_destination", + userConfig.GetText("trans_destination"), + row => _ruleDisplay?.DisplayTranslatedDestination(row.Rule, GetActLocation()) ?? string.Empty, + row => row.Rule.NatData.TranslatedDestination ?? string.Empty, + "table-active")); + + columns.Add(CreateMarkupColumn( + "trans_services", + userConfig.GetText("trans_services"), + row => _ruleDisplay?.DisplayTranslatedService(row.Rule, GetActLocation()) ?? string.Empty, + row => row.Rule.NatData.TranslatedService ?? string.Empty, + "table-active")); + } + else + { + columns.Add(CreateTextColumn( + "action", + userConfig.GetText("action"), + row => row.Rule.Action, + row => row.Rule.Action)); + + columns.Add(CreateTextColumn( + "track", + userConfig.GetText("track"), + row => row.Rule.Track, + row => row.Rule.Track)); + } + + columns.Add(CreateMarkupColumn( + "enabled", + userConfig.GetText("enabled"), + row => RuleDisplayHtml.DisplayEnabled(row.Rule, GetActLocation()), + row => row.Rule.Disabled.ToString())); + + columns.Add(CreateMarkupColumn( + "enforcing_devices", + userConfig.GetText("enforcing_devices"), + row => RuleDisplayHtml.DisplayEnforcingGateways(row.Rule, GetActLocation(), SelectedReportType), + row => string.Join(", ", row.Rule.EnforcingGateways.Select(device => device.Content.Name)))); + + columns.Add(CreateTextColumn( + "uid", + userConfig.GetText("uid"), + row => row.Rule.Uid ?? string.Empty, + row => row.Rule.Uid ?? string.Empty, + "minw-200", + hideable: true)); + + columns.Add(CreateTextColumn( + "comment", + userConfig.GetText("comment"), + row => row.Rule.Comment ?? string.Empty, + row => row.Rule.Comment ?? string.Empty, + hideable: true)); + + if (!ShowLastModifiedAfterLastHit) + { + columns.Add(CreateMarkupColumn( + "last_modified", + LastModifiedTitle, + row => RuleDisplayBase.DisplayLastModified(row.Rule), + row => row.Rule.LastModified?.ToString("O") ?? string.Empty, + hideable: true)); + } + + if (ShowLastHitColumn) + { + columns.Add(CreateMarkupColumn( + "last_hit", + userConfig.GetText("last_hit"), + row => RuleDisplayHtml.DisplayLastHit(row.Rule.Metadata), + row => row.Rule.Metadata.LastHit?.ToString("O") ?? string.Empty, + "minw-80")); + } + + return columns; } - private OutputLocation GetActLocation() + private FancyColumn CreateTextColumn( + string key, + string title, + Func valueSelector, + Func sortValueSelector, + string? cellClass = null, + bool hideable = false) { - return Recertification ? OutputLocation.certification : OutputLocation.report; + return new FancyColumn + { + Key = key, + Title = title, + Sortable = true, + Searchable = true, + Hideable = hideable, + CellClass = cellClass, + ValueSelector = valueSelector, + SortValueSelector = row => sortValueSelector(row) + }; } - private IEnumerable GetAllInlineRecursive(IEnumerable rules, Dictionary> dictRuleInlines) + private FancyColumn CreateMarkupColumn( + string key, + string title, + Func markupSelector, + Func sortValueSelector, + string? cellClass = null, + bool hideable = false) { - foreach (var r in rules) + return new FancyColumn { - yield return r; - - if (dictRuleInlines.TryGetValue(r.DisplayOrderNumberString, out var children) && children.Count > 0) - { - foreach (var c in GetAllInlineRecursive(children, dictRuleInlines)) - yield return c; - } - } + Key = key, + Title = title, + Sortable = true, + Searchable = true, + Hideable = hideable, + CellClass = cellClass, + ValueSelector = sortValueSelector, + SearchTextSelector = row => sortValueSelector(row), + SortValueSelector = row => sortValueSelector(row), + CellTemplate = CreateMarkupCell(markupSelector) + }; } - private RuleTreeItem[] GetRuleTreeItems(ManagementReport managementReport, DeviceReport deviceReport) + private static RenderFragment CreateMarkupCell(Func markupSelector) { - if (!ruleTreeBuilder.RuleTreeCache.TryGetValue((managementReport.Id, deviceReport.Id), out RuleTreeItem? ruleTree)) - { - // No tree built (yet) for this device - render an empty table instead of failing the whole report. - return []; - } - return ruleTree.ElementsFlat - .Where(ruleTreeItem => ruleTreeItem.IsVisible) - .ToArray(); + return row => builder => builder.AddMarkupContent(0, markupSelector(row)); } - private void ToggleAll(ManagementReport management, DeviceReport device, bool collapse) + private OutputLocation GetActLocation() { - RuleTreeItem.SetExpandedRecursively(GetRuleTree(management, device), !collapse); + return Recertification ? OutputLocation.certification : OutputLocation.report; } - private string GetToggleArrow(Rule rule, ManagementReport managementReport, DeviceReport deviceReport) + private void RefreshColumns() { - // Get corresponding tree item for rule + _ruleColumns = BuildRuleColumns(); + _cachedReportType = SelectedReportType; + } - RuleTreeItem ruleTree = GetRuleTree(managementReport, deviceReport); - RuleTreeItem? treeItem = ruleTree.ItemsByRule.GetValueOrDefault(rule); + private void RefreshRuleRows() + { + _ruleRowsByDevice.Clear(); - if (treeItem == null) + foreach (ManagementReport management in Managements) { - throw new Exception("No tree item found for rule"); + foreach (DeviceReport device in management.Devices) + { + _ruleRowsByDevice[(management.Id, device.Id)] = BuildRuleRows(management, device); + } } - // Print hidden span for rows (rules or dummy rules for rulebase headers) which are not expandable + _cachedManagementsHash = CalculateManagementsHash(); + } + + private int CalculateManagementsHash() + { + HashCode hash = new(); + hash.Add(Managements.Count); - if (!treeItem.Children.Any()) + foreach (ManagementReport management in Managements) { - return $"{Icons.HtmlArrowCollapsed}"; + hash.Add(management.Id); + hash.Add(management.Rulebases.Length); + hash.Add(management.Devices.Length); + + foreach (DeviceReport device in management.Devices) + { + hash.Add(device.Id); + hash.Add(device.GetNumberOfRules()); + } } - // Show correct arrow + return hash.ToHashCode(); + } - if (treeItem.IsExpanded) - { - return Icons.HtmlArrowExpanded; - } - else + private IReadOnlyList GetRuleRows(ManagementReport managementReport, DeviceReport deviceReport) + { + return _ruleRowsByDevice.GetValueOrDefault((managementReport.Id, deviceReport.Id), []); + } + + private IReadOnlyList BuildRuleRows(ManagementReport managementReport, DeviceReport deviceReport) + { + if (!ruleTreeBuilder.RuleTreeCache.TryGetValue((managementReport.Id, deviceReport.Id), out RuleTreeItem? ruleTree)) { - return Icons.HtmlArrowCollapsed; + return []; } + + return MapTreeNodes(ruleTree.Children, deviceReport.Name); } - private void OnNumberIconClickToggle(Rule rule, ManagementReport managementReport, DeviceReport deviceReport) + private IReadOnlyList MapTreeNodes(IEnumerable nodes, string? deviceName) { - // Capture clicked rule + List rows = []; + + foreach (RuleTreeItem node in nodes) + { + if (node.IsInlineLayerRoot) + { + rows.AddRange(MapTreeNodes(node.Children, deviceName)); + continue; + } + + if (node.Data == null) + { + continue; + } - _lastClickedRule = rule; + node.Data.DeviceName = deviceName ?? string.Empty; + rows.Add(new RuleReportTreeRow(node, MapTreeNodes(node.Children, deviceName))); + } - // Get corresponding tree item for rule + return rows; + } - RuleTreeItem ruleTree = GetRuleTree(managementReport, deviceReport); - RuleTreeItem? treeItem = ruleTree.ItemsByRule.GetValueOrDefault(rule); + private static int CountRealRuleDescendants(RuleReportTreeRow row) + { + int count = row.IsSectionHeader ? 0 : 1; - if (treeItem != null) + foreach (RuleReportTreeRow child in row.Children) { - // Update exapandable state - - treeItem.IsExpanded = !treeItem.IsExpanded; + count += CountRealRuleDescendants(child); } - InvokeAsync(StateHasChanged); + return count; } - private RuleTreeItem GetRuleTree(ManagementReport managementReport, DeviceReport deviceReport) + private sealed class RuleReportTreeRow { - if (ruleTreeBuilder.RuleTreeCache.TryGetValue((managementReport.Id, deviceReport.Id), out RuleTreeItem? ruleTree)) + public RuleReportTreeRow(RuleTreeItem node, IReadOnlyList children) { - return ruleTree; + Node = node; + Rule = node.Data; + Children = children; } - throw new InvalidOperationException("No rule tree found for management and device."); + public RuleTreeItem Node { get; } + public Rule Rule { get; } + public IReadOnlyList Children { get; } + public bool IsSectionHeader => Node.IsSectionHeader; + public string DisplayName => IsSectionHeader ? string.Empty : Rule.Name ?? string.Empty; + public string SectionHeaderTitle => Rule.SectionHeader ?? string.Empty; } #endregion diff --git a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor.css b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor.css deleted file mode 100644 index 8436db7e16..0000000000 --- a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor.css +++ /dev/null @@ -1,9 +0,0 @@ -.sticky-collapse { - padding-left: 0.5rem; - padding-bottom: 0.5rem; - position: sticky; - top: 6rem; - z-index: 14; -} - - diff --git a/roles/ui/files/FWO.UI/_Imports.razor b/roles/ui/files/FWO.UI/_Imports.razor index d44a7133da..ee2bcc0434 100644 --- a/roles/ui/files/FWO.UI/_Imports.razor +++ b/roles/ui/files/FWO.UI/_Imports.razor @@ -1,4 +1,4 @@ -@using System.Net.Http +@using System.Net.Http @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @@ -15,3 +15,4 @@ @using FWO.Data.Workflow @using FWO.Services.Modelling @using FWO.Services.Workflow +@using SoPro.FancyTable.Components diff --git a/roles/ui/files/FWO.UI/wwwroot/css/site.css b/roles/ui/files/FWO.UI/wwwroot/css/site.css index 8960615db8..aa5f66c2d9 100644 --- a/roles/ui/files/FWO.UI/wwwroot/css/site.css +++ b/roles/ui/files/FWO.UI/wwwroot/css/site.css @@ -619,3 +619,11 @@ div.modal-content .sticky-header thead { font-weight: 600; line-height: 1.2; } + +.sticky-collapse { + padding-left: 0.5rem; + padding-bottom: 0.5rem; + position: sticky; + top: 6rem; + z-index: 14; +} From 94ebe94bfb3054b46d5d15a7c1eae6ede3fd81c4 Mon Sep 17 00:00:00 2001 From: Luca Date: Wed, 19 Aug 2026 18:34:38 +0200 Subject: [PATCH 2/5] . --- .../files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor index df6c5c89b9..0968f7bd5d 100644 --- a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor +++ b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor @@ -39,7 +39,11 @@ TreeActionsContainerClass="sticky-collapse" TreeActionsTemplate="RuleTreeActionsTemplate" RowTemplate="SectionHeaderTemplate" - RowTemplateSelector="context => context.Item.IsSectionHeader" /> + RowTemplateSelector="context => context.Item.IsSectionHeader" + ShowPaginationSummary="true" + PageSize="5" + PaginationEnabled="true" + ShowPageSizeSelector="true"/> } } From 222843347aa08f097e3a2cd5915bd6d7d2e2953d Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 21 Aug 2026 15:11:06 +0200 Subject: [PATCH 3/5] add localization and initial expanded/collapsed state for rule report table --- roles/FWO.sln | 8 ++++- roles/lib/files/FWO.Config.Api/UserConfig.cs | 19 +++++++++++ .../files/FWO.Test/UiSettingsLdapTest.cs | 8 ++--- roles/ui/files/FWO.UI/FWO.Ui.csproj | 2 +- .../Pages/Reporting/Reports/RulesReport.razor | 9 ++--- .../FWO.UI/Services/FancyTableTextFactory.cs | 34 +++++++++++++++++++ 6 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 roles/ui/files/FWO.UI/Services/FancyTableTextFactory.cs diff --git a/roles/FWO.sln b/roles/FWO.sln index 28220348ad..021b41bbb1 100644 --- a/roles/FWO.sln +++ b/roles/FWO.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.1.11312.151 d18.0 +VisualStudioVersion = 18.1.11312.151 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{CE55F125-0CD2-4789-A3C1-045DEF33ABA5}" EndProject @@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FWO.Compliance", "lib\files EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FWO.ExternalSystems", "lib\files\FWO.ExternalSystems\FWO.ExternalSystems.csproj", "{4AEFC459-C0F3-4091-AA84-FDCE3F03F347}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoPro.FancyTable", "..\..\SoPro.FancyTable\SoPro.FancyTable.csproj", "{0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -130,6 +132,10 @@ Global {4AEFC459-C0F3-4091-AA84-FDCE3F03F347}.Debug|Any CPU.Build.0 = Debug|Any CPU {4AEFC459-C0F3-4091-AA84-FDCE3F03F347}.Release|Any CPU.ActiveCfg = Release|Any CPU {4AEFC459-C0F3-4091-AA84-FDCE3F03F347}.Release|Any CPU.Build.0 = Release|Any CPU + {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/roles/lib/files/FWO.Config.Api/UserConfig.cs b/roles/lib/files/FWO.Config.Api/UserConfig.cs index 9bceb6f1f8..e65e2419ea 100644 --- a/roles/lib/files/FWO.Config.Api/UserConfig.cs +++ b/roles/lib/files/FWO.Config.Api/UserConfig.cs @@ -6,6 +6,7 @@ using FWO.Data; using FWO.Api.Client.Queries; using System.Reflection; +using System.Globalization; using System.Text.Json.Serialization; namespace FWO.Config.Api @@ -229,6 +230,24 @@ public override string GetText(string key) } } + public CultureInfo GetCultureInfo() + { + ThrowIfDisposed(); + + string currentLanguage = GetUserLanguage(); + string? cultureName = GlobalConfig?.UiLanguages + .FirstOrDefault(language => language.Name == currentLanguage)?.CultureInfo; + + if (!string.IsNullOrWhiteSpace(cultureName)) + { + return CultureInfo.GetCultureInfo(cultureName); + } + + return currentLanguage == GlobalConst.kEnglish + ? CultureInfo.GetCultureInfo("en") + : CultureInfo.GetCultureInfo("de"); + } + public string PureLine(string text) { ThrowIfDisposed(); diff --git a/roles/tests-unit/files/FWO.Test/UiSettingsLdapTest.cs b/roles/tests-unit/files/FWO.Test/UiSettingsLdapTest.cs index 4d5cd854f8..2438bbb3d0 100644 --- a/roles/tests-unit/files/FWO.Test/UiSettingsLdapTest.cs +++ b/roles/tests-unit/files/FWO.Test/UiSettingsLdapTest.cs @@ -244,10 +244,10 @@ public async Task TestConnection_ShowsExpectedMessagesForResponseCodes() await InvokePrivateTask(component, "TestConnection"); - Assert.That(messages, Has.Count.EqualTo(1)); - Assert.That(messages[0].Title, Is.EqualTo(GetMember(component, "userConfig").GetText("test_connection"))); - Assert.That(messages[0].Message, Is.EqualTo(testCase.ExpectedMessage)); - messages.Clear(); + Assert.That(messages, Has.Count.EqualTo(1)); + Assert.That(messages[0].Title, Is.EqualTo(GetMember(component, "userConfig").GetText("test_connection"))); + Assert.That(messages[0].Message, Is.EqualTo(testCase.ExpectedMessage)); + messages.Clear(); } } diff --git a/roles/ui/files/FWO.UI/FWO.Ui.csproj b/roles/ui/files/FWO.UI/FWO.Ui.csproj index 3db818d7c2..cd19f164bd 100644 --- a/roles/ui/files/FWO.UI/FWO.Ui.csproj +++ b/roles/ui/files/FWO.UI/FWO.Ui.csproj @@ -21,6 +21,7 @@ + @@ -36,6 +37,5 @@ - diff --git a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor index 0968f7bd5d..759c830e8a 100644 --- a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor +++ b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor @@ -36,14 +36,13 @@ Columns="RuleColumns" ChildItemsSelector="row => row.Children" HasChildrenSelector="row => row.Children.Count > 0" + InitiallyExpandedSelector="row => row.IsInitiallyExpanded" + Texts="RuleTableTexts" TreeActionsContainerClass="sticky-collapse" TreeActionsTemplate="RuleTreeActionsTemplate" RowTemplate="SectionHeaderTemplate" RowTemplateSelector="context => context.Item.IsSectionHeader" - ShowPaginationSummary="true" - PageSize="5" - PaginationEnabled="true" - ShowPageSizeSelector="true"/> + /> } } @@ -136,6 +135,7 @@ #region Properties - Private private IReadOnlyList> RuleColumns => _ruleColumns; + private FancyTableTexts RuleTableTexts => FancyTableTextFactory.Create(userConfig); private bool ShowLastModifiedAfterLastHit => SelectedReportType == ReportType.UnusedRules || SelectedReportType == ReportType.AppRules; private bool ShowLastHitColumn => ReportTypeGroups.IsRuleReport(SelectedReportType); @@ -494,6 +494,7 @@ public RuleTreeItem Node { get; } public Rule Rule { get; } public IReadOnlyList Children { get; } + public bool IsInitiallyExpanded => Node.IsExpanded; public bool IsSectionHeader => Node.IsSectionHeader; public string DisplayName => IsSectionHeader ? string.Empty : Rule.Name ?? string.Empty; public string SectionHeaderTitle => Rule.SectionHeader ?? string.Empty; diff --git a/roles/ui/files/FWO.UI/Services/FancyTableTextFactory.cs b/roles/ui/files/FWO.UI/Services/FancyTableTextFactory.cs new file mode 100644 index 0000000000..de3245b887 --- /dev/null +++ b/roles/ui/files/FWO.UI/Services/FancyTableTextFactory.cs @@ -0,0 +1,34 @@ +using FWO.Config.Api; +using SoPro.FancyTable.Components; + +namespace FWO.Ui.Services +{ + /// + /// Creates centralized localized text sets for FancyTable-based components. + /// + public static class FancyTableTextFactory + { + public static FancyTableTexts Create(UserConfig userConfig) + { + return new FancyTableTexts + { + SearchPlaceholder = userConfig.GetText("search"), + RowsPerPageLabel = userConfig.GetText("rows_per_page"), + AllItemsLabel = userConfig.GetText("all"), + HiddenColumnsLabel = userConfig.GetText("hidden_columns"), + PaginationAriaLabel = userConfig.GetText("pagination"), + PreviousPageLabel = userConfig.GetText("previous"), + NextPageLabel = userConfig.GetText("next"), + NoRowsLabel = userConfig.GetText("no_rows"), + NoMatchingRowsFormat = userConfig.GetText("no_matching_rows"), + NoMatchingRowsWithTotalFormat = userConfig.GetText("no_matching_rows_with_total"), + ShowingItemsFormat = userConfig.GetText("showing_items_format"), + ShowingFilteredItemsFormat = userConfig.GetText("showing_filtered_items_format"), + ExpandLabel = userConfig.GetText("expand"), + CollapseLabel = userConfig.GetText("collapse"), + NoMatchingRootItemsFormat = userConfig.GetText("no_matching_root_items_format"), + ShowingVisibleRowsFormat = userConfig.GetText("showing_visible_rows_format") + }; + } + } +} From 5fd12e47b27f3065347666141fc56891661c9b97 Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 27 Aug 2026 09:48:48 +0200 Subject: [PATCH 4/5] switch to fancytable nuget package --- roles/FWO.sln | 6 ------ roles/ui/files/FWO.UI/FWO.Ui.csproj | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/roles/FWO.sln b/roles/FWO.sln index 021b41bbb1..8dda236564 100644 --- a/roles/FWO.sln +++ b/roles/FWO.sln @@ -48,8 +48,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FWO.Compliance", "lib\files EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FWO.ExternalSystems", "lib\files\FWO.ExternalSystems\FWO.ExternalSystems.csproj", "{4AEFC459-C0F3-4091-AA84-FDCE3F03F347}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoPro.FancyTable", "..\..\SoPro.FancyTable\SoPro.FancyTable.csproj", "{0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -132,10 +130,6 @@ Global {4AEFC459-C0F3-4091-AA84-FDCE3F03F347}.Debug|Any CPU.Build.0 = Debug|Any CPU {4AEFC459-C0F3-4091-AA84-FDCE3F03F347}.Release|Any CPU.ActiveCfg = Release|Any CPU {4AEFC459-C0F3-4091-AA84-FDCE3F03F347}.Release|Any CPU.Build.0 = Release|Any CPU - {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DF05B8C-67A0-4B5F-75E6-280F2E4EAF44}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/roles/ui/files/FWO.UI/FWO.Ui.csproj b/roles/ui/files/FWO.UI/FWO.Ui.csproj index cd19f164bd..68bfb7b1dc 100644 --- a/roles/ui/files/FWO.UI/FWO.Ui.csproj +++ b/roles/ui/files/FWO.UI/FWO.Ui.csproj @@ -18,10 +18,10 @@ + - From c2ce5cbba4a139aa928f705c6650a53e9a129222 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 21 Sep 2026 18:07:54 +0200 Subject: [PATCH 5/5] chore(deps): update FancyTable --- roles/ui/files/FWO.UI/FWO.Ui.csproj | 2 +- .../Pages/Reporting/Reports/RulesReport.razor | 68 +++++++++++++------ 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/roles/ui/files/FWO.UI/FWO.Ui.csproj b/roles/ui/files/FWO.UI/FWO.Ui.csproj index bf1a1d4be8..69c387b453 100644 --- a/roles/ui/files/FWO.UI/FWO.Ui.csproj +++ b/roles/ui/files/FWO.UI/FWO.Ui.csproj @@ -18,7 +18,7 @@ - + diff --git a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor index 759c830e8a..04131cbc74 100644 --- a/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor +++ b/roles/ui/files/FWO.UI/Pages/Reporting/Reports/RulesReport.razor @@ -42,7 +42,8 @@ TreeActionsTemplate="RuleTreeActionsTemplate" RowTemplate="SectionHeaderTemplate" RowTemplateSelector="context => context.Item.IsSectionHeader" - /> + EnableColumnReordering="true" + EnableColumnResizing="true" /> } } @@ -215,37 +216,43 @@ "name", userConfig.GetText("name"), row => row.DisplayName, - row => row.DisplayName)); + row => row.DisplayName, + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "source_zone", userConfig.GetText("source_zone"), row => RuleDisplayHtml.DisplaySourceZones(row.Rule), - row => string.Join(", ", row.Rule.RuleFromZones.Select(zone => zone.Content.Name)))); + row => string.Join(", ", row.Rule.RuleFromZones.Select(zone => zone.Content.Name)), + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "source", userConfig.GetText("source"), row => _ruleDisplay?.DisplaySource(row.Rule, GetActLocation(), SelectedReportType) ?? string.Empty, - row => row.Rule.Source)); + row => row.Rule.Source, + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "destination_zone", userConfig.GetText("destination_zone"), row => RuleDisplayHtml.DisplayDestinationZones(row.Rule), - row => string.Join(", ", row.Rule.RuleToZones.Select(zone => zone.Content.Name)))); + row => string.Join(", ", row.Rule.RuleToZones.Select(zone => zone.Content.Name)), + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "destination", userConfig.GetText("destination"), row => _ruleDisplay?.DisplayDestination(row.Rule, GetActLocation(), SelectedReportType) ?? string.Empty, - row => row.Rule.Destination)); + row => row.Rule.Destination, + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "services", userConfig.GetText("services"), row => _ruleDisplay?.DisplayServices(row.Rule, GetActLocation(), SelectedReportType) ?? string.Empty, - row => row.Rule.Service)); + row => row.Rule.Service, + hideable: true, enableResizing: true, enableReordering: true)); if (SelectedReportType == ReportType.NatRules) { @@ -254,21 +261,24 @@ userConfig.GetText("trans_source"), row => _ruleDisplay?.DisplayTranslatedSource(row.Rule, GetActLocation()) ?? string.Empty, row => row.Rule.NatData.TranslatedSource ?? string.Empty, - "table-active")); + "table-active", + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "trans_destination", userConfig.GetText("trans_destination"), row => _ruleDisplay?.DisplayTranslatedDestination(row.Rule, GetActLocation()) ?? string.Empty, row => row.Rule.NatData.TranslatedDestination ?? string.Empty, - "table-active")); + "table-active", + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "trans_services", userConfig.GetText("trans_services"), row => _ruleDisplay?.DisplayTranslatedService(row.Rule, GetActLocation()) ?? string.Empty, row => row.Rule.NatData.TranslatedService ?? string.Empty, - "table-active")); + "table-active", + hideable: true, enableResizing: true, enableReordering: true)); } else { @@ -276,26 +286,30 @@ "action", userConfig.GetText("action"), row => row.Rule.Action, - row => row.Rule.Action)); + row => row.Rule.Action, + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateTextColumn( "track", userConfig.GetText("track"), row => row.Rule.Track, - row => row.Rule.Track)); + row => row.Rule.Track, + hideable: true, enableResizing: true, enableReordering: true)); } columns.Add(CreateMarkupColumn( "enabled", userConfig.GetText("enabled"), row => RuleDisplayHtml.DisplayEnabled(row.Rule, GetActLocation()), - row => row.Rule.Disabled.ToString())); + row => row.Rule.Disabled.ToString(), + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateMarkupColumn( "enforcing_devices", userConfig.GetText("enforcing_devices"), row => RuleDisplayHtml.DisplayEnforcingGateways(row.Rule, GetActLocation(), SelectedReportType), - row => string.Join(", ", row.Rule.EnforcingGateways.Select(device => device.Content.Name)))); + row => string.Join(", ", row.Rule.EnforcingGateways.Select(device => device.Content.Name)), + enableResizing: true, enableReordering: true)); columns.Add(CreateTextColumn( "uid", @@ -303,14 +317,14 @@ row => row.Rule.Uid ?? string.Empty, row => row.Rule.Uid ?? string.Empty, "minw-200", - hideable: true)); + hideable: true, enableResizing: true, enableReordering: true)); columns.Add(CreateTextColumn( "comment", userConfig.GetText("comment"), row => row.Rule.Comment ?? string.Empty, row => row.Rule.Comment ?? string.Empty, - hideable: true)); + hideable: true, enableResizing: true, enableReordering: true)); if (!ShowLastModifiedAfterLastHit) { @@ -319,7 +333,7 @@ LastModifiedTitle, row => RuleDisplayBase.DisplayLastModified(row.Rule), row => row.Rule.LastModified?.ToString("O") ?? string.Empty, - hideable: true)); + hideable: true, enableResizing: true, enableReordering: true)); } if (ShowLastHitColumn) @@ -329,7 +343,7 @@ userConfig.GetText("last_hit"), row => RuleDisplayHtml.DisplayLastHit(row.Rule.Metadata), row => row.Rule.Metadata.LastHit?.ToString("O") ?? string.Empty, - "minw-80")); + "minw-80", enableResizing: true, enableReordering: true)); } return columns; @@ -341,7 +355,9 @@ Func valueSelector, Func sortValueSelector, string? cellClass = null, - bool hideable = false) + bool hideable = false, + bool enableResizing = false, + bool enableReordering = false) { return new FancyColumn { @@ -352,7 +368,10 @@ Hideable = hideable, CellClass = cellClass, ValueSelector = valueSelector, - SortValueSelector = row => sortValueSelector(row) + SortValueSelector = row => sortValueSelector(row), + Resizable = enableResizing, + Reorderable = enableReordering, + MinWidth = 150 }; } @@ -362,7 +381,9 @@ Func markupSelector, Func sortValueSelector, string? cellClass = null, - bool hideable = false) + bool hideable = false, + bool enableResizing = false, + bool enableReordering = false) { return new FancyColumn { @@ -375,7 +396,10 @@ ValueSelector = sortValueSelector, SearchTextSelector = row => sortValueSelector(row), SortValueSelector = row => sortValueSelector(row), - CellTemplate = CreateMarkupCell(markupSelector) + CellTemplate = CreateMarkupCell(markupSelector), + Resizable = enableResizing, + Reorderable = enableReordering, + MinWidth = 150 }; }