How to create a dashboard with Dashing and integrate with Drupal data

Show off you Drupal data on your TV on the wall

How to create a dashboard with Dashing and integrate with Drupal data

What is Dashing

Dashing is a Sinatra (think ruby but not rails) based framework that lets you build dashboards. It was originally made by the guys at Shopify for displaying custom dashboards on TVs around the office.

Why use Dashing

Dashing makes your life easier, freeing you up to focus on more important things - like what data you are looking to display, and what time of widget you want to use.

Features of dashing:

  • Opensource (MIT license)
  • Widgets are tiny and encapsulated, made with SASS, HTML and coffeescript
  • The dashboard itself is simply HTML and SASS, meaning you can theme and style it to suit your needs
  • Comes bundled with several powerful widgets
  • Widgets are powered by simply data bindings (powered by batman.js)
  • Push and pull methods available to each widget
  • Pull jobs can be configured to run in the background on a set interval (e.g. every 30 seconds, poll Chartbeat for new data)
  • Layout is drag & drop interface for re-arranging widgets

Why not make a dashboard in Drupal

There are several dashboard modules in Drupal, and yes you can go to a bit lot of trouble and re-create the power of Dashing in Drupal, but there is no need.

Dashing is great at what it does, and it only does one thing.

Another advantage is that you can query other sources of data - e.g. Google Analytics or MailChimp and display metrics from those applications on your dashboard.

A really great example (including code) and can found over at http://derekweitzel.blogspot.co.nz/2014/03/a-hcc-dashboard-with-osg-accounting.html

Installation of Dashing on Ubuntu 14.04

The only real requirement is ruby 1.9+ (this comes by default in Ubunty 14.04, in Ubuntu 12.04 you need to install ruby-1.9 explicitly)

sudo apt-get install ruby ruby-dev nodejs g++ bundler
sudo gem install dashing

you can create a new dashboard with

dashing new awesome_dashboard
cd awesome_dashboard
bundle

You start the application by

sudo dashing start

You now have a dashboard on http://localhost:3030 ready to go

Creating a new Dashing widget

There are already a few tutorials online, the best of which is probably just the existing suite of widgets available.

Here we will go through a simple example where we want to graph the of pieces of content in the “Needs review” state (provided by Workbench moderation) in Drupal. This serves as a mini-todo list for content authors, as ideally this number should be as low as possible.

In this example, we are re-cycling the “List” widget.

Place an instance of the “List” widget on a dashboard - e.g. sample.erb

    <li data-row="1" data-col="1" data-sizex="1" data-sizey="1">
      <div data-id="newsarticlesreview" data-view="List" data-unordered="true" data-title="News articles in 'Needs review'"></div>
      <i class="icon-check-sign icon-background"></i>
    </li>

Create a new job to poll for data

Create a new file in jobs/newsarticlesreview.rb, and place:

#!/bin/env ruby
# encoding: utf-8

require 'net/http'
require 'uri'
require 'json'

# TODO replace with a real production host
server = "https://localhost"

SCHEDULER.every '30s', :first_in => 0 do |job|

  url = URI.parse("#{server}/api/content/dashboard?token=FawTP0fJgSagS1aYcM2a5Bx-MaJI8Y975NwYWP12B0E")
  http = Net::HTTP.new(url.host, url.port)
  http.use_ssl = (url.scheme == 'https')
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  response = http.request(Net::HTTP::Get.new(url.request_uri))

  # Convert to JSON
  j = JSON[response.body]

  # Send the joke to the text widget
  review_content = {}
  review_content['en'] = { label: 'English', value: j['en']['news_article']['needs_review'] }
  review_content['mi'] = { label: 'Māori', value: j['mi']['news_article']['needs_review'] }
  send_event("newsarticlesreview", { items: review_content.values })
end

Create a Drupal data source

Now we need to feed the Dashing request with a Drupal API. I have chosen to do all of these custom, as they are straight forward. In theory you could also craft these with the services module as well.

Create hook_menu() entry

/**
 * Implements hook_menu().
 */
function CUSTOM_menu() {
  // Dashboard API requests. Protected using a token.
  // e.g. api/content/dashboard?token=FawTP0fJgSagS1aYcM2a5Bx-MaJI8Y975NwYWP12B0E
  $items['api/content/dashboard'] = array(
    'title' => 'Content types broken down by workflow status',
    'page callback' => 'CUSTOM_content_dashboard',
    'access callback' => 'CUSTOM_dashboard_api_access',
    'access arguments' => array('api/content/dashboard'),
    'type' => MENU_CALLBACK,
    'file' => 'CUSTOM.dashboard.inc',
  );
  return $items;
}

Here we define a custom route, and declare the access callback. The access callback is special as it needs to ensure that access is restricted to only requests with a special token. The token being created from a hash of the Drupal salt combined with the current path and private key, and base64 encoded (much like drupal_get_token() without the session ID check).

/**
 * Access callback to the dashboard API endpoints. These are protected by a
 * token.
 *
 * @param String $path
 *   The path that is being requested.
 *
 * @return Boolean
 *   Whether or not the use has access to the callback.
 */
function CUSTOM_dashboard_api_access($path) {
  global $is_https;

  // HTTPS only.
  $only_allow_https = (bool) variable_get('api_https_only', 1);
  if ($only_allow_https && !$is_https) {
    return FALSE;
  }

  // Only allow get requests.
  if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    return FALSE;
  }

  // Check token is correct.
  $params = drupal_get_query_parameters();
  if (!isset($params['token']) || empty($params['token'])) {
    return FALSE;
  }
  $valid_token = CUSTOM_token_validation($path);
  if ($params['token'] !== $valid_token) {
    return FALSE;
  }

  return TRUE;
}

And finally the data for the callback.

/**
 * Gathers current content statistics from Drupal, including the amount of
 * content broken down by a) content type, b) workflow state, c) status.
 *
 * @return JSON
 */
function CUSTOM_content_dashboard() {
  $output = array();

  $languages = language_list('enabled');
  $types = node_type_get_types();

  // Workbench states.
  foreach ($languages[1] as $langcode => $language) {
    foreach ($types as $machine_name => $type) {
      // Workbench moderation in use (remove this if you do not have the module).
      if (workbench_moderation_node_type_moderated($machine_name)) {
        $results = db_query("SELECT COUNT(n.vid) AS total, w.state
                             FROM {node} n
                             JOIN {workbench_moderation_node_history} w
                               ON w.vid = n.vid
                             WHERE n.type = :type
                               AND n.language = :lang
                               AND w.current = 1
                             GROUP BY w.state", array(':type' => $machine_name, ':lang' => $langcode))->fetchAllAssoc('state');

        foreach ($results as $state => $result) {
          $output[$langcode][$machine_name][$state] = (int) $result->total;
        }
      }

      // No workbench moderation for this content type, use the status column.
      else {
        $results = db_query("SELECT COUNT(n.nid) AS total, n.status
                             FROM {node} n
                             WHERE n.type = :type
                               AND n.language = :lang
                             GROUP BY n.status", array(':type' => $machine_name, ':lang' => $langcode))->fetchAllAssoc('status');

        foreach ($results as $status => $result) {
          if ($status == NODE_PUBLISHED) {
            $status = 'published';
          }
          else {
            $status = 'unpublished';
          }
          $output[$langcode][$machine_name][$status] = (int) $result->total;
        }
      }
    }
  }

  drupal_json_output($output);
  drupal_exit();
}

And there you have it. Note the above code relies on workbench moderation being present, if you do not have it, simply remove the section of the code that is relevant. Note that the API response is considerably more complex and complete than the example calls for, but this just means you can display more data in more ways on your dashboard.

Here is the finished product:

Extra for experts

Create an init.d script for dashing, here is a good starter.

Comments

Let me know if you have completed (or started) a recent project to visual data from Drupal (or related third party applications) and your experiences there. Pictures are always welcome.