mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-18 20:40:03 +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>
101 lines
4.4 KiB
Ruby
Vendored
101 lines
4.4 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module AdminDashboard
|
|
module Reports
|
|
# Base class for anything that contributes mountable reports to the
|
|
# customisable Reports section on the new admin dashboard. Subclasses are
|
|
# registered with DiscoursePluginRegistry (or, for built-ins, listed in
|
|
# AdminDashboard::Reports::Registry::CORE_PROVIDERS) and dispatched to by
|
|
# source_name.
|
|
#
|
|
# Every method on the provider is batch-shaped to keep the dashboard
|
|
# render path bounded — never one-by-one resolution.
|
|
class SourceProvider
|
|
# @return [String] the value stored in admin_dashboard_reports.source for
|
|
# rows this provider owns. Examples: "core_report",
|
|
# "data_explorer_query".
|
|
def self.source_name
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# @return [String, nil] a short, translated label rendered as a tag pill
|
|
# in the UI to distinguish this provider's reports from
|
|
# the standard ones. Return nil for the standard/default
|
|
# provider so its reports render without a pill.
|
|
def self.label
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# Cheap metadata resolution. Called server-side on dashboard render and
|
|
# by the Manage Reports modal to populate its enabled list.
|
|
#
|
|
# @param identifiers [Array<String>]
|
|
# @param guardian [Guardian]
|
|
# @return [Hash{String => AdminDashboard::Reports::ResolvedReport}]
|
|
# identifiers that cannot be resolved (deleted, hidden, no
|
|
# permission) are simply absent from the hash.
|
|
def self.resolve_many(identifiers, guardian:)
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# Expensive data fetch. Called by the bulk endpoint to load chart/table
|
|
# content. Providers own their own caching.
|
|
#
|
|
# @param identifiers [Array<String>]
|
|
# @param guardian [Guardian]
|
|
# @param filters [Hash] dashboard-level filter values (date range, etc).
|
|
# @return [Hash{String => Object}] identifier -> report data payload.
|
|
def self.fetch_many(identifiers, guardian:, filters: {})
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# Universe of items of this source. Powers the Manage Reports modal's
|
|
# available list and search filter. Only invoked in admin-only contexts,
|
|
# so implementations should not perform per-user access filtering here.
|
|
#
|
|
# Implements keyset pagination so the controller can merge every
|
|
# provider into one globally title-sorted stream without loading the
|
|
# whole universe. Items must come back sorted by
|
|
# [title.downcase, key] (key being "source:identifier") and limited to
|
|
# those strictly after `after`.
|
|
#
|
|
# @param search [String, nil] optional name/description filter.
|
|
# @param after [Hash, nil] cursor of the last item from the previous
|
|
# page: { title:, key: }. nil for the first page.
|
|
# @param limit [Integer, nil] maximum number of items to return.
|
|
# @return [Array<AdminDashboard::Reports::ResolvedReport>]
|
|
def self.list_all(search: nil, after: nil, limit: nil)
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# Sort + keyset-filter an in-memory set of resolved reports for
|
|
# `list_all`. Suitable for providers small enough to materialise their
|
|
# whole set (e.g. core reports); SQL-backed providers should push the
|
|
# keyset into the query instead.
|
|
def self.seek(reports, after:, limit:)
|
|
reports = reports.sort_by { |report| sort_key(report) }
|
|
if after
|
|
threshold = [after[:title].to_s.downcase, after[:key].to_s]
|
|
reports = reports.select { |report| (sort_key(report) <=> threshold) == 1 }
|
|
end
|
|
limit ? reports.first(limit) : reports
|
|
end
|
|
|
|
def self.sort_key(report)
|
|
[report.title.to_s.downcase, report.key]
|
|
end
|
|
|
|
# Identifiers from the input that the guardian is allowed to mount /
|
|
# interact with. Default implementation is a subset of `resolve_many`
|
|
# keys, which works for any provider whose access check is identical
|
|
# to its resolution check.
|
|
#
|
|
# @param identifiers [Array<String>]
|
|
# @param guardian [Guardian]
|
|
# @return [Set<String>]
|
|
def self.accessible_ids(identifiers, guardian:)
|
|
resolve_many(identifiers, guardian: guardian).keys.to_set
|
|
end
|
|
end
|
|
end
|
|
end
|