diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 5a724b1..2b67530 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -420,6 +420,8 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue. **Goal:** Move `areHistoryQueriesEqual` to `frontend/src/utils/` (either into a new `queryUtils.ts` or an existing utility file if a suitable one exists). Import it back into `HistoryPage.tsx` from that location. Verify the function has no implicit dependency on page-local types — if it relies on `HistoryQuery` from `types/history.ts`, it can still live in utils by importing that type. +**Status:** Completed. + **Possible traps and issues:** - This is a mechanical move with no behavioral change. The only risk is a stale import if the function is used in more than one place (confirm with a search before moving). - If similar utility functions exist in other page files (a search for `function` declarations inside page files is worthwhile), extract them in the same pass. @@ -445,6 +447,8 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue. - Move `theme/commonStyles.ts`: if the styles in it are shared across multiple components with no better home, move them to the component directory that uses them most, or rename the file to `components/commonStyles.ts`. The `theme/` directory must contain only `customTheme.ts` and token-related files. - For `configStyles.ts` and `blocklistStyles.ts`: inline each exported `makeStyles` hook into the component file that uses it, or keep the file in the same directory as the components but document that it is a style helper rather than a theme definition. The architecture rule is "co-locate styles in the same file as the component" — a shared styles file is acceptable only if documented as an explicit exception for styles used by multiple components in the same subdirectory. +**Status:** Completed. + **Possible traps and issues:** - Moving `isoNumericToAlpha2.ts` changes its import path in `WorldMap.tsx` and any other consumer — update all import sites. - Moving `api/map.test.ts` to a `__tests__` subdirectory requires the test runner config (`vitest.config.ts`) to include `api/__tests__/` if it only scans `__tests__/` top-level directories — verify the glob pattern first. diff --git a/frontend/src/api/map.test.ts b/frontend/src/api/__tests__/map.test.ts similarity index 84% rename from frontend/src/api/map.test.ts rename to frontend/src/api/__tests__/map.test.ts index f561b71..f69621a 100644 --- a/frontend/src/api/map.test.ts +++ b/frontend/src/api/__tests__/map.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Mock } from "vitest"; -import { ENDPOINTS } from "./endpoints"; -import { fetchBansByCountry } from "./map"; -import { get } from "./client"; +import { ENDPOINTS } from "../endpoints"; +import { fetchBansByCountry } from "../map"; +import { get } from "../client"; -vi.mock("./client", () => ({ +vi.mock("../client", () => ({ get: vi.fn(), })); diff --git a/frontend/src/components/DashboardFilterBar.tsx b/frontend/src/components/DashboardFilterBar.tsx index f0ba69a..1a9feae 100644 --- a/frontend/src/components/DashboardFilterBar.tsx +++ b/frontend/src/components/DashboardFilterBar.tsx @@ -15,7 +15,7 @@ import { makeStyles, tokens, } from "@fluentui/react-components"; -import { useCardStyles } from "../theme/commonStyles"; +import { useCardStyles } from "../components/commonStyles"; import type { BanOriginFilter, TimeRange } from "../types/ban"; import { BAN_ORIGIN_FILTER_LABELS, diff --git a/frontend/src/components/ServerStatusBar.tsx b/frontend/src/components/ServerStatusBar.tsx index df96b87..074b25c 100644 --- a/frontend/src/components/ServerStatusBar.tsx +++ b/frontend/src/components/ServerStatusBar.tsx @@ -18,7 +18,7 @@ import { tokens, Tooltip, } from "@fluentui/react-components"; -import { useCardStyles } from "../theme/commonStyles"; +import { useCardStyles } from "../components/commonStyles"; import { ArrowClockwiseRegular, ShieldRegular } from "@fluentui/react-icons"; import { useServerStatus } from "../hooks/useServerStatus"; diff --git a/frontend/src/components/WorldMap.tsx b/frontend/src/components/WorldMap.tsx index 70d2e34..71e2a4d 100644 --- a/frontend/src/components/WorldMap.tsx +++ b/frontend/src/components/WorldMap.tsx @@ -22,8 +22,8 @@ import type { Topology, } from "topojson-specification"; import worldData from "world-atlas/countries-110m.json"; -import { useCardStyles } from "../theme/commonStyles"; -import { ISO_NUMERIC_TO_ALPHA2 } from "../data/isoNumericToAlpha2"; +import { useCardStyles } from "../components/commonStyles"; +import { ISO_NUMERIC_TO_ALPHA2 } from "../utils/isoNumericToAlpha2"; import { getBanCountColor } from "../utils/mapColors"; const MAP_WIDTH = 800; diff --git a/frontend/src/components/blocklist/BlocklistImportLogSection.tsx b/frontend/src/components/blocklist/BlocklistImportLogSection.tsx index 1840e2e..3aa4d19 100644 --- a/frontend/src/components/blocklist/BlocklistImportLogSection.tsx +++ b/frontend/src/components/blocklist/BlocklistImportLogSection.tsx @@ -1,6 +1,6 @@ import { Button, Badge, Table, TableBody, TableCell, TableCellLayout, TableHeader, TableHeaderCell, TableRow, Text, MessageBar, MessageBarBody, Spinner } from "@fluentui/react-components"; import { ArrowClockwiseRegular } from "@fluentui/react-icons"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useImportLog } from "../../hooks/useBlocklist"; import { useBlocklistStyles } from "./blocklistStyles"; diff --git a/frontend/src/components/blocklist/BlocklistScheduleSection.tsx b/frontend/src/components/blocklist/BlocklistScheduleSection.tsx index 5ce98f6..5ed3dda 100644 --- a/frontend/src/components/blocklist/BlocklistScheduleSection.tsx +++ b/frontend/src/components/blocklist/BlocklistScheduleSection.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Button, Field, Input, MessageBar, MessageBarBody, Select, Spinner, Text } from "@fluentui/react-components"; import { PlayRegular } from "@fluentui/react-icons"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useSchedule } from "../../hooks/useBlocklist"; import { useBlocklistStyles } from "./blocklistStyles"; import type { ScheduleConfig, ScheduleFrequency } from "../../types/blocklist"; diff --git a/frontend/src/components/blocklist/BlocklistSourcesSection.tsx b/frontend/src/components/blocklist/BlocklistSourcesSection.tsx index 9345137..f876653 100644 --- a/frontend/src/components/blocklist/BlocklistSourcesSection.tsx +++ b/frontend/src/components/blocklist/BlocklistSourcesSection.tsx @@ -14,7 +14,7 @@ import { TableRow, Text, } from "@fluentui/react-components"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { AddRegular, ArrowClockwiseRegular, diff --git a/frontend/src/theme/commonStyles.ts b/frontend/src/components/commonStyles.ts similarity index 100% rename from frontend/src/theme/commonStyles.ts rename to frontend/src/components/commonStyles.ts diff --git a/frontend/src/components/jail/BannedIpsSection.tsx b/frontend/src/components/jail/BannedIpsSection.tsx index ff1b3f5..b6438b9 100644 --- a/frontend/src/components/jail/BannedIpsSection.tsx +++ b/frontend/src/components/jail/BannedIpsSection.tsx @@ -33,7 +33,7 @@ import { type TableColumnDefinition, createTableColumn, } from "@fluentui/react-components"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { formatTimestamp } from "../../utils/formatDate"; import { ArrowClockwiseRegular, diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index b8fe6a1..d0a235b 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -15,7 +15,7 @@ import { DashboardFilterBar } from "../components/DashboardFilterBar"; import { ServerStatusBar } from "../components/ServerStatusBar"; import { TopCountriesBarChart } from "../components/TopCountriesBarChart"; import { TopCountriesPieChart } from "../components/TopCountriesPieChart"; -import { useCommonSectionStyles } from "../theme/commonStyles"; +import { useCommonSectionStyles } from "../components/commonStyles"; import { useDashboardCountryData } from "../hooks/useDashboardCountryData"; import type { BanOriginFilter, TimeRange } from "../types/ban"; diff --git a/frontend/src/pages/HistoryPage.tsx b/frontend/src/pages/HistoryPage.tsx index 6f7bf31..01b56ae 100644 --- a/frontend/src/pages/HistoryPage.tsx +++ b/frontend/src/pages/HistoryPage.tsx @@ -32,6 +32,7 @@ import { import { DashboardFilterBar } from "../components/DashboardFilterBar"; import { useHistory } from "../hooks/useHistory"; import { IpDetailView } from "./history/IpDetailView"; +import { areHistoryQueriesEqual } from "../utils/queryUtils"; import type { HistoryBanItem, HistoryQuery, TimeRange } from "../types/history"; import type { BanOriginFilter } from "../types/ban"; @@ -119,25 +120,6 @@ 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.source === b.source && - a.page === b.page && - a.page_size === b.page_size - ); -} - // --------------------------------------------------------------------------- // Column definitions for the main history table // --------------------------------------------------------------------------- diff --git a/frontend/src/pages/history/IpDetailView.tsx b/frontend/src/pages/history/IpDetailView.tsx index 8faab34..f061f5e 100644 --- a/frontend/src/pages/history/IpDetailView.tsx +++ b/frontend/src/pages/history/IpDetailView.tsx @@ -15,7 +15,7 @@ import { tokens, } from "@fluentui/react-components"; import { ArrowCounterclockwiseRegular, ArrowLeftRegular } from "@fluentui/react-icons"; -import { useCardStyles } from "../../theme/commonStyles"; +import { useCardStyles } from "../../components/commonStyles"; import { useIpHistory } from "../../hooks/useHistory"; interface IpDetailViewProps { diff --git a/frontend/src/pages/jail/BantimeEscalationSection.tsx b/frontend/src/pages/jail/BantimeEscalationSection.tsx index 4c2aff4..c11339b 100644 --- a/frontend/src/pages/jail/BantimeEscalationSection.tsx +++ b/frontend/src/pages/jail/BantimeEscalationSection.tsx @@ -1,5 +1,5 @@ import { Badge, Text } from "@fluentui/react-components"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useJailDetailPageStyles } from "./jailDetailPageStyles"; import type { Jail } from "../../types/jail"; import { formatSeconds } from "../../utils/formatDate"; diff --git a/frontend/src/pages/jail/IgnoreListSection.tsx b/frontend/src/pages/jail/IgnoreListSection.tsx index 39d56f1..64ffe80 100644 --- a/frontend/src/pages/jail/IgnoreListSection.tsx +++ b/frontend/src/pages/jail/IgnoreListSection.tsx @@ -11,7 +11,7 @@ import { Tooltip, } from "@fluentui/react-components"; import { DismissRegular } from "@fluentui/react-icons"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useJailDetailPageStyles } from "./jailDetailPageStyles"; interface IgnoreListSectionProps { diff --git a/frontend/src/pages/jail/JailInfoSection.tsx b/frontend/src/pages/jail/JailInfoSection.tsx index 296ba4d..2305428 100644 --- a/frontend/src/pages/jail/JailInfoSection.tsx +++ b/frontend/src/pages/jail/JailInfoSection.tsx @@ -14,7 +14,7 @@ import { PlayRegular, StopRegular, } from "@fluentui/react-icons"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useJailDetailPageStyles } from "./jailDetailPageStyles"; import type { Jail } from "../../types/jail"; diff --git a/frontend/src/pages/jail/PatternsSection.tsx b/frontend/src/pages/jail/PatternsSection.tsx index 1dc7d72..dbd575c 100644 --- a/frontend/src/pages/jail/PatternsSection.tsx +++ b/frontend/src/pages/jail/PatternsSection.tsx @@ -1,5 +1,5 @@ import { Text } from "@fluentui/react-components"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import type { Jail } from "../../types/jail"; import { CodeList } from "./CodeList"; diff --git a/frontend/src/pages/jails/BanUnbanForm.tsx b/frontend/src/pages/jails/BanUnbanForm.tsx index ee70545..856e51f 100644 --- a/frontend/src/pages/jails/BanUnbanForm.tsx +++ b/frontend/src/pages/jails/BanUnbanForm.tsx @@ -10,7 +10,7 @@ import { tokens, } from "@fluentui/react-components"; import { LockClosedRegular, LockOpenRegular } from "@fluentui/react-icons"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useJailsPageStyles } from "./jailsPageStyles"; import { ApiError } from "../../api/client"; diff --git a/frontend/src/pages/jails/IpLookupSection.tsx b/frontend/src/pages/jails/IpLookupSection.tsx index ed94d30..5851ad7 100644 --- a/frontend/src/pages/jails/IpLookupSection.tsx +++ b/frontend/src/pages/jails/IpLookupSection.tsx @@ -10,7 +10,7 @@ import { Text, } from "@fluentui/react-components"; import { SearchRegular } from "@fluentui/react-icons"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useJailsPageStyles } from "./jailsPageStyles"; import { useIpLookup } from "../../hooks/useJails"; diff --git a/frontend/src/pages/jails/JailOverviewSection.tsx b/frontend/src/pages/jails/JailOverviewSection.tsx index f1fac34..d7dbdb2 100644 --- a/frontend/src/pages/jails/JailOverviewSection.tsx +++ b/frontend/src/pages/jails/JailOverviewSection.tsx @@ -25,7 +25,7 @@ import { PlayRegular, StopRegular, } from "@fluentui/react-icons"; -import { useCommonSectionStyles } from "../../theme/commonStyles"; +import { useCommonSectionStyles } from "../../components/commonStyles"; import { useJailsPageStyles } from "./jailsPageStyles"; import { useJails } from "../../hooks/useJails"; import type { JailSummary } from "../../types/jail"; diff --git a/frontend/src/utils/__tests__/queryUtils.test.ts b/frontend/src/utils/__tests__/queryUtils.test.ts new file mode 100644 index 0000000..c8549c8 --- /dev/null +++ b/frontend/src/utils/__tests__/queryUtils.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { areHistoryQueriesEqual } from "../queryUtils"; + +describe("areHistoryQueriesEqual", () => { + it("returns true for identical history queries", () => { + const a = { + range: "7d", + origin: "blocklist", + jail: "ssh", + ip: "192.0.2.1", + source: "archive", + page: 2, + page_size: 50, + }; + + const b = { ...a }; + + expect(areHistoryQueriesEqual(a, b)).toBe(true); + }); + + it("returns false when a single query field differs", () => { + const base = { + range: "7d", + origin: "all", + jail: "ssh", + ip: "192.0.2.1", + source: "archive", + page: 2, + page_size: 50, + }; + + expect( + areHistoryQueriesEqual(base, { ...base, page: 3 }), + ).toBe(false); + expect( + areHistoryQueriesEqual(base, { ...base, ip: "198.51.100.1" }), + ).toBe(false); + }); +}); diff --git a/frontend/src/data/isoNumericToAlpha2.ts b/frontend/src/utils/isoNumericToAlpha2.ts similarity index 100% rename from frontend/src/data/isoNumericToAlpha2.ts rename to frontend/src/utils/isoNumericToAlpha2.ts diff --git a/frontend/src/utils/queryUtils.ts b/frontend/src/utils/queryUtils.ts new file mode 100644 index 0000000..6f757f6 --- /dev/null +++ b/frontend/src/utils/queryUtils.ts @@ -0,0 +1,27 @@ +/** + * Shared query utilities used by pages and hooks. + */ + +import type { HistoryQuery } from "../types/history"; + +/** + * Compare two history query objects for semantic equality. + * + * @param a - First query object. + * @param b - Second query object. + * @returns True when every query field has the same value. + */ +export 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.source === b.source && + a.page === b.page && + a.page_size === b.page_size + ); +}