- @* All filters sit in one wrapped row. The four multi-value dimensions
- (Channel / Kind / Status / Site) use compact MultiSelectDropdown
- controls so the bar stays a row or two tall instead of four stacked
- blocks of chip buttons. *@
+ @* All filters sit in one wrapped row. Kind / Status / Site use compact
+ MultiSelectDropdown controls; Channel is a single-select because the
+ Kind options narrow to the chosen channel — so the bar stays a row or
+ two tall instead of four stacked blocks of chip buttons. *@
+ @* Single-select: one channel at a time, so the Kind options below
+ narrow cleanly to that channel. "All channels" clears it. *@
-
-
-
-
+
+
@* Kind options are narrowed by the Channel selection (VisibleKinds). *@
diff --git a/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs b/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs
index 051b0c6..8966503 100644
--- a/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs
+++ b/src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor.cs
@@ -8,13 +8,14 @@ namespace ScadaLink.CentralUI.Components.Audit;
///
/// Filter bar for the central Audit Log page (#23 M7-T2). Owns the
/// binding state and renders the filter controls
-/// — Channel / Kind / Status / Site as compact
+/// — Channel as a single-select (one channel at a time, so the Kind options
+/// narrow to it cleanly); Kind / Status / Site as compact
///
-/// controls, plus the time range, free-text searches and the Errors-only
+/// controls; plus the time range, free-text searches and the Errors-only
/// toggle — and publishes an via
-/// when the user clicks Apply. The four
-/// multi-value dimensions map straight through to the filter's list fields;
-/// see for the Errors-only and time-range rules.
+/// when the user clicks Apply. The selected
+/// dimensions map through to the filter's list fields; see
+/// for the Errors-only and time-range rules.
///
public partial class AuditFilterBar
{
@@ -82,8 +83,29 @@ public partial class AuditFilterBar
}
///
- /// Runs after a Channel selection changes. Drops any Kind selections that fell
- /// outside the new visible set — without this, removing a channel could leave
+ /// Single-select Channel binding for the filter bar. The Audit Log filters one
+ /// channel at a time so the Kind options narrow cleanly to it; the model still
+ /// stores the selection as a set (0 or 1 entry) so
+ /// and are unchanged. null = all channels.
+ ///
+ private AuditChannel? SelectedChannel
+ {
+ get => _model.Channels.Count > 0 ? _model.Channels.First() : null;
+ set
+ {
+ _model.Channels.Clear();
+ if (value is { } channel)
+ {
+ _model.Channels.Add(channel);
+ }
+
+ OnChannelsChanged();
+ }
+ }
+
+ ///
+ /// Runs after the Channel selection changes. Drops any Kind selections that fell
+ /// outside the new visible set — without this, changing the channel could leave
/// stale Kind selections that no longer match any visible option.
///
private void OnChannelsChanged()
diff --git a/tests/ScadaLink.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs b/tests/ScadaLink.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs
index 8bce836..68305d0 100644
--- a/tests/ScadaLink.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs
+++ b/tests/ScadaLink.CentralUI.PlaywrightTests/Audit/AuditLogPageTests.cs
@@ -91,9 +91,8 @@ public class AuditLogPageTests
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
// Pre-Apply, both rows are absent because the grid stays empty until
- // the user filters. Open the Channel dropdown, tick ApiOutbound, Apply.
- await page.Locator("[data-test='filter-channel-ms-toggle']").ClickAsync();
- await page.Locator("[data-test='filter-channel-ms-opt-ApiOutbound']").ClickAsync();
+ // the user filters. Pick the ApiOutbound channel, then Apply.
+ await page.Locator("[data-test='filter-channel-select']").SelectOptionAsync("ApiOutbound");
await page.Locator("[data-test='filter-apply']").ClickAsync();
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
diff --git a/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs b/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs
index 673971e..d62918d 100644
--- a/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs
+++ b/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs
@@ -13,10 +13,11 @@ namespace ScadaLink.CentralUI.Tests.Components.Audit;
///
/// bUnit tests for (#23 M7-T2 / Bundle B).
///
-/// The bar carries the 10 spec filter elements plus the Errors-only toggle. The
-/// Channel / Kind / Status / Site dimensions are rendered as
+/// The bar carries the 10 spec filter elements plus the Errors-only toggle.
+/// Channel is a single-select <select data-test="filter-channel-select">;
+/// Kind / Status / Site are
///
-/// controls; each option is a checkbox tagged
+/// controls whose options are checkboxes tagged
/// data-test="filter-<dim>-ms-opt-<value>". Tests pin:
/// (1) the full filter set renders; (2) Apply raises OnFilterChanged with
/// the selected values; (3) the Channel→Kind narrowing map drives Kind option
@@ -75,8 +76,8 @@ public class AuditFilterBarTests : BunitContext
var cut = Render(p => p
.Add(c => c.OnFilterChanged, EventCallback.Factory.Create(this, f => captured = f)));
- // Drive UI: tick a Channel option, type in the Target search box, click Apply.
- cut.Find("[data-test=\"filter-channel-ms-opt-ApiOutbound\"]").Change(true);
+ // Drive UI: pick a Channel, type in the Target search box, click Apply.
+ cut.Find("[data-test=\"filter-channel-select\"]").Change("ApiOutbound");
cut.Find("[data-test=\"filter-target\"] input").Change("Plant-A-OPC");
cut.Find("[data-test=\"filter-apply\"]").Click();
@@ -86,23 +87,25 @@ public class AuditFilterBarTests : BunitContext
}
[Fact]
- public void Apply_WithMultipleChannelChips_PassesAllSelectedChannels()
+ public void ChangingChannel_ReplacesTheSelection_SingleSelect()
{
- // Task 9: ToFilter no longer collapses the chip multi-select — every
- // selected channel chip reaches the filter's Channels list.
+ // Channel is single-select: picking a second channel replaces the first
+ // rather than adding to it (the page filters one channel at a time).
AuditLogQueryFilter? captured = null;
var cut = Render(p => p
.Add(c => c.OnFilterChanged, EventCallback.Factory.Create(this, f => captured = f)));
- cut.Find("[data-test=\"filter-channel-ms-opt-ApiOutbound\"]").Change(true);
- cut.Find("[data-test=\"filter-channel-ms-opt-Notification\"]").Change(true);
+ cut.Find("[data-test=\"filter-channel-select\"]").Change("ApiOutbound");
+ cut.Find("[data-test=\"filter-channel-select\"]").Change("Notification");
cut.Find("[data-test=\"filter-apply\"]").Click();
Assert.NotNull(captured);
- Assert.NotNull(captured!.Channels);
- Assert.Equal(2, captured.Channels!.Count);
- Assert.Contains(AuditChannel.ApiOutbound, captured.Channels);
- Assert.Contains(AuditChannel.Notification, captured.Channels);
+ Assert.Equal(new[] { AuditChannel.Notification }, captured!.Channels);
+
+ // Selecting "All channels" clears the channel filter entirely.
+ cut.Find("[data-test=\"filter-channel-select\"]").Change(string.Empty);
+ cut.Find("[data-test=\"filter-apply\"]").Click();
+ Assert.Null(captured!.Channels);
}
[Fact]
@@ -116,8 +119,8 @@ public class AuditFilterBarTests : BunitContext
Assert.Contains($"data-test=\"filter-kind-ms-opt-{kind}\"", cut.Markup);
}
- // Select only ApiOutbound; Kind options outside the channel-kind map drop out.
- cut.Find("[data-test=\"filter-channel-ms-opt-ApiOutbound\"]").Change(true);
+ // Select ApiOutbound; Kind options outside the channel-kind map drop out.
+ cut.Find("[data-test=\"filter-channel-select\"]").Change("ApiOutbound");
var apiKinds = AuditQueryModel.KindsByChannel[AuditChannel.ApiOutbound];
foreach (var kind in apiKinds)