0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-12 05:37:26 +08:00
discourse/app/services/admin_dashboard_engagement.rb
Osama Sayegh abcda0f58a
FEATURE: Persist "Activity by category" dashboard filter selection (#41758)
Admins can choose which categories appear in the admin dashboard's
"Activity by category" section. That choice now sticks for everyone
across refreshes instead of resetting to the defaults on each visit.

The selection is stored through a generic per-section settings storage
system. Any dashboard section can declare the settings it supports, so
future section configuration and filters can reuse the same storage,
endpoint, and validation without new plumbing.
2026-07-17 10:54:28 +03:00

153 lines
4.5 KiB
Ruby
Vendored

# frozen_string_literal: true
class AdminDashboardEngagement
include AdminDashboardKpis
DEFAULT_RANGE_DAYS = 30
KPI_REPORTS = {
dau_mau: "dau_by_mau",
daily_engaged_users: "daily_engaged_users",
new_signups: "signups",
}.freeze
def self.build(start_date:, end_date:, current_user: nil)
new(start_date: start_date, end_date: end_date, current_user: current_user).build
end
def initialize(start_date:, end_date:, current_user: nil)
@start_date = parse_date(start_date) || DEFAULT_RANGE_DAYS.days.ago.beginning_of_day
@end_date = parse_date(end_date)&.end_of_day || Time.zone.now.end_of_day
@current_user = current_user
end
def build
kpis = build_kpis
{
kpis: kpis,
headline: build_headline(kpis),
trust_level_pipeline: build_trust_level_pipeline,
posters: build_posters,
activity_by_category: build_activity_by_category,
}
end
private
attr_reader :start_date, :end_date, :current_user
def parse_date(value)
return nil if value.blank?
Time.zone.parse(value.to_s)&.beginning_of_day
rescue ArgumentError, TypeError
nil
end
def build_kpis
KPI_REPORTS.filter_map { |type, report| build_kpi(type, report) }
end
HEADLINE_KEYS = {
healthy_growth: "admin.dashboard.sections.engagement.headline.healthy_growth",
declining: "admin.dashboard.sections.engagement.headline.declining",
engaged_but_shrinking: "admin.dashboard.sections.engagement.headline.engaged_but_shrinking",
growing_but_distracted: "admin.dashboard.sections.engagement.headline.growing_but_distracted",
mixed: "admin.dashboard.sections.engagement.headline.mixed",
no_signal: "admin.dashboard.sections.engagement.headline.no_signal",
}.freeze
def build_headline(kpis)
stickiness = sign_of(kpi_change(kpis, :dau_mau))
signups = sign_of(kpi_change(kpis, :new_signups))
engaged = sign_of(kpi_change(kpis, :daily_engaged_users))
key =
if [stickiness, signups, engaged].all?(&:zero?)
:no_signal
elsif stickiness >= 0 && signups >= 0 && engaged >= 0
:healthy_growth
elsif stickiness <= 0 && signups <= 0 && engaged <= 0
:declining
elsif stickiness >= 0 && (signups < 0 || engaged < 0)
:engaged_but_shrinking
elsif stickiness < 0 && signups > 0
:growing_but_distracted
else
:mixed
end
{ key: HEADLINE_KEYS[key] }
end
def kpi_change(kpis, type)
kpis.find { |k| k[:type] == type }&.dig(:percent_change)
end
def sign_of(value)
return 0 if value.nil? || value.zero?
value.positive? ? 1 : -1
end
def build_trust_level_pipeline
args = { start_date: start_date, end_date: end_date }
report = Report.find_cached("trust_level_pipeline", args)
if report.nil?
report = Report.find("trust_level_pipeline", args)
Report.cache(report) if report && report.error.blank?
end
return nil if report.nil? || report_error?(report)
{
rows: report_data(report),
trend: report_prev_period(report),
total_members: report.is_a?(Hash) ? report[:total] : report.total,
}
end
def build_posters
args = { start_date: start_date, end_date: end_date, current_user: current_user }
report = Report.find_cached("posters_by_member_type", args)
if report.nil?
report = Report.find("posters_by_member_type", args)
Report.cache(report) if report && report.error.blank?
end
return nil if report.nil? || report_error?(report)
{ rows: report_data(report), total: report.is_a?(Hash) ? report[:total] : report.total }
end
def build_activity_by_category
args = { start_date: start_date, end_date: end_date, current_user: current_user }
selected_category_ids =
AdminDashboardSectionConfiguration.settings_for("engagement").dig(
"activity_by_category",
"category_ids",
)
args[:filters] = { category_ids: selected_category_ids } if selected_category_ids.present?
report = Report.find_cached("activity_by_category", args)
if report.nil?
report = Report.find("activity_by_category", args)
Report.cache(report) if report && report.error.blank?
end
return nil if report.nil? || report_error?(report)
{
rows: report_data(report),
total: report.is_a?(Hash) ? report[:total] : report.total,
category_ids: visible_category_ids(selected_category_ids),
}
end
def visible_category_ids(category_ids)
return category_ids if category_ids.blank?
Category.secured(Guardian.new(current_user)).in_order_of(:id, category_ids).pluck(:id)
end
end