discourse/app/controllers/admin/dashboard_controller.rb
Natalie Tay 37e13f0afc
FEATURE: Allow admins to configure sections in the experimental admin dashboard (#39964)
Allow admins to configure which sections they want to see.

A few tasks in this commit:
- rename the "Customise" menu to "Configure" and removes the calendar
icon from the Custom date range option
- implements the Configure menu:
  - toggle section visibility
  - drag-and-drop reorder on desktop, up/down arrows on mobile
  - empty state when all sections are off
- persists configuration site-wide on menu close via
`admin_dashboard_sections` site setting
  - admins and moderators can view, but admins write and moderators read
- `AdminDashboardSectionConfiguration` owns configuration, so storage
can change later if we move to per-user
- `PUT /admin/dashboard/configuration.json` is admin-only (mentioned
above)
  - audit log (`SiteSetting.set_and_log`)
- the backend filters `sections` payload to only the visible-and-ordered
list, so mods render the layout admins set without seeing the
configuration meta
    - hidden sections skip their server-side data computation
2026-05-13 18:03:39 +08:00

105 lines
2.9 KiB
Ruby
Vendored

# frozen_string_literal: true
class Admin::DashboardController < Admin::StaffController
def index
if SiteSetting.dashboard_improvements
visible_ids = AdminDashboardSectionConfiguration.visible_section_ids
data = { sections: visible_ids.map { |id| { id: id, data: section_data(id) } } }
if current_user.admin?
data[:configuration] = { sections: AdminDashboardSectionConfiguration.sections }
end
else
data = AdminDashboardIndexData.fetch_cached_stats
if SiteSetting.version_checks?
data.merge!(version_check: DiscourseUpdates.check_version.as_json)
end
end
render json: data
end
def update_configuration
sections = params.permit(sections: %i[id visible])[:sections] || []
AdminDashboardSectionConfiguration.update(sections, actor: current_user)
head :no_content
end
def moderation
end
def security
end
def reports
end
def general
render json: AdminDashboardGeneralData.fetch_cached_stats
end
def problems
ProblemCheck.realtime.run_all
render json: { problems: serialize_data(AdminNotice.problem.all, AdminNoticeSerializer) }
end
def new_features
force_refresh = params[:force_refresh] == "true"
if force_refresh
RateLimiter.new(
current_user,
"force-refresh-new-features",
5,
1.minute,
apply_limit_to_staff: true,
).performed!
end
new_features = DiscourseUpdates.new_features(force_refresh:)
new_features_with_permanent_uc =
DiscourseUpdates.merge_new_features_with_upcoming_changes(
new_features&.map { |item| item.symbolize_keys } || [],
)
if current_user.admin? && most_recent = new_features_with_permanent_uc&.first
DiscourseUpdates.bump_last_viewed_feature_date(current_user.id, most_recent[:created_at])
end
data = {
new_features: new_features_with_permanent_uc,
has_unseen_features: DiscourseUpdates.has_unseen_features?(current_user.id),
release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"],
}
mark_new_features_as_seen
render json: data
end
def toggle_feature
UpcomingChanges::Toggle.call(service_params) do
on_success { render(json: success_json) }
on_failure { render(json: failed_json, status: :unprocessable_entity) }
on_failed_policy(:current_user_is_admin) { raise Discourse::InvalidAccess }
on_failed_policy(:setting_is_available) { raise Discourse::InvalidAccess }
on_failed_contract do |contract|
render(json: failed_json.merge(errors: contract.errors.full_messages), status: :bad_request)
end
end
end
private
def section_data(id)
case id
when "highlights"
AdminDashboardHighlights.build(start_date: params[:start_date], end_date: params[:end_date])
end
end
def mark_new_features_as_seen
DiscourseUpdates.mark_new_features_as_seen(current_user.id)
end
end