CmAdmin Interactive Dashboard System
Overviewâ
The CmAdmin Interactive Dashboard System allows you to create fully interactive, namespaced dashboards with dynamic widgets. These dashboards can be global or associated with specific models and can be embedded in model views or other sections.
This guide will walk you through setting up and using the dashboard system with the new dashboard helper functionality and permission scoping.
Setupâ
Database Setupâ
First, you need to run the migration to create the necessary tables:
rails db:migrate
Dashboard Helper Systemâ
Creating Dashboard Helpersâ
Create custom dashboard helper methods in /app/helpers/cm_admin/dashboard_helper.rb:
module CmAdmin
module DashboardHelper
# Dashboard helper methods receive (scoped_ar_object, scoped_filters)
# scoped_ar_object is already permission-scoped based on user permissions
def total_users_metric(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object # Already permission-scoped
# Apply dashboard filters
if scoped_filters[:single_select].present?
scoped_filters[:single_select].each do |key, value|
next if value.blank?
base_query = base_query.where(key => value)
end
end
if scoped_filters[:search].present?
search_term = scoped_filters[:search]
base_query = base_query.where("email ILIKE ? OR first_name ILIKE ?",
"%#{search_term}%", "%#{search_term}%")
end
base_query.count
end
def users_by_role_chart_data(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object
# Apply filters similar to above
# ... filter logic ...
base_query.joins(:cm_role).group('cm_roles.name').count
end
end
end
Permission Scopingâ
Dashboard helpers automatically receive permission-scoped data:
- Dashboard-specific permissions: Uses
scope_namefrom permissions with matchingaction_nameandar_model_name - Policy scope fallback: Falls back to
CmAdmin::{Model}PolicyIndexScopeif no dashboard-specific scope - Filter integration: Dashboard filters are automatically scoped and passed to helper methods
Creating Dashboardsâ
Global Dashboardsâ
Global dashboards are not associated with any specific model:
dashboard "Admin Dashboard" do
box "User Statistics" do
metric value: :total_users_metric, title: "Total Users"
chart "Users by Role", type: :pie, data: :users_by_role_chart_data
table "Recent Users", data: :recent_users_table_data
end
box "System Statistics" do
metric value: -> { Rails.cache.stats[:hits] }, title: "Cache Hits"
chart "Daily Signups", type: :line, data: :daily_signups_chart_data
end
end
Model-Associated Dashboardsâ
Associate dashboards with specific models:
# app/models/concerns/cm_admin/student.rb
module CmAdmin
module Student
extend ActiveSupport::Concern
included do
cm_admin do
actions only: []
set_icon 'fa fa-graduation-cap'
cm_dashboard :dashboard_student_type,
display_name: 'Dashboard',
page_title: 'Dashboard',
page_description: 'Student Dashboard' do
# Permissions / access control to be wired into your system:
# - Full Access
# - User Office Only
# - Assignee Only
# Filters - These will be automatically passed to dashboard helpers
filter :office_id,
:multi_select,
helper_method: :select_options_for_office,
placeholder: 'Office',
display_name: 'Office',
active_by_default: true
filter :assigned_to_id,
:multi_select,
helper_method: :select_options_for_lead_assignees,
placeholder: 'Assignee',
display_name: 'Assignee',
active_by_default: true
filter :application_coordinator_id,
:multi_select,
helper_method: :select_options_for_application_coordinators,
placeholder: 'Application Coordinator',
display_name: 'Application Coordinator',
active_by_default: true,
filter_with: :with_application_coordinator_ids
# Row 1 â 3 metric cards (Total Leads | Total Prospects | Total Applications)
row do
cm_section '' do
metric_card :total_leads_metric,
title: 'Total Leads'
end
cm_section '' do
metric_card :total_prospects_metric,
title: 'Total Prospects'
end
cm_section '' do
metric_card :total_applications_metric,
title: 'Total Applications'
end
end
# Charts â Layout: 2 columns, Pie Chart for all
# Row 2 â Leads by Status | Prospects by Status
row do
cm_section '' do
chart :leads_by_status_chart_data,
title: 'Leads by Status',
chart_type: :pie_chart
end
end
row do
cm_section '' do
chart :prospects_by_status_chart_data,
title: 'Prospects by Status',
chart_type: :pie_chart
end
end
# Row 3 â Applications by Status (single column)
row do
cm_section '' do
chart :applications_by_status_chart_data,
title: 'Applications by Status',
chart_type: :pie_chart
end
end
end
cm_index do
page_title 'Students'
# These same filters will be used for dashboard filtering
filter %i[first_name last_name email], :search, placeholder: 'Search'
filter :office_id, :multi_select, helper_method: :select_options_for_office
filter :assigned_to_id, :multi_select, helper_method: :select_options_for_lead_assignees
sort_column :created_at
sort_direction :desc
column :full_name
column :email
column :office_name, header: 'Office'
column :status
column :created_at, field_type: :date, format: '%d %b, %Y'
end
end
end
end
end
Corresponding Dashboard Helper Methodsâ
Create the dashboard helper methods in /app/helpers/cm_admin/dashboard_helper.rb:
module CmAdmin
module DashboardHelper
# Metric card methods - receive permission-scoped data and filters
def total_leads_metric(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object.where(status: 'lead')
apply_dashboard_filters(base_query, scoped_filters).count
end
def total_prospects_metric(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object.where(status: 'prospect')
apply_dashboard_filters(base_query, scoped_filters).count
end
def total_applications_metric(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object.where(status: 'application')
apply_dashboard_filters(base_query, scoped_filters).count
end
# Chart data methods
def leads_by_status_chart_data(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object.where(status: 'lead')
filtered_query = apply_dashboard_filters(base_query, scoped_filters)
filtered_query.group(:sub_status).count
end
def prospects_by_status_chart_data(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object.where(status: 'prospect')
filtered_query = apply_dashboard_filters(base_query, scoped_filters)
filtered_query.group(:sub_status).count
end
def applications_by_status_chart_data(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object.where(status: 'application')
filtered_query = apply_dashboard_filters(base_query, scoped_filters)
filtered_query.group(:application_status).count
end
private
# Helper method to apply dashboard filters consistently
def apply_dashboard_filters(base_query, scoped_filters)
return base_query if scoped_filters.blank?
# Apply multi-select filters
if scoped_filters[:multi_select].present?
scoped_filters[:multi_select].each do |key, values|
next if values.blank?
case key.to_s
when 'office_id'
base_query = base_query.where(office_id: values)
when 'assigned_to_id'
base_query = base_query.where(assigned_to_id: values)
when 'application_coordinator_id'
base_query = base_query.where(application_coordinator_id: values)
end
end
end
# Apply search filters
if scoped_filters[:search].present?
search_term = scoped_filters[:search]
base_query = base_query.where(
"first_name ILIKE ? OR last_name ILIKE ? OR email ILIKE ?",
"%#{search_term}%", "%#{search_term}%", "%#{search_term}%"
)
end
# Apply date filters
if scoped_filters[:date].present?
scoped_filters[:date].each do |key, date_range|
next if date_range.blank?
base_query = base_query.where(key => date_range)
end
end
base_query
end
end
end
Widget Typesâ
Metric Cardsâ
Display single numeric values:
metric :total_users_metric, title: "Total Users", description: "All registered users"
Chartsâ
Display data visualizations:
# Pie Chart
chart :users_by_role_chart_data, type: :pie, title: "Users by Role"
# Line Chart
chart :daily_signups_chart_data, type: :line, title: "Daily Signups"
# Bar Chart
chart :monthly_stats_chart_data, type: :bar, title: "Monthly Stats"
Tablesâ
Display tabular data:
table "Recent Users", data: :recent_users_table_data
Advanced Featuresâ
Filter Integrationâ
Dashboard filters are automatically passed to helper methods:
def filtered_users_metric(scoped_ar_object, scoped_filters = {})
base_query = scoped_ar_object
# Single select filters
if scoped_filters[:single_select]&.dig(:cm_role_id).present?
base_query = base_query.where(cm_role_id: scoped_filters[:single_select][:cm_role_id])
end
# Date filters
if scoped_filters[:date]&.dig(:created_at).present?
date_range = scoped_filters[:date][:created_at]
base_query = base_query.where(created_at: date_range)
end
# Search filters
if scoped_filters[:search].present?
search_term = scoped_filters[:search]
base_query = base_query.where("email ILIKE ?", "%#{search_term}%")
end
base_query.count
end
Permission-Based Scopingâ
Dashboard helpers automatically respect user permissions:
# This helper will only see data the user has permission to access
def department_users_metric(scoped_ar_object, scoped_filters = {})
# scoped_ar_object is already filtered based on:
# 1. Dashboard-specific permission scope (if configured)
# 2. Model policy scope (fallback)
scoped_ar_object.where(department: 'Engineering').count
end
Error Handlingâ
Dashboard helpers include built-in error handling:
def safe_metric_calculation(scoped_ar_object, scoped_filters = {})
begin
# Your calculation logic
scoped_ar_object.complex_calculation
rescue StandardError => e
Rails.logger.error "Dashboard metric error: #{e.message}"
0 # Return safe default value
end
end
Best Practicesâ
- Use descriptive method names:
total_active_users_metricinstead ofuser_count - Handle empty data gracefully: Return appropriate defaults for empty datasets
- Apply filters consistently: Use the same filter logic across related widgets
- Optimize queries: Use includes, joins, and select to optimize database queries
- Cache expensive calculations: Use Rails caching for complex metrics
- Test with different permission levels: Ensure widgets work for all user types
Security Considerationsâ
- Dashboard helpers automatically receive permission-scoped data
- Filters are validated against dashboard configuration
- All database queries respect user access controls
- Error messages don't expose sensitive information
This updated dashboard system provides a secure, flexible, and powerful way to create interactive dashboards with proper permission handling and filter integration.