mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
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>
106 lines
3.4 KiB
Ruby
Vendored
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
|