Skip to content

Add pagination to all unbounded tables #313

Description

@DominicBM

Problem

Several tables in the dashboard render an unbounded number of rows in a single page load with no pagination. This causes slow renders, large DOM sizes, and in some cases silently truncates data the user cannot reach.


Affected tables

1. Website events table — /events/:event_id (all four event types)

View: app/views/shared/_events_table.html.erb
Data source: GaResponseBuilder#response — a single GA4 page with limit: 10_000

The template renders all rows from the GA4 response directly:

<% @events.rows.each do |row| %>

The template already shows a "Showing X–Y of Z items" header using total_results, start_index, and items_per_page from the GA4 response — but there are no next/previous controls. When total_results exceeds 10,000 (confirmed for at least one hub), items beyond the first 10,000 are inaccessible in the UI. The CSV export uses multi_page_response and is unaffected.

Fix: GaResponseBuilder already accepts a start_index= setter that maps to the GA4 offset parameter. Adding a page query param to the controller, passing it to the builder, and rendering prev/next links in the template is sufficient.


2. Search terms table — /search_terms/website, /search_terms/api

View: app/views/shared/_search_terms_table.html.erb
Data source: GaResponseBuilder#response — same single GA4 page, limit: 10_000

Same pattern as the events table: "Showing X–Y of Z terms" header is already present, no navigation controls. When total unique search terms exceed 10,000 for a hub/date range, later terms are unreachable.

Fix: Same approach as events — page param → start_index offset in the builder.


3. Contributors index — /hubs/:hub_id/contributors

View: app/views/contributors/index.html.erb
Data source: Hub.contributors_item_countHubStats S3 file — all contributors for the hub, no cap

<% @contributors_item_count.each do |c| %>

Currently renders all contributors in one table. Digital Maryland has 2,652; no other hub is close today, but the count will grow over time.

Fix: Slice @contributors_item_count in the controller by page offset. Since the data is already loaded in memory from the S3 cache, this is pure Ruby slicing — no additional data source changes needed. Add a total count and prev/next links.


4. Contributor comparison table — hub show page (async section)

View: app/views/shared/_contributor_comparison.html.erb
Data source: ContributorComparison#totalscontributors_item_count — same S3-sourced array, all contributors

<% @contributor_comparison.totals.each do |contributor, totals| %>

Rendered as an async section via render_async_section. The comparison table also loads GA4 data per contributor via a follow-up fetch — rendering all 2,652 Digital Maryland contributors triggers 2,652 individual GA4 data-cell fetches in the browser.

Fix: Same server-side slicing as #3. The follow-up GA4 fetch should only request data for the visible page of contributors.


Suggested page size

50 rows per page for all four tables is a reasonable starting point and consistent with common dashboard conventions. The page size could be made configurable later.

Notes

  • Tables 1 and 2 can share a single implementation since both use GaResponseBuilder with identical pagination support.
  • Tables 3 and 4 share the same data source (contributors_item_count) and can share a page parameter, but the comparison table's async GA4 fetch needs to be scoped to the current page's contributor slice.
  • The existing CSV download links are unaffected — they use multi_page_response (events/search terms) or the full unsliced array (contributors) and should continue to export all data.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions