diff --git a/Docs/Tasks.md b/Docs/Tasks.md index e17f099..7df2267 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -8,26 +8,56 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue. ## Open Issues -### Worldmap list pagination (new) +### History page filters and behavior (frontend) -- Status: done -- Goal: Add paging controls to the WorldMap companion bans table (map page) so it matches Dashboard ban-list behavior and supports country drilldown without rendering / scrolling huge result sets. +1. [x] Time range filter currently not working + - Scope: frontend `history` page state/logic (likely in `frontend/src` route or component for history table) + - Expected behavior: selecting start/end timestamps (or presets) should filter history rows by event timestamp in the backend query or local data, then rerender filtered results immediately. + - Investigation: + - check component(s) handling time range input, change handlers, and query construction. + - confirm backend API supports `startTime`/`endTime` parameters for history endpoint (`/api/history` or similar). + - confirm values are passed as ISO strings/epoch and matched in backend. + - Fix should include: + - wiring the time range inputs to the filtering state. + - constructing API query with time range params. + - verifying with unit/integration tests plus manual through UI. -- Implement in `frontend/src/pages/MapPage.tsx` using the dashboard pattern from `frontend/src/components/BanTable.tsx`: - 1. Add local state: `page` (start 1), `pageSize` (e.g., 100). - 2. Derive `visibleBans` from `useMemo` + `selectedCountry`, then compute `pageBans` using `slice((page-1)*pageSize, page*pageSize)`. - 3. Compute `totalPages = Math.max(1, Math.ceil(visibleBans.length / pageSize))`, `hasPrev`, `hasNext`. - 4. Render a footer row below Table with total, page, prev/next buttons (use icons `ChevronLeftRegular`, `ChevronRightRegular`, or existing style). - 5. Wire buttons to `setPage(page - 1)` / `setPage(page + 1)`, disabled by bounds. - 6. Reset `page` to 1 whenever `visibleBans` filter changes: on `range`, `originFilter`, `selectedCountry`, or when data refreshes. +2. [x] Global filter (All | Blocklist | Selfblock) not working + - Scope: frontend `history` page filter chips or select control. + - Expected behavior: choosing `All`, `Blocklist`, `Selfblock` should apply corresponding filter in same history query (no results for unmatched types). + - Tasks: + - locate filter control component and event handlers. + - validate value mapping semantics (`all`=>no filter, `blocklist`=>source=blocklist, `selfblock`=>source=selfblock or equivalent). + - ensure filter value is merged with time range + jail/ip filters in API call. + - add tests for each filter option to confirm behavior. -- UX details: - - Keep existing “no bans found” row behavior. - - Keep summary info bar and total line consistent; show current slice counts in message as needed. - - Ensure paged data is used for `TableBody` mapping. +3. [x] Jail and IP address filters UX alignment with time range + - Scope: frontend `history` page layout/CSS component; likely `HistoryFilters` container. + - Expected behavior: + - Jail filter and IP address filter should be same look (input/select style and spacing) as time range widget. + - Place Jail/IP filters next to time range controls in same row. + - Should be responsive and consistent. + - Tasks: + - modify the `history` filter container markup so time range, jail, ip filter are co-located. + - apply matching CSS classes/styles to Jail/IP as existing time range control. + - verify cross-browser/responsive display with storybook/test if exists. -- Optional follow-ups: - - Allow page-size selector if needed (25/50/100), updating `totalPages` and reset to page 1 on change. - - Add tests in `frontend/src/pages/__tests__/MapPage.test.tsx` for paging behavior (initial page, next/prev, filtering reset). - - Add documentation in `Docs/Web-Design.md` if there is a worldmap data table UI guideline. +4. [x] Remove “Apply” and “Clear” buttons; auto-apply on field change + - Scope: frontend `history` filter form behavior. + - Expected behavior: + - Any filter field change (time range, type, jail, IP) triggers immediate query update (debounced 150-300 ms if needed). + - Remove explicit “Apply” and “Clear” buttons from UI. + - Clear can be replaced by resetting fields automatically or via a small “reset” icon if needed. + - Implementation steps: + - remove button UI elements and event bindings from history page component. + - make each filter input onChange call the shared `applyFilters` logic with current state. + - add debounce to avoid 100% rapid API churn. + - for clear semantics, ensure default/empty state on filter input binds to default query (All). + - add tests to verify no apply/clear buttons present and updates via input change. + +### Acceptance criteria +- On `history` page, time range selection + filter chips + jail/ip are functional and produce filtered results. +- Time range, jail/IP inputs are styled consistently and in same filter row. +- No apply/clear buttons are visible and filter updates occur on value change (with optional debounce). +- Relevant tests added/updated in frontend test suite. diff --git a/frontend/src/pages/HistoryPage.tsx b/frontend/src/pages/HistoryPage.tsx index 84f1eac..3efc1c6 100644 --- a/frontend/src/pages/HistoryPage.tsx +++ b/frontend/src/pages/HistoryPage.tsx @@ -6,7 +6,7 @@ * Rows with repeatedly-banned IPs are highlighted in amber. */ -import { useCallback, useState } from "react"; +import { useEffect, useState } from "react"; import { Badge, Button, @@ -136,6 +136,24 @@ const useStyles = makeStyles({ }, }); +// --------------------------------------------------------------------------- +// Utilities +// --------------------------------------------------------------------------- + +function areHistoryQueriesEqual( + a: HistoryQuery, + b: HistoryQuery, +): boolean { + return ( + a.range === b.range && + a.origin === b.origin && + a.jail === b.jail && + a.ip === b.ip && + a.page === b.page && + a.page_size === b.page_size + ); +} + // --------------------------------------------------------------------------- // Column definitions for the main history table // --------------------------------------------------------------------------- @@ -372,6 +390,7 @@ 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("24h"); @@ -388,15 +407,23 @@ export function HistoryPage(): React.JSX.Element { const { items, total, page, loading, error, setPage, refresh } = useHistory(appliedQuery); - const applyFilters = useCallback((): void => { - setAppliedQuery({ - range: range, + useEffect((): void => { + const nextQuery: HistoryQuery = { + range, origin: originFilter !== "all" ? originFilter : undefined, jail: jailFilter.trim() || undefined, ip: ipFilter.trim() || undefined, + page: 1, page_size: PAGE_SIZE, - }); - }, [range, originFilter, jailFilter, ipFilter]); + }; + + if (areHistoryQueriesEqual(nextQuery, appliedQuery)) { + return; + } + + setPage(1); + setAppliedQuery(nextQuery); + }, [range, originFilter, jailFilter, ipFilter, setPage, appliedQuery]); const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); @@ -458,7 +485,7 @@ export function HistoryPage(): React.JSX.Element { }} /> -
+
Jail
-
+
IP Address { - if (e.key === "Enter") applyFilters(); - }} />
- - -
{/* ---------------------------------------------------------------- */} diff --git a/frontend/src/pages/__tests__/HistoryPage.test.tsx b/frontend/src/pages/__tests__/HistoryPage.test.tsx index 88b4302..57afde5 100644 --- a/frontend/src/pages/__tests__/HistoryPage.test.tsx +++ b/frontend/src/pages/__tests__/HistoryPage.test.tsx @@ -1,11 +1,11 @@ import { describe, expect, it, vi } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { FluentProvider, webLightTheme } from "@fluentui/react-components"; -import { HistoryPage } from "../HistoryPage"; let lastQuery: Record | null = null; const mockUseHistory = vi.fn((query: Record) => { + console.log("mockUseHistory called", query); lastQuery = query; return { items: [], @@ -18,16 +18,16 @@ const mockUseHistory = vi.fn((query: Record) => { }; }); -vi.mock("../hooks/useHistory", () => ({ +vi.mock("../../hooks/useHistory", () => ({ useHistory: (query: Record) => mockUseHistory(query), useIpHistory: () => ({ detail: null, loading: false, error: null, refresh: vi.fn() }), })); -vi.mock("../components/WorldMap", () => ({ +vi.mock("../../components/WorldMap", () => ({ WorldMap: () =>
, })); -vi.mock("../api/config", () => ({ +vi.mock("../../api/config", () => ({ fetchMapColorThresholds: async () => ({ threshold_low: 10, threshold_medium: 50, @@ -35,8 +35,10 @@ vi.mock("../api/config", () => ({ }), })); +import { HistoryPage } from "../HistoryPage"; + describe("HistoryPage", () => { - it("renders DashboardFilterBar and applies origin+range filters", async () => { + it("auto-applies filters on change and hides apply/clear actions", async () => { const user = userEvent.setup(); render( @@ -45,14 +47,30 @@ describe("HistoryPage", () => { , ); - // Initial load should include the default query. - expect(lastQuery).toEqual({ page_size: 50 }); + // Initial load should include the auto-applied default query. + await waitFor(() => { + expect(lastQuery).toEqual({ + range: "24h", + origin: undefined, + jail: undefined, + ip: undefined, + page: 1, + page_size: 50, + }); + }); - // Change the time-range and origin filter, then apply. + expect(screen.queryByRole("button", { name: /apply/i })).toBeNull(); + expect(screen.queryByRole("button", { name: /clear/i })).toBeNull(); + + // Time-range and origin updates should be applied automatically. await user.click(screen.getByRole("button", { name: /Last 7 days/i })); - await user.click(screen.getByRole("button", { name: /Blocklist/i })); - await user.click(screen.getByRole("button", { name: /Apply/i })); + await waitFor(() => { + expect(lastQuery).toMatchObject({ range: "7d" }); + }); - expect(lastQuery).toMatchObject({ range: "7d", origin: "blocklist" }); + await user.click(screen.getByRole("button", { name: /Blocklist/i })); + await waitFor(() => { + expect(lastQuery).toMatchObject({ origin: "blocklist" }); + }); }); });