0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/spec/lib/admin_dashboard/reports/section_spec.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

106 lines
3.4 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe AdminDashboard::Reports::Section do
fab!(:admin)
let(:guardian) { admin.guardian }
let(:fake_provider) do
Class.new(AdminDashboard::Reports::SourceProvider) do
def self.source_name = "fake"
def self.label = "Fake"
def self.resolve_many(identifiers, guardian:)
identifiers
.reject { |id| id.to_s.start_with?("missing_") }
.each_with_object({}) do |id, hash|
hash[id.to_s] = AdminDashboard::Reports::ResolvedReport.new(
source: "fake",
identifier: id.to_s,
title: "Title for #{id}",
description: "Desc for #{id}",
label: label,
url: "/fake/#{id}",
)
end
end
end
end
let(:plugin) { Plugin::Instance.new }
before do
AdminDashboardReport.delete_all
DiscoursePluginRegistry.register_admin_dashboard_report_source(fake_provider, plugin)
end
after do
DiscoursePluginRegistry._raw_admin_dashboard_report_sources.reject! do |entry|
entry[:value] == fake_provider
end
end
it "returns an empty items list when there are no rows" do
result = described_class.build(guardian: guardian)
expect(result[:items]).to eq([])
end
it "returns items in position order, ignoring insertion order" do
AdminDashboardReport.create!(source: "fake", identifier: "a", position: 1)
AdminDashboardReport.create!(source: "fake", identifier: "b", position: 0)
AdminDashboardReport.create!(source: "fake", identifier: "c", position: 2)
result = described_class.build(guardian: guardian)
expect(result[:items].map { |item| item[:identifier] }).to eq(%w[b a c])
end
it "serializes the resolved metadata along with a composite key" do
AdminDashboardReport.create!(source: "fake", identifier: "x", position: 0)
item = described_class.build(guardian: guardian)[:items].first
expect(item).to eq(
source: "fake",
identifier: "x",
title: "Title for x",
description: "Desc for x",
label: "Fake",
url: "/fake/x",
key: "fake:x",
)
end
it "drops rows whose source has no registered provider" do
AdminDashboardReport.create!(source: "fake", identifier: "good", position: 0)
orphan = AdminDashboardReport.create!(source: "fake", identifier: "becomes_orphan", position: 1)
orphan.update_column(:source, "unregistered_source")
result = described_class.build(guardian: guardian)
expect(result[:items].map { |item| item[:identifier] }).to eq(%w[good])
end
it "drops rows the provider declines to resolve" do
AdminDashboardReport.create!(source: "fake", identifier: "good", position: 0)
AdminDashboardReport.create!(source: "fake", identifier: "missing_one", position: 1)
result = described_class.build(guardian: guardian)
expect(result[:items].map { |item| item[:identifier] }).to eq(%w[good])
end
it "caps at VISIBLE_CAP, dropping the oldest rows by created_at" do
stub_const(AdminDashboardReport, :VISIBLE_CAP, 3) do
5.times do |index|
AdminDashboardReport.create!(
source: "fake",
identifier: "r_#{index}",
position: index,
created_at: index.minutes.ago,
)
end
result = described_class.build(guardian: guardian)
identifiers = result[:items].map { |item| item[:identifier] }
expect(identifiers).to eq(%w[r_0 r_1 r_2])
end
end
end