Add DashboardFilterBar and move global filters to top of dashboard
- Create DashboardFilterBar component with time-range and origin-filter toggle-button groups in a single card row (Stage 7, Tasks 7.1–7.3) - Integrate filter bar below ServerStatusBar in DashboardPage; remove filter toolbars from the Ban List section header (Task 7.2) - Add 6 tests covering rendering, active-state reflection, and callbacks - tsc --noEmit, eslint, npm run build, npm test all pass (27/27 tests)
This commit is contained in:
154
Docs/Tasks.md
154
Docs/Tasks.md
@@ -527,3 +527,157 @@ Run the complete quality-assurance pipeline:
|
||||
- Build artifacts generated successfully.
|
||||
|
||||
---
|
||||
|
||||
## Stage 7 — Global Dashboard Filter Bar
|
||||
|
||||
The time-range and origin-filter controls currently live inside the "Ban List" section header, but they control **every** section on the dashboard (Ban Trend, Top Countries, Jail Distribution, **and** Ban List). This creates a misleading UX: the buttons appear scoped to the ban list when they are actually global. This stage extracts those controls into a dedicated, always-visible filter bar at the top of the dashboard, directly below the `ServerStatusBar`.
|
||||
|
||||
### Task 7.1 — Create the `DashboardFilterBar` component
|
||||
|
||||
**Status:** `done`
|
||||
|
||||
Create `frontend/src/components/DashboardFilterBar.tsx`. This is a self-contained toolbar component that renders the time-range presets and origin filter as two groups of toggle buttons.
|
||||
|
||||
**Props interface:**
|
||||
|
||||
```ts
|
||||
interface DashboardFilterBarProps {
|
||||
timeRange: TimeRange;
|
||||
onTimeRangeChange: (value: TimeRange) => void;
|
||||
originFilter: BanOriginFilter;
|
||||
onOriginFilterChange: (value: BanOriginFilter) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Visual requirements:**
|
||||
|
||||
- Render inside a card-like container using the existing `section` style pattern (neutral background, border, border-radius, padding) — but **without** a section title. The toolbar **is** the content.
|
||||
- Layout: a single row with two `<Toolbar>` groups separated by a visual divider (use Fluent UI `<Divider vertical>` or a horizontal gap of `spacingHorizontalXL`).
|
||||
- **Left group** — "Time Range" label + four `<ToggleButton>` presets:
|
||||
- `Last 24 h` (value `"24h"`)
|
||||
- `Last 7 days` (value `"7d"`)
|
||||
- `Last 30 days` (value `"30d"`)
|
||||
- `Last 365 days` (value `"365d"`)
|
||||
- **Right group** — "Filter" label + three `<ToggleButton>` options:
|
||||
- `All` (value `"all"`)
|
||||
- `Blocklist` (value `"blocklist"`)
|
||||
- `Selfblock` (value `"selfblock"`)
|
||||
- Each group label is a `<Text weight="semibold" size={300}>` rendered inline before the buttons.
|
||||
- Use `size="small"` on all toggle buttons. The active button uses `checked={true}` and `aria-pressed={true}`.
|
||||
- On narrow viewports (< 640 px), the two groups should **wrap** onto separate lines (use `flexWrap: "wrap"` on the outer container).
|
||||
- Reuse `TIME_RANGE_LABELS` and `BAN_ORIGIN_FILTER_LABELS` from `types/ban.ts` — no hard-coded label strings.
|
||||
- Use `makeStyles` for all styling. Follow [Web-Design.md](Web-Design.md) spacing conventions: `spacingHorizontalM` between buttons within a group, `spacingHorizontalXL` between groups, `spacingVerticalS` for vertical padding.
|
||||
|
||||
**Behaviour:**
|
||||
|
||||
- Clicking a time-range button calls `onTimeRangeChange(value)`.
|
||||
- Clicking an origin-filter button calls `onOriginFilterChange(value)`.
|
||||
- Exactly one button per group is active at any time (mutually exclusive — not multi-select).
|
||||
- Component is fully controlled: it does not own state, it receives and reports values only.
|
||||
|
||||
**File structure rules:**
|
||||
|
||||
- One component per file. No barrel exports needed — import directly.
|
||||
- Keep under 100 lines.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- The component renders two labelled button groups in a single row.
|
||||
- Calls the correct callback with the correct value when a button is clicked.
|
||||
- Buttons reflect the current selection via `checked` / `aria-pressed`.
|
||||
- Wraps gracefully on narrow viewports.
|
||||
- `tsc --noEmit` passes. No `any`. ESLint clean.
|
||||
|
||||
---
|
||||
|
||||
### Task 7.2 — Integrate `DashboardFilterBar` into `DashboardPage`
|
||||
|
||||
**Status:** `done`
|
||||
|
||||
Move the global filter controls out of the "Ban List" section and replace them with the new `DashboardFilterBar`, placed at the top of the page.
|
||||
|
||||
**Changes to `DashboardPage.tsx`:**
|
||||
|
||||
1. **Add** `<DashboardFilterBar>` immediately **below** `<ServerStatusBar />` and **above** the "Ban Trend" section. Pass the existing `timeRange`, `setTimeRange`, `originFilter`, and `setOriginFilter` as props.
|
||||
2. **Remove** the two `<Toolbar>` blocks (time-range selector and origin filter) that are currently inside the "Ban List" section header. The section header should keep only the `<Text as="h2">Ban List</Text>` title — no filter buttons.
|
||||
3. **Remove** the `TIME_RANGES` and `ORIGIN_FILTERS` local constant arrays from `DashboardPage.tsx` since the `DashboardFilterBar` component now owns the iteration. (If `DashboardFilterBar` re-uses these arrays, it defines them locally or imports them from `types/ban.ts`.)
|
||||
4. **Keep** the `timeRange` and `originFilter` state (`useState`) in `DashboardPage` — the page still owns the state; it just no longer renders the buttons directly.
|
||||
5. **Verify** that all sections (Ban Trend, Top Countries, Jail Distribution, Ban List) still receive the filter values as props and re-render when they change — this should already work since the state location is unchanged.
|
||||
|
||||
**Layout after change:**
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ ServerStatusBar │
|
||||
├──────────────────────────────────────┤
|
||||
│ DashboardFilterBar │ ← NEW location
|
||||
│ [24h] [7d] [30d] [365d] │ [All] [Blocklist] [Selfblock] │
|
||||
├──────────────────────────────────────┤
|
||||
│ Ban Trend (chart) │
|
||||
├──────────────────────────────────────┤
|
||||
│ Top Countries (pie + bar) │
|
||||
├──────────────────────────────────────┤
|
||||
│ Jail Distribution (bar) │
|
||||
├──────────────────────────────────────┤
|
||||
│ Ban List (table) │ ← filters REMOVED from here
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- The filter bar is visible at the top of the dashboard, below the status bar.
|
||||
- Changing a filter updates all four sections simultaneously.
|
||||
- The "Ban List" section header no longer contains filter buttons.
|
||||
- No functional regression — the dashboard behaves identically, filters are just relocated.
|
||||
- `tsc --noEmit` and ESLint pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 7.3 — Write tests for `DashboardFilterBar`
|
||||
|
||||
**Status:** `done`
|
||||
|
||||
Create `frontend/src/components/__tests__/DashboardFilterBar.test.tsx`.
|
||||
|
||||
**Test cases:**
|
||||
|
||||
1. **Renders all time-range buttons** — confirm four buttons with correct labels appear.
|
||||
2. **Renders all origin-filter buttons** — confirm three buttons with correct labels appear.
|
||||
3. **Active state matches props** — given `timeRange="7d"` and `originFilter="blocklist"`, the corresponding buttons have `aria-pressed="true"` and the others `"false"`.
|
||||
4. **Time-range click fires callback** — click the "Last 30 days" button, assert `onTimeRangeChange` was called with `"30d"`.
|
||||
5. **Origin-filter click fires callback** — click the "Selfblock" button, assert `onOriginFilterChange` was called with `"selfblock"`.
|
||||
6. **Already-active button click still fires callback** — clicking the currently active button should still call the callback (no no-op guard).
|
||||
|
||||
**Test setup:**
|
||||
|
||||
- Wrap the component in `<FluentProvider theme={webLightTheme}>` (required for Fluent UI token resolution).
|
||||
- Use `vi.fn()` for the callback props.
|
||||
- Follow the existing test patterns in `frontend/src/components/__tests__/`.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- All 6 test cases pass.
|
||||
- Tests are fully typed — no `any`.
|
||||
- ESLint clean.
|
||||
|
||||
---
|
||||
|
||||
### Task 7.4 — Final lint, type-check, and build verification
|
||||
|
||||
**Status:** `done`
|
||||
|
||||
Run the full quality-assurance pipeline after the filter-bar changes:
|
||||
|
||||
1. `tsc --noEmit` — zero errors.
|
||||
2. `npm run lint` — zero warnings, zero errors.
|
||||
3. `npm run build` — succeeds.
|
||||
4. `npm test` — all frontend tests pass (including the new `DashboardFilterBar` tests).
|
||||
5. Backend: `ruff check`, `mypy --strict`, `pytest` — still green (no backend changes expected, but verify no accidental modifications).
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- Zero lint warnings/errors.
|
||||
- All tests pass on both frontend and backend.
|
||||
- Production build succeeds.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user