fix(history): unify History filter bar with Jail and IP inputs
This commit is contained in:
@@ -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 476–510) 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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,34 +476,17 @@ export function HistoryPage(): React.JSX.Element {
|
||||
onOriginFilterChange={(value) => {
|
||||
setOriginFilter(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);
|
||||
jail={jailFilter}
|
||||
onJailChange={(value) => {
|
||||
setJailFilter(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);
|
||||
ip={ipFilter}
|
||||
onIpChange={(value) => {
|
||||
setIpFilter(value);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
{/* Error / loading state */}
|
||||
{/* ---------------------------------------------------------------- */}
|
||||
|
||||
Reference in New Issue
Block a user