fix(history): unify History filter bar with Jail and IP inputs

This commit is contained in:
2026-04-01 09:37:38 +02:00
parent 7967191ccd
commit 13823b1182
4 changed files with 141 additions and 32 deletions

View File

@@ -7,3 +7,38 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue.
---
## Open Issues
### 1. History Screen — Move Jail & IP Address Filters Into the Time Range Bar
**Goal:** Unify all History-page filters into a single bordered bar so that Jail and IP Address sit inside the same card/border as Time Range and Filter, separated by vertical dividers.
**Current state:**
- `DashboardFilterBar` (`frontend/src/components/DashboardFilterBar.tsx`) renders a single bordered card (`cardStyles.card`) that contains two groups — **Time Range** (toggle buttons) and **Filter** (origin toggle buttons) — separated by a vertical `<Divider>`.
- In `HistoryPage` (`frontend/src/pages/HistoryPage.tsx`, lines 476510) the Jail `<Input>` and IP Address `<Input>` are rendered **outside** that bar as separate cards, each wrapped in their own `cardStyles.card` div, laid out horizontally via `styles.filterRow` (flexbox row with gap).
**Desired state:**
- The Jail and IP Address inputs must move **inside** the `DashboardFilterBar` card border (or the equivalent combined container) so the entire filter strip looks like one cohesive section.
- Each new group (Jail, IP Address) is separated from its neighbor by a vertical divider (`|`), using the same `<Divider vertical />` + `styles.divider` pattern already used between Time Range and Filter.
- Inside each group the label text ("Jail", "IP Address") must appear **to the left** of its input field (i.e. `flexDirection: "row"` with `alignItems: "center"`, not above it). This matches the existing group style where the title text sits to the left of the toolbar buttons.
- The visual order inside the bar is: **Time Range** | **Filter** | **Jail** | **IP Address**.
**Files to change:**
1. **`frontend/src/components/DashboardFilterBar.tsx`**
- Accept two new optional props (e.g. `jailSlot?: React.ReactNode` and `ipSlot?: React.ReactNode`, or pass the value+onChange pairs directly). Keep the component reusable — the Dashboard page uses the same component but does not need the Jail/IP inputs, so these slots must be optional.
- After the existing Filter group, conditionally render a `<div className={styles.divider}>` + `<Divider vertical />` followed by a new group for Jail, and repeat for IP Address.
- Each new group should follow the existing `styles.group` layout: a row with the label `<Text weight="semibold" size={300}>` on the left and the `<Input>` on the right, separated by `gap: tokens.spacingHorizontalM`.
2. **`frontend/src/pages/HistoryPage.tsx`**
- Remove the two standalone Jail and IP Address `<div>` cards (currently wrapped in `styles.filterLabel` + `cardStyles.card`).
- Instead, pass the Jail and IP Address controls into `<DashboardFilterBar>` via the new props/slots.
- The `styles.filterLabel` style can be removed if no longer used elsewhere.
**Acceptance criteria:**
- All four filter groups (Time Range, Filter, Jail, IP Address) render inside a single bordered bar.
- Each group is separated by a vertical divider identical to the existing one between Time Range and Filter.
- The labels "Jail" and "IP Address" sit to the **left** of their respective input fields (horizontal layout), not above them.
- The Dashboard page's usage of `DashboardFilterBar` is unaffected (no Jail/IP inputs shown there).
- Existing filter functionality (debounced input, query params, pagination reset) remains unchanged.
Status: completed

View File

@@ -8,6 +8,7 @@
import {
Divider,
Input,
Text,
ToggleButton,
Toolbar,
@@ -35,6 +36,14 @@ export interface DashboardFilterBarProps {
originFilter: BanOriginFilter;
/** Called when the user selects a different origin filter. */
onOriginFilterChange: (value: BanOriginFilter) => void;
/** Jail filter value (optional). */
jail?: string;
/** Called when the jail filter text changes (optional). */
onJailChange?: (value: string) => void;
/** IP address filter value (optional). */
ip?: string;
/** Called when the IP address filter text changes (optional). */
onIpChange?: (value: string) => void;
}
// ---------------------------------------------------------------------------
@@ -92,6 +101,10 @@ export function DashboardFilterBar({
onTimeRangeChange,
originFilter,
onOriginFilterChange,
jail,
onJailChange,
ip,
onIpChange,
}: DashboardFilterBarProps): React.JSX.Element {
const styles = useStyles();
const cardStyles = useCardStyles();
@@ -146,6 +159,48 @@ export function DashboardFilterBar({
))}
</Toolbar>
</div>
{onJailChange && (
<>
<div className={styles.divider}>
<Divider vertical />
</div>
<div className={styles.group}>
<Text weight="semibold" size={300}>
Jail
</Text>
<Input
placeholder="e.g. sshd"
size="small"
value={jail ?? ""}
onChange={(_ev, data): void => {
onJailChange(data.value);
}}
/>
</div>
</>
)}
{onIpChange && (
<>
<div className={styles.divider}>
<Divider vertical />
</div>
<div className={styles.group}>
<Text weight="semibold" size={300}>
IP Address
</Text>
<Input
placeholder="e.g. 192.168"
size="small"
value={ip ?? ""}
onChange={(_ev, data): void => {
onIpChange(data.value);
}}
/>
</div>
</>
)}
</div>
);
}

View File

@@ -125,4 +125,47 @@ describe("DashboardFilterBar", () => {
expect(onTimeRangeChange).toHaveBeenCalledOnce();
expect(onTimeRangeChange).toHaveBeenCalledWith("24h");
});
it("renders jail and ip input controls when provided", async () => {
const onJailChange = vi.fn();
const onIpChange = vi.fn();
render(
<FluentProvider theme={webLightTheme}>
<DashboardFilterBar
timeRange="24h"
onTimeRangeChange={vi.fn()}
originFilter="all"
onOriginFilterChange={vi.fn()}
jail=""
onJailChange={onJailChange}
ip=""
onIpChange={onIpChange}
/>
</FluentProvider>,
);
expect(screen.getByText(/Jail/i)).toBeInTheDocument();
expect(screen.getByText(/IP Address/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/e.g. sshd/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/e.g. 192.168/i)).toBeInTheDocument();
const jailInput = screen.getByPlaceholderText(/e.g. sshd/i);
const ipInput = screen.getByPlaceholderText(/e.g. 192.168/i);
const user = userEvent.setup();
await user.clear(jailInput);
await user.type(jailInput, "x");
expect(onJailChange).toHaveBeenLastCalledWith("x");
await user.clear(ipInput);
await user.type(ipInput, "1");
expect(onIpChange).toHaveBeenLastCalledWith("1");
});
it("does not render jail or ip inputs when handlers are missing", () => {
renderBar();
expect(screen.queryByText(/Jail/i)).toBeNull();
expect(screen.queryByText(/IP Address/i)).toBeNull();
});
});

View File

@@ -16,7 +16,6 @@ import {
DataGridHeader,
DataGridHeaderCell,
DataGridRow,
Input,
MessageBar,
MessageBarBody,
Spinner,
@@ -82,11 +81,6 @@ const useStyles = makeStyles({
gap: tokens.spacingHorizontalM,
flexWrap: "wrap",
},
filterLabel: {
display: "flex",
flexDirection: "column",
gap: tokens.spacingVerticalXS,
},
tableWrapper: {
overflow: "auto",
borderRadius: tokens.borderRadiusMedium,
@@ -390,7 +384,6 @@ function IpDetailView({ ip, onBack }: IpDetailViewProps): React.JSX.Element {
export function HistoryPage(): React.JSX.Element {
const styles = useStyles();
const cardStyles = useCardStyles();
// Filter state
const [range, setRange] = useState<TimeRange>("24h");
@@ -483,32 +476,15 @@ export function HistoryPage(): React.JSX.Element {
onOriginFilterChange={(value) => {
setOriginFilter(value);
}}
jail={jailFilter}
onJailChange={(value) => {
setJailFilter(value);
}}
ip={ipFilter}
onIpChange={(value) => {
setIpFilter(value);
}}
/>
<div className={`${styles.filterLabel} ${cardStyles.card}`}>
<Text size={200}>Jail</Text>
<Input
placeholder="e.g. sshd"
value={jailFilter}
onChange={(_ev, data): void => {
setJailFilter(data.value);
}}
size="small"
/>
</div>
<div className={`${styles.filterLabel} ${cardStyles.card}`}>
<Text size={200}>IP Address</Text>
<Input
placeholder="e.g. 192.168"
value={ipFilter}
onChange={(_ev, data): void => {
setIpFilter(data.value);
}}
size="small"
/>
</div>
</div>
{/* ---------------------------------------------------------------- */}