Skip to main content

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:

  1. Dashboard-specific permissions: Uses scope_name from permissions with matching action_name and ar_model_name
  2. Policy scope fallback: Falls back to CmAdmin::{Model}PolicyIndexScope if no dashboard-specific scope
  3. 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​

  1. Use descriptive method names: total_active_users_metric instead of user_count
  2. Handle empty data gracefully: Return appropriate defaults for empty datasets
  3. Apply filters consistently: Use the same filter logic across related widgets
  4. Optimize queries: Use includes, joins, and select to optimize database queries
  5. Cache expensive calculations: Use Rails caching for complex metrics
  6. 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.