Wednesday, 8 June 2011

Automatic Dashboard Refresh

This weeks post concerns a piece of functionality that I've been struggling with for ages, and I'm quite pleased with myself to have it working.  It's a feature that is requested time and time again by clients - a dashboard that refreshes itself.

The sticking point for this has always been that a Visualforce component is required to add logic to a dashboard, but as Visualforce pages are contained in an iframe served from a different host to the dashboard page, it isn't possible to navigate to the Refresh button and click it programmatically - the browser blocks this as a cross-site scripting attack.  There is another technique to embed Javascript into a Salesforce page via a sidebar component, but dashboard pages don't have a sidebar so that's out too.

Thus I needed to take an alternative approach to this and started to investigate effecting the refresh server side from Apex code.

The first thing that I decided I needed to get at was the ID of the dashboard itself.  This is encoded in the dashboard URL, an example from my dev org is : https://na6.salesforce.com/01Z80000000lf7nEAA, where 01Z80000000lf7nEAA is the ID.  Once again though, it isn't possible to interrogate the URL of the main window from a Visualforce component due to Cross Site Scripting.  Thinking back to my web development days at Olive Systems Limited, it struck me that the HTTP request headers for the Visualforce component might provide some useful information.  I therefore created a Visualforce page with a controller that traversed the HTTP headers and output the details to the System log:

Map headers=ApexPages.currentPage().getHeaders();
for (String key : headers.keySet())
{
   System.debug('### ' + key + '=' + headers.get(key));
}

I then added this page to my dashboard which gave the following output in the system log:

14:50:34.055 (55491000)|USER_DEBUG|[20]|DEBUG|### Accept=application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
14:50:34.055 (55645000)|USER_DEBUG|[20]|DEBUG|### Accept-Charset=ISO-8859-1,utf-8;q=0.7,*;q=0.3
14:50:34.055 (55790000)|USER_DEBUG|[20]|DEBUG|### Accept-Encoding=gzip,deflate,sdch
14:50:34.055 (55936000)|USER_DEBUG|[20]|DEBUG|### Accept-Language=en-GB,en-US;q=0.8,en;q=0.6
14:50:34.056 (56092000)|USER_DEBUG|[20]|DEBUG|### CipherSuite=RC4-MD5 TLSv1 128-bits
14:50:34.056 (56239000)|USER_DEBUG|[20]|DEBUG|### Connection=keep-alive
14:50:34.056 (56388000)|USER_DEBUG|[20]|DEBUG|### Host=kab-tutorial.na6.visual.force.com
14:50:34.056 (56532000)|USER_DEBUG|[20]|DEBUG|### Referer=https://na6.salesforce.com/01Z80000000lf7nEAA
14:50:34.056 (56677000)|USER_DEBUG|[20]|DEBUG|### User-Agent=Mozilla/5.0 (X11; Jolicloud Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Joli OS/1.2 Chromium/11.0.696.14 Chrome/11.0.696.14 Safari/534.24
14:50:34.056 (56823000)|USER_DEBUG|[20]|DEBUG|### X-Salesforce-Forwarded-To=na6.salesforce.com
14:50:34.056 (56967000)|USER_DEBUG|[20]|DEBUG|### X-Salesforce-SIP=80.176.152.54

and I have a result - the Referer header shows the full URL of the dashboard that my Visualforce component has been added to.

Referer=https://na6.salesforce.com/01Z80000000lf7nEAA

The next hurdle was how to programmatically trigger a dashboard refresh.  Inspecting the Refresh button didn't help much - it was tied to a bunch of Javascript that wouldn't help me server side.   Using the Fiddler web debugger, I clicked the Refresh button and monitored the requests that were submitted to the Salesforce server with fingers crossed that it would be a GET rather than a POST request.  Once again I had a result and could see that one of the requests was an HTTP GET on:


https://na6.salesforce.com/dash/dashboardRefresh.apexp?id=01Z80000000lf7nEAA.

I copied this URL and submitted it directly via the browser. The first attempt returned a minimal page body that the refresh had been submitted - not quite what I had in mind. Upon requesting the page again, I received another small page, but this time telling me that the dashboard had been refreshed. Opening the dashboard page in another tab showed that the refresh had taken place successfully.

At this point I started to think that this might be possible after all!

Next up I added code to my controller to retrieve the referrer and extract the id.  Upon adding this to my dashboard it threw an error.  Not quite what I had in mind.  The reason for this turned out to be simple - the URL for a dashboard edit page is in a different format to the view.  A similar issue was waiting when the dashboard was embedded in a Home page - in this case the Referer header wasn't present.  Luckily these were straightforward to detect and put out a message that the dashboard wouldn't refresh.

I then created an action method that would carry out the HTTP GET, by creating a PageReference to the  refresh page identified above, adding the id to the parameters and executing the getContent() method.  This code is inside a loop, which will make a maximum of 10 attempts to refresh before giving up.  An actionFunction on the page in association with a Javascript setTimeout() invoked the refresh action method 60 seconds after the page was loaded.   Finally, using a technique outlined in an earlier post from this blog, I was able to automatically reload the dashboard after the refresh.

Refreshing after 60 seconds seemed far too arduous for the Salesforce servers, so I upped the timeout to one hour.  This introduced the next issue - session timeout.  The next version reduced the timeout to 60 seconds, but for the first 59 times the timeout expired, the page simply refreshed.  Only on the 60th did the dashboard actually refresh.  This nicely circumvented the session timeout, as the page was submitted every minute.

Finally, I wanted to give the users a visual indication that the dashboard would automatically refresh, resulting in a recurring timeout that fired every second and counted down the seconds until refresh.

While this is probably starting to sound like quite a lot of code, there isn't actually that much to it.

Controller:

////////////////////////////////////////////////////////////
//
// Custom controller for dashboard refresh Visualforce page
//
// Author: Keir Bowden
//
////////////////////////////////////////////////////////////
public with sharing class DashboardRefreshController
{
 public Boolean needsRefresh {get; set;}
 public Boolean canRefresh {get; set;}
 public Id dbIdent {get; set;}
 public Integer minutes {get; set;}
 
 public DashboardRefreshController()
 {
  needsRefresh=true;
  setup();
  minutes=59;
 }
 
 public void setup()
 {
  Map<String, String> headers=ApexPages.currentPage().getHeaders();
  String referrer=headers.get('Referer');
  
  if (null==referrer)
  {
   canRefresh=false;
  }
  else
  {
   Integer lastSlashPos=referrer.lastIndexOf('/');
   lastSlashPos++;
   Integer paramPos=referrer.indexOf('?', lastSlashPos);
  
  
   String result='';
   if (-1!=paramPos)
   {
    result=referrer.substring(lastSlashPos, paramPos);
   }
   else
   {
    result=referrer.substring(lastSlashPos);
   }
   
   try
   {
    dbIdent=result;
    canRefresh=true;
   }
   catch (Exception e)
   {
    canRefresh=false; 
   }
  }
 }
 
 public PageReference refreshDashboard()
 {
  minutes--;
  if (-1==minutes)
  {
   needsRefresh=false;
   String refUrlStr='/dash/dashboardRefresh.apexp?id='+dbIdent;
   Boolean refreshed=false;
   Integer idx=0;
   while ( (!refreshed) && (idx<10) )
   {
    PageReference pr=new PageReference(refUrlStr);
    Blob body=pr.getContent();
    String bodyStr=body.toString();
    refreshed=(-1!=bodyStr.indexOf('Last refreshed'));
    idx++;
   }
  }
   
  return null;
 }
}

Page:


<apex:page sidebar="false" showheader="false" standardstylesheets="false" controller="DashboardRefreshController">

<apex:outputPanel rendered="{!canRefresh}">
 <apex:form >
     <apex:actionFunction name="doRefresh" action="{!refreshDashboard}" />
     <apex:outputPanel id="detail">
   <div id="countDown"></div>
     </apex:outputPanel>
 </apex:form>

 <apex:outputPanel id="scriptPanel">
     <apex:outputPanel rendered="{!needsRefresh}">
   <script>
    window.onload = function() 
    {
     startCountDown(59, 1000, doRefresh);
    } 

    function startCountDown(i, p, f) 
    {
     var pause = p;
     var fn = f;
    
     var countDownObj = document.getElementById("countDown");
     if (countDownObj == null) 
     {
      alert("div not found, check your id");
      return;
     }
    
     countDownObj.count = function(i) 
     {
      countDownObj.innerHTML = 'Refreshing in {!minutes} minutes ' + i + ' seconds';
      if (i == 0) 
      {
       fn();
       return;
      }
      setTimeout(function() 
       {
        countDownObj.count(i - 1);
       },
       pause
      );
     } 
  
     countDownObj.count(i);
    }
   </script>
     </apex:outputPanel>
     
  <apex:outputPanel rendered="{!NOT(needsRefresh)}">
   <script>
       window.top.location='/{!dbIdent}';
   </script>
     </apex:outputPanel>
 </apex:outputPanel>

</apex:outputPanel>
<apex:outputPanel rendered="{!NOT(canRefresh)}">
   Edit mode/home page - refresh disabled
</apex:outputPanel>


</apex:page>

Below is a screenshot of the page embedded in a dashboard:


Once the countdown reaches 0 minutes and zero seconds, the page is automatically refreshed and reloaded to show the updated details.

A couple of words of caution:


  • The page that I'm hitting to refresh isn't documented by Salesforce, which most likely means it is unsupported.  Thus a future release could easily break this functionality.
  • I haven't tried this on a hugely complex dashboard, so I don't know what would happen if the dashboard took significant time to refresh.
  • This is using the browser to trigger the refresh, so if you close the browser session, the refresh won't happen.
  • Refreshing dashboards pulls in information from a number of sources, so refreshing with a short time interval will put additional strain on the Salesforce servers.  
  • I've only tested this with google chrome, so it may not work with other browsers.

32 comments:

  1. Have you tested this out in differnet browsers? I recall that there are differences in whether an iframe request will contain the referrer header to the parent page.

    ...stephan

    ReplyDelete
  2. No, just google chrome. It wouldn't surprise me if there were issues in different browsers, I was just pleased to get it working on one!

    I'll update the post to reflect that though.

    ReplyDelete
  3. Fantastic work mate, loving it. Super easy to deploy, and easy to change the refresh time as well. Good stuff!

    ReplyDelete
  4. It was just what I was looking for.
    I only have a problem in the visualization of the VF component in the dashboard. It shows only the apex code.
    Do youhave any idea why?
    Many thanks.

    ReplyDelete
  5. That smacks that the apex code has been pasted into a visualforce page inadvertently - there's no other way I can think of that the code would be exposed via the page.

    ReplyDelete
    Replies
    1. I was having the same problem and it was a simple fix. It was because my account was in development mode. Simply untick the option from your user setting and it should all work

      Delete
  6. I'm trying to do this but I wasn't sure where you put in the controller. Will this work in Professional edition?

    ReplyDelete
  7. You have to pay extra for apex in PE I believe, so you may not be able to use it in this way. You may be able to build a managed package in a dev org and install that.

    ReplyDelete
  8. Bob,

    I have to admit I know nothing of apex code. But I wanted to see if it was possible to tweak this controller and vf page so that the dashboard refreshes on page load. My people are constantly navigating away from the dashboard and it would help if it just automatically refreshed when they came back to the page. Any help in achieving this would be appreciated. I have tried cobbling things together but my lack of knowledge is really hindering me. Thanks

    ReplyDelete
  9. hi bob,

    where do I have to place the controller?

    ReplyDelete
  10. @Juul - the controller would be in your apex classes - you'll need to be on Enterprise or Unlimited edition to author apex, and you'll need to create it in a sandbox or dev org.

    ReplyDelete
  11. Does this still work?

    I've created the controller and page and put the controller into my dashboard (in Sandbox environment of Enterprise Edition). I'm using Google Chrome.

    I get the message: "Edit mode/home page - refresh disabled"

    Cheers, Robin

    ReplyDelete
    Replies
    1. It still works in my dev org. That message indicates one of the following has occurred:

      (1) The browser didn't pass a referer header (not a great error message I know)
      (2) The dashboard is being edited or has been embedded into a home page

      Delete
  12. Hey Bob... Excellent post... thanks!

    You know what else you can do? You can use XMLHttp in javascript to do the refresh in a HTML homepage component. Since the homepage component runs on the same server as the dashboards, it don't violate cross-site scripting.

    Here's how I put a gauge that refreshes every 1/2 hour into the sidebar:

    <script type="text/javascript">
    function hitRefreshPage() {
    var req = new XMLHttpRequest();
    req.open('GET', "https://na12.salesforce.com/dash/dashboardRefresh.apexp?id=DASH_ID", false);
    req.send(null);
    }
    function refreshDash() {
    hitRefreshPage();
    hitRefreshPage();
    setTimeout(function() { document.getElementById("totalWorkDashImg").src = document.getElementById("totalWorkDashImg").src;}, 5*1000);
    }
    setInterval(refreshDash, 1000*60*30);
    </script>
    <img id="totalWorkDashImg" width="100%" src="https://na12.salesforce.com/servlet/servlet.ChartServer?PARAMS_FROM_DASH">

    ReplyDelete
  13. I tend to use jquery to locate and click the refresh button - that way if he URL of the refresh changes my refresh will still work. Nice idea though.

    ReplyDelete
  14. Do these dashboards appear on the home page, without changing the appearance/functionality of the home page?

    Does it work in IE, Firefox and Chrome?


    --David

    ReplyDelete
    Replies
    1. As mentioned in the post, these won't work in the home page. I've only tested this on chrome and it worked fine there for a full dashboard.

      Delete
    2. I have tested this in both IE and Chrome, but I will say that Chrome was much more stable. I modified it to refresh our call center dashboard every 5 minutes. Running IE (ver 9 I think; might have been 10) it would run fine for about 12-18 hours, but stop refreshing at least once a day (usually around 9PM after starting it at 8AM). However, running in Chrome, the dashboard refreshed perfectly for weeks at a time.

      Delete
  15. I've created the controller and page In my dev org and I'm using Google Chrome.
    This code throws me this line:
    "Edit mode/home page - refresh disabled"

    How to remove this error

    ReplyDelete
  16. This is perfect!!! Exactly what I've been looking for to display our service dashboard on large monitors in our call center. Do you happen to have the test class for this also?

    ReplyDelete
  17. Anybody have a test class for this to get it to work in Production?

    ReplyDelete
    Replies
    1. Carl,

      I wrote the follow test class to achieve 75% code coverage to push the controller to production. The code is very ugly, but it gets the job done. It provides the coverage, but doesn't necessarily test the class. I've also modified the above code to pull the refresh interval from a Custom Setting value. You'll need to modify the code accordingly if you aren't doing the same.

      {Start Code}
      @isTest

      private class DashboardRefreshControllerTest {

      static testMethod void myUnitSingleTest() {

      Id OrgId = [SELECT Id FROM Organization WHERE Name = 'Xiotech Corporation' LIMIT 1].Id;
      XIOSettings__c OrgDefaults = new XIOSettings__c(
      SetupOwnerId = OrgId
      , ActiveWatchPurgeDaysCaseClosed__c = 60
      , HoldQueueId__c = 'None'
      , HoursTZDiff__c = 0
      , DashboardRefreshInterval__c = 5
      );
      insert OrgDefaults;

      Account TestAccount = new Account(
      Name='TestAccount'
      , Industry = 'Unknown'
      );
      insert TestAccount;

      TestAccount = [SELECT Id, Name FROM Account WHERE Name='TestAccount' LIMIT 1];

      String TestURL = '/' + TestAccount.Id;
      PageReference TestPage=new PageReference(TestURL);
      Test.SetCurrentPage(TestPage);
      DashboardRefreshController theController = new DashboardRefreshController();

      TestPage.GetHeaders().Put('Referer', TestURL);
      Test.SetCurrentPage(TestPage);
      DashboardRefreshController theController1 = new DashboardRefreshController();

      OrgDefaults = XIOSettings__c.getOrgDefaults();
      OrgDefaults.DashboardRefreshInterval__c = NULL;
      update OrgDefaults;

      TestURL = TestURL + '?idw';
      TestPage = new PageReference(TestURL);
      TestPage.GetHeaders().Put('Referer', TestURL);
      Test.SetCurrentPage(TestPage);
      DashboardRefreshController theController2 = new DashboardRefreshController();

      theController1.SetMinutes(2);

      Test.startTest();
      theController1.refreshDashboard();
      Test.stopTest();
      }
      }
      {End Code}

      Delete
  18. Hi, I want to Refresh Standard Page - Opportunity, After 3-4 second. ( Auto Refresh). Is it Possible in Standard Page...

    Please give me quick answer..

    ReplyDelete
  19. Hi,
    I am getting Edit mode/home page - refresh disabled error and using chrome. Does anyone know why?

    ReplyDelete
    Replies
    1. That message indicates one of the following has occurred:

      (1) The browser didn't pass a referer header (not a great error message I know)
      (2) The dashboard is being edited or has been embedded into a home page

      Delete
    2. Thanks Bob for quick reply, how to check if browser didn't pass a referer header? And what is the fix for that.
      Thanks much!!

      Delete
    3. make sure your user hasn't got Developement Mode enabled, if it does then just disable and that should fix it.

      Delete
  20. This comment has been removed by the author.

    ReplyDelete
  21. Hi,

    This is great! Just what I've've been looking for. Unfortunately I have no APEX coding ability and am hitting the code coverage issue when trying to deploy to production. Do you have a text class that works for the original code above?

    Thanks

    ReplyDelete
    Replies
    1. There's a test class (not mine) posted in one of the earlier comments.

      Delete
    2. Thanks Bob, unfortunately the Test Class above has been written for a variation of the original code (yours), and I'm not able to make the changes required to get it to work. If you could post a version of the above that will work with your original code I'd really appreciate it.

      Thanks

      Delete