0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/app/services/admin_dashboard_section_configuration.rb
Krzysztof Kotlarek 1101080b10
FEATURE: Add a Support section to the admin dashboard0 (#41359)
Previously, the redesigned admin dashboard couldn't show how a community
handles its support topics, and plugins had no way to contribute their
own dashboard sections — only individual highlight KPIs.
This change adds a register_admin_dashboard_section plugin API and uses
it in discourse-solved to add a Support section for communities with at
least one category where accepted answers are enabled.

**Core: register_admin_dashboard_section**

A new plugin API (mirroring the existing highlight-KPI registration)
lets a plugin register a dashboard section with an id, an optional
enabled gate, and a data loader; the JS
api.registerAdminDashboardSection(id, component) supplies the matching
client component. The section configuration, parallel loader, and
dashboard component render registered sections dynamically alongside the
core ones.

Also in the core commit: the dev database pool is raised so the
dashboard's parallel section loader isn't starved of connections with an
extra section, and a stray db-section__row-block divider on mobile is
fixed.

**Support section (discourse-solved)**

For the selected period, scoped to categories where "Allow topic owner
and staff to mark a reply as the solution" is enabled, the section
shows:

- KPIs with previous-period deltas: resolution rate (drill-through to
the accepted_solutions report), staff involvement, and average
first-reply time.
- Topic outcomes - resolved / in progress / unanswered.
- Who's answering - a breakdown of repliers by trust level and staff.
- Response time distribution - first-reply times bucketed into <1h,
1–4h, 4–24h, >24h, with a faster/slower trend.
- A category filter when more than one support category exists.

Screenshots:
<img width="804" height="748" alt="Screenshot 2026-07-02 at 10 58 13 am"
src="https://github.com/user-attachments/assets/8e938a5e-93df-446f-885c-3d779431ab9b"
/>
<img width="396" height="789" alt="Screenshot 2026-07-02 at 10 58 01 am"
src="https://github.com/user-attachments/assets/9a9a171d-42d9-482c-950c-5daad055a746"
/>

---------

Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
2026-07-06 09:31:51 +08:00

70 lines
2 KiB
Ruby
Vendored

# frozen_string_literal: true
class AdminDashboardSectionConfiguration
KNOWN_SECTIONS = %w[highlights reports traffic engagement search].freeze
def self.available_plugin_section_ids
DiscoursePluginRegistry
.admin_dashboard_sections
.select { |s| !s[:enabled].respond_to?(:call) || s[:enabled].call }
.map { |s| s[:id] }
end
def self.all_known_section_ids
KNOWN_SECTIONS + available_plugin_section_ids
end
def self.sections
known = all_known_section_ids
persisted =
AdminDashboardSection
.where(section_id: known)
.order(:position)
.pluck(:section_id, :visible)
.map { |id, visible| { id:, visible: } }
not_persisted = (known - persisted.map { |s| s[:id] }).map { |id| { id:, visible: true } }
persisted + not_persisted
end
def self.visible_section_ids
sections.select { |s| s[:visible] }.map { |s| s[:id] }
end
def self.update(input_sections, actor:)
sanitized =
Array(input_sections)
.filter_map do |s|
attrs = (s.respond_to?(:to_unsafe_h) ? s.to_unsafe_h : s.to_h).symbolize_keys
id = attrs[:id].to_s
next if all_known_section_ids.exclude?(id)
{ id:, visible: ActiveModel::Type::Boolean.new.cast(attrs[:visible]) }
end
.uniq { |s| s[:id] }
present_ids = sanitized.map { |s| s[:id] }
missing = sections.reject { |s| present_ids.include?(s[:id]) }
ordered = sanitized + missing
now = Time.zone.now
rows =
ordered.each_with_index.map do |section, index|
{
section_id: section[:id],
position: index,
visible: section[:visible],
created_at: now,
updated_at: now,
}
end
AdminDashboardSection.upsert_all(rows, unique_by: :section_id)
StaffActionLogger.new(actor).log_custom(
"update_dashboard_sections",
layout: ordered.map { |s| "#{s[:id]}:#{s[:visible] ? "visible" : "hidden"}" }.join(", "),
)
sections
end
end