discourse/lib/admin_dashboard/reports/section.rb
chapoi 9d6605bf0d
UX: admin dashboard design fixes (#40476)
This branch is a round of design and interaction fixes for the
redesigned admin dashboard:

### Engagement

* Replaces the KpiTile-based tiles with a Metrics (value, label +
tooltip, and delta) so the headline reads as a row of metrics rather
than cards.
* Formats percentage KPIs (e.g. dau_mau) with a % suffix and shows a
neutral "stable" pill when there's no meaningful change.
* Swaps SearchAdvancedCategoryChooser for a plain CategoryChooser with
an "All categories" item, so the single-category filter no longer
renders as a removable multi-select chip with a clear "×".

### Activity by category

* Makes every column in the activity table fully sortable, confirm with
how it's displayed on underlying report page.
* Fixes the table overflowing on mobile by adding min-width: 0 to the
flex row-block and wrapping the table in a horizontally scrollable
container.

### Report cards

Depends also on https://github.com/discourse/discourse/pull/40404 !

- Turns each report card title into a link to its report.
- Renders the provider label as a per-source pill (with a --source
modifier). The standard/core provider now returns nil for its label so
its reports render without a pill — labels exist only to distinguish
plugin-contributed sources.
- Drops the inline remove "×" button and the show_labels plumbing.

### Highlights & misc

- Removes the "vs prior" comparison footer from the highlights section.
- Updates tooltip icons from circle-question to far-circle-question and
trims "in this period" wording from KPI tooltip copy.

### General

- Numerous responsive/mobile styling fixes across admin_dashboard.scss
(sticky header, metrics, row blocks)
- i18n updates for consistency and conciseness
- Remove floating of data/custom buttons on mobile: this isn't a
standard pattern so holding off on this
- Abstract stable and delta classes

### Looks like
| BC | AC |
|--------|--------|
| <img width="1769" height="2957" alt="image"
src="https://github.com/user-attachments/assets/d60a8eeb-e2ed-4929-9f29-f6c7062fc66f"
/> | <img width="1769" height="2957" alt="image"
src="https://github.com/user-attachments/assets/188403a7-dc31-4b12-bf26-0455cd2a272b"
/> |
| <img width="543" height="3120" alt="image"
src="https://github.com/user-attachments/assets/87035498-fa8a-496c-b721-b7cb7bc44a43"
/>| <img width="391" height="3336" alt="image"
src="https://github.com/user-attachments/assets/a0173425-e60e-4b21-bdcb-5e6c642796cd"
/> |

---------

Co-authored-by: Krzysztof Kotlarek <kotlarek.krzysztof@gmail.com>
2026-06-03 13:35:05 +02:00

63 lines
1.7 KiB
Ruby
Vendored

# frozen_string_literal: true
module AdminDashboard
module Reports
class Section
def self.build(guardian:, search: nil)
new(guardian: guardian, search: search).build
end
def initialize(guardian:, search: nil)
@guardian = guardian
@search = search.presence
end
def build
items = visible_items.map { |_row, resolved| serialize(resolved) }
items = filter_by_search(items) if @search
{ items: items }
end
private
attr_reader :guardian
def visible_items
rows = AdminDashboardReport.order(created_at: :desc).to_a
resolved_by_row_id = resolve_rows(rows)
# When more rows resolve than VISIBLE_CAP allows, the older overflow
# is hidden — clip by created_at recency first, then re-sort the
# survivors by the admin's chosen position.
rows
.filter_map { |row| (obj = resolved_by_row_id[row.id]) && [row, obj] }
.first(AdminDashboardReport::VISIBLE_CAP)
.sort_by { |row, _obj| row.position }
end
def resolve_rows(rows)
per_source =
AdminDashboard::Reports::Registry.dispatch_per_source(rows) do |provider, group|
provider.resolve_many(group.map(&:identifier), guardian: guardian)
end
rows.each_with_object({}) do |row, resolved|
resolved[row.id] = per_source.dig(row.source, row.identifier)
end
end
def serialize(resolved)
resolved.to_h
end
def filter_by_search(items)
query = @search.downcase
items.select do |item|
item[:title].to_s.downcase.include?(query) ||
item[:description].to_s.downcase.include?(query)
end
end
end
end
end