github.com/Ne0nd0g/gophish@v0.7.1-0.20190220040016-11493024a07d/static/js/src/app/campaign_results.js (about)

     1  var map = null
     2  var doPoll = true;
     3  
     4  // statuses is a helper map to point result statuses to ui classes
     5  var statuses = {
     6      "Email Sent": {
     7          color: "#1abc9c",
     8          label: "label-success",
     9          icon: "fa-envelope",
    10          point: "ct-point-sent"
    11      },
    12      "Emails Sent": {
    13          color: "#1abc9c",
    14          label: "label-success",
    15          icon: "fa-envelope",
    16          point: "ct-point-sent"
    17      },
    18      "In progress": {
    19          label: "label-primary"
    20      },
    21      "Queued": {
    22          label: "label-info"
    23      },
    24      "Completed": {
    25          label: "label-success"
    26      },
    27      "Email Opened": {
    28          color: "#f9bf3b",
    29          label: "label-warning",
    30          icon: "fa-envelope-open",
    31          point: "ct-point-opened"
    32      },
    33      "Clicked Link": {
    34          color: "#F39C12",
    35          label: "label-clicked",
    36          icon: "fa-mouse-pointer",
    37          point: "ct-point-clicked"
    38      },
    39      "Success": {
    40          color: "#f05b4f",
    41          label: "label-danger",
    42          icon: "fa-exclamation",
    43          point: "ct-point-clicked"
    44      },
    45      //not a status, but is used for the campaign timeline and user timeline
    46      "Email Reported": {
    47          color: "#45d6ef",
    48          label: "label-info",
    49          icon: "fa-bullhorn",
    50          point: "ct-point-reported"
    51      },
    52      "Error": {
    53          color: "#6c7a89",
    54          label: "label-default",
    55          icon: "fa-times",
    56          point: "ct-point-error"
    57      },
    58      "Error Sending Email": {
    59          color: "#6c7a89",
    60          label: "label-default",
    61          icon: "fa-times",
    62          point: "ct-point-error"
    63      },
    64      "Submitted Data": {
    65          color: "#f05b4f",
    66          label: "label-danger",
    67          icon: "fa-exclamation",
    68          point: "ct-point-clicked"
    69      },
    70      "Unknown": {
    71          color: "#6c7a89",
    72          label: "label-default",
    73          icon: "fa-question",
    74          point: "ct-point-error"
    75      },
    76      "Sending": {
    77          color: "#428bca",
    78          label: "label-primary",
    79          icon: "fa-spinner",
    80          point: "ct-point-sending"
    81      },
    82      "Retrying": {
    83          color: "#6c7a89",
    84          label: "label-default",
    85          icon: "fa-clock-o",
    86          point: "ct-point-error"
    87      },
    88      "Scheduled": {
    89          color: "#428bca",
    90          label: "label-primary",
    91          icon: "fa-clock-o",
    92          point: "ct-point-sending"
    93      },
    94      "Campaign Created": {
    95          label: "label-success",
    96          icon: "fa-rocket"
    97      }
    98  }
    99  
   100  var statusMapping = {
   101      "Email Sent": "sent",
   102      "Email Opened": "opened",
   103      "Clicked Link": "clicked",
   104      "Submitted Data": "submitted_data",
   105      "Email Reported": "reported",
   106  }
   107  
   108  // This is an underwhelming attempt at an enum
   109  // until I have time to refactor this appropriately.
   110  var progressListing = [
   111      "Email Sent",
   112      "Email Opened",
   113      "Clicked Link",
   114      "Submitted Data"
   115  ]
   116  
   117  var campaign = {}
   118  var bubbles = []
   119  
   120  function dismiss() {
   121      $("#modal\\.flashes").empty()
   122      $("#modal").modal('hide')
   123      $("#resultsTable").dataTable().DataTable().clear().draw()
   124  }
   125  
   126  // Deletes a campaign after prompting the user
   127  function deleteCampaign() {
   128      swal({
   129          title: "Are you sure?",
   130          text: "This will delete the campaign. This can't be undone!",
   131          type: "warning",
   132          animation: false,
   133          showCancelButton: true,
   134          confirmButtonText: "Delete Campaign",
   135          confirmButtonColor: "#428bca",
   136          reverseButtons: true,
   137          allowOutsideClick: false,
   138          showLoaderOnConfirm: true,
   139          preConfirm: function () {
   140              return new Promise(function (resolve, reject) {
   141                  api.campaignId.delete(campaign.id)
   142                      .success(function (msg) {
   143                          resolve()
   144                      })
   145                      .error(function (data) {
   146                          reject(data.responseJSON.message)
   147                      })
   148              })
   149          }
   150      }).then(function () {
   151          swal(
   152              'Campaign Deleted!',
   153              'This campaign has been deleted!',
   154              'success'
   155          );
   156          $('button:contains("OK")').on('click', function () {
   157              location.href = '/campaigns'
   158          })
   159      })
   160  }
   161  
   162  // Completes a campaign after prompting the user
   163  function completeCampaign() {
   164      swal({
   165          title: "Are you sure?",
   166          text: "Gophish will stop processing events for this campaign",
   167          type: "warning",
   168          animation: false,
   169          showCancelButton: true,
   170          confirmButtonText: "Complete Campaign",
   171          confirmButtonColor: "#428bca",
   172          reverseButtons: true,
   173          allowOutsideClick: false,
   174          showLoaderOnConfirm: true,
   175          preConfirm: function () {
   176              return new Promise(function (resolve, reject) {
   177                  api.campaignId.complete(campaign.id)
   178                      .success(function (msg) {
   179                          resolve()
   180                      })
   181                      .error(function (data) {
   182                          reject(data.responseJSON.message)
   183                      })
   184              })
   185          }
   186      }).then(function () {
   187          swal(
   188              'Campaign Completed!',
   189              'This campaign has been completed!',
   190              'success'
   191          );
   192          $('#complete_button')[0].disabled = true;
   193          $('#complete_button').text('Completed!')
   194          doPoll = false;
   195      })
   196  }
   197  
   198  // Exports campaign results as a CSV file
   199  function exportAsCSV(scope) {
   200      exportHTML = $("#exportButton").html()
   201      var csvScope = null
   202      var filename = campaign.name + ' - ' + capitalize(scope) + '.csv'
   203      switch (scope) {
   204          case "results":
   205              csvScope = campaign.results
   206              break;
   207          case "events":
   208              csvScope = campaign.timeline
   209              break;
   210      }
   211      if (!csvScope) {
   212          return
   213      }
   214      $("#exportButton").html('<i class="fa fa-spinner fa-spin"></i>')
   215      var csvString = Papa.unparse(csvScope, {})
   216      var csvData = new Blob([csvString], {
   217          type: 'text/csv;charset=utf-8;'
   218      });
   219      if (navigator.msSaveBlob) {
   220          navigator.msSaveBlob(csvData, filename);
   221      } else {
   222          var csvURL = window.URL.createObjectURL(csvData);
   223          var dlLink = document.createElement('a');
   224          dlLink.href = csvURL;
   225          dlLink.setAttribute('download', filename)
   226          document.body.appendChild(dlLink)
   227          dlLink.click();
   228          document.body.removeChild(dlLink)
   229      }
   230      $("#exportButton").html(exportHTML)
   231  }
   232  
   233  function replay(event_idx) {
   234      request = campaign.timeline[event_idx]
   235      details = JSON.parse(request.details)
   236      url = null
   237      form = $('<form>').attr({
   238          method: 'POST',
   239          target: '_blank',
   240      })
   241      /* Create a form object and submit it */
   242      $.each(Object.keys(details.payload), function (i, param) {
   243          if (param == "rid") {
   244              return true;
   245          }
   246          if (param == "__original_url") {
   247              url = details.payload[param];
   248              return true;
   249          }
   250          $('<input>').attr({
   251              name: param,
   252          }).val(details.payload[param]).appendTo(form);
   253      })
   254      /* Ensure we know where to send the user */
   255      // Prompt for the URL
   256      swal({
   257          title: 'Where do you want the credentials submitted to?',
   258          input: 'text',
   259          showCancelButton: true,
   260          inputPlaceholder: "http://example.com/login",
   261          inputValue: url || "",
   262          inputValidator: function (value) {
   263              return new Promise(function (resolve, reject) {
   264                  if (value) {
   265                      resolve();
   266                  } else {
   267                      reject('Invalid URL.');
   268                  }
   269              });
   270          }
   271      }).then(function (result) {
   272          url = result
   273          submitForm()
   274      })
   275      return
   276      submitForm()
   277  
   278      function submitForm() {
   279          form.attr({
   280              action: url
   281          })
   282          form.appendTo('body').submit().remove()
   283      }
   284  }
   285  
   286  /**
   287   * Returns an HTML string that displays the OS and browser that clicked the link
   288   * or submitted credentials.
   289   * 
   290   * @param {object} event_details - The "details" parameter for a campaign
   291   *  timeline event
   292   * 
   293   */
   294  var renderDevice = function (event_details) {
   295      var ua = UAParser(details.browser['user-agent'])
   296      var detailsString = '<div class="timeline-device-details">'
   297  
   298      var deviceIcon = 'laptop'
   299      if (ua.device.type) {
   300          if (ua.device.type == 'tablet' || ua.device.type == 'mobile') {
   301              deviceIcon = ua.device.type
   302          }
   303      }
   304  
   305      var deviceVendor = ''
   306      if (ua.device.vendor) {
   307          deviceVendor = ua.device.vendor.toLowerCase()
   308          if (deviceVendor == 'microsoft') deviceVendor = 'windows'
   309      }
   310  
   311      var deviceName = 'Unknown'
   312      if (ua.os.name) {
   313          deviceName = ua.os.name
   314          if (deviceName == "Mac OS") {
   315              deviceVendor = 'apple'
   316          } else if (deviceName == "Windows") {
   317              deviceVendor = 'windows'
   318          }
   319          if (ua.device.vendor && ua.device.model) {
   320              deviceName = ua.device.vendor + ' ' + ua.device.model
   321          }
   322      }
   323  
   324      if (ua.os.version) {
   325          deviceName = deviceName + ' (OS Version: ' + ua.os.version + ')'
   326      }
   327  
   328      deviceString = '<div class="timeline-device-os"><span class="fa fa-stack">' +
   329          '<i class="fa fa-' + escapeHtml(deviceIcon) + ' fa-stack-2x"></i>' +
   330          '<i class="fa fa-vendor-icon fa-' + escapeHtml(deviceVendor) + ' fa-stack-1x"></i>' +
   331          '</span> ' + escapeHtml(deviceName) + '</div>'
   332  
   333      detailsString += deviceString
   334  
   335      var deviceBrowser = 'Unknown'
   336      var browserIcon = 'info-circle'
   337      var browserVersion = ''
   338  
   339      if (ua.browser && ua.browser.name) {
   340          deviceBrowser = ua.browser.name
   341          // Handle the "mobile safari" case
   342          deviceBrowser = deviceBrowser.replace('Mobile ', '')
   343          if (deviceBrowser) {
   344              browserIcon = deviceBrowser.toLowerCase()
   345              if (browserIcon == 'ie') browserIcon = 'internet-explorer'
   346          }
   347          browserVersion = '(Version: ' + ua.browser.version + ')'
   348      }
   349  
   350      var browserString = '<div class="timeline-device-browser"><span class="fa fa-stack">' +
   351          '<i class="fa fa-' + escapeHtml(browserIcon) + ' fa-stack-1x"></i></span> ' +
   352          deviceBrowser + ' ' + browserVersion + '</div>'
   353  
   354      detailsString += browserString
   355      detailsString += '</div>'
   356      return detailsString
   357  }
   358  
   359  function renderTimeline(data) {
   360      record = {
   361          "id": data[0],
   362          "first_name": data[2],
   363          "last_name": data[3],
   364          "email": data[4],
   365          "position": data[5],
   366          "status": data[6],
   367          "reported": data[7],
   368          "send_date": data[8]
   369      }
   370      results = '<div class="timeline col-sm-12 well well-lg">' +
   371          '<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
   372          '</h6><span class="subtitle">Email: ' + escapeHtml(record.email) +
   373          '<br>Result ID: ' + escapeHtml(record.id) + '</span>' +
   374          '<div class="timeline-graph col-sm-6">'
   375      $.each(campaign.timeline, function (i, event) {
   376          if (!event.email || event.email == record.email) {
   377              // Add the event
   378              results += '<div class="timeline-entry">' +
   379                  '    <div class="timeline-bar"></div>'
   380              results +=
   381                  '    <div class="timeline-icon ' + statuses[event.message].label + '">' +
   382                  '    <i class="fa ' + statuses[event.message].icon + '"></i></div>' +
   383                  '    <div class="timeline-message">' + escapeHtml(event.message) +
   384                  '    <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
   385              if (event.details) {
   386                  details = JSON.parse(event.details)
   387                  if (event.message == "Clicked Link" || event.message == "Submitted Data") {
   388                      deviceView = renderDevice(details)
   389                      if (deviceView) {
   390                          results += deviceView
   391                      }
   392                  }
   393                  if (event.message == "Submitted Data") {
   394                      results += '<div class="timeline-replay-button"><button onclick="replay(' + i + ')" class="btn btn-success">'
   395                      results += '<i class="fa fa-refresh"></i> Replay Credentials</button></div>'
   396                      results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
   397                  }
   398                  if (details.payload) {
   399                      results += '<div class="timeline-event-results">'
   400                      results += '    <table class="table table-condensed table-bordered table-striped">'
   401                      results += '        <thead><tr><th>Parameter</th><th>Value(s)</tr></thead><tbody>'
   402                      $.each(Object.keys(details.payload), function (i, param) {
   403                          if (param == "rid") {
   404                              return true;
   405                          }
   406                          results += '    <tr>'
   407                          results += '        <td>' + escapeHtml(param) + '</td>'
   408                          results += '        <td>' + escapeHtml(details.payload[param]) + '</td>'
   409                          results += '    </tr>'
   410                      })
   411                      results += '       </tbody></table>'
   412                      results += '</div>'
   413                  }
   414                  if (details.error) {
   415                      results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
   416                      results += '<div class="timeline-event-results">'
   417                      results += '<span class="label label-default">Error</span> ' + details.error
   418                      results += '</div>'
   419                  }
   420              }
   421              results += '</div></div>'
   422          }
   423      })
   424      // Add the scheduled send event at the bottom
   425      if (record.status == "Scheduled" || record.status == "Retrying") {
   426          results += '<div class="timeline-entry">' +
   427              '    <div class="timeline-bar"></div>'
   428          results +=
   429              '    <div class="timeline-icon ' + statuses[record.status].label + '">' +
   430              '    <i class="fa ' + statuses[record.status].icon + '"></i></div>' +
   431              '    <div class="timeline-message">' + "Scheduled to send at " + record.send_date + '</span>'
   432      }
   433      results += '</div></div>'
   434      return results
   435  }
   436  
   437  var renderTimelineChart = function (chartopts) {
   438      return Highcharts.chart('timeline_chart', {
   439          chart: {
   440              zoomType: 'x',
   441              type: 'line',
   442              height: "200px"
   443          },
   444          title: {
   445              text: 'Campaign Timeline'
   446          },
   447          xAxis: {
   448              type: 'datetime',
   449              dateTimeLabelFormats: {
   450                  second: '%l:%M:%S',
   451                  minute: '%l:%M',
   452                  hour: '%l:%M',
   453                  day: '%b %d, %Y',
   454                  week: '%b %d, %Y',
   455                  month: '%b %Y'
   456              }
   457          },
   458          yAxis: {
   459              min: 0,
   460              max: 2,
   461              visible: false,
   462              tickInterval: 1,
   463              labels: {
   464                  enabled: false
   465              },
   466              title: {
   467                  text: ""
   468              }
   469          },
   470          tooltip: {
   471              formatter: function () {
   472                  return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) +
   473                      '<br>Event: ' + this.point.message + '<br>Email: <b>' + this.point.email + '</b>'
   474              }
   475          },
   476          legend: {
   477              enabled: false
   478          },
   479          plotOptions: {
   480              series: {
   481                  marker: {
   482                      enabled: true,
   483                      symbol: 'circle',
   484                      radius: 3
   485                  },
   486                  cursor: 'pointer',
   487              },
   488              line: {
   489                  states: {
   490                      hover: {
   491                          lineWidth: 1
   492                      }
   493                  }
   494              }
   495          },
   496          credits: {
   497              enabled: false
   498          },
   499          series: [{
   500              data: chartopts['data'],
   501              dashStyle: "shortdash",
   502              color: "#cccccc",
   503              lineWidth: 1,
   504              turboThreshold: 0
   505          }]
   506      })
   507  }
   508  
   509  /* Renders a pie chart using the provided chartops */
   510  var renderPieChart = function (chartopts) {
   511      return Highcharts.chart(chartopts['elemId'], {
   512          chart: {
   513              type: 'pie',
   514              events: {
   515                  load: function () {
   516                      var chart = this,
   517                          rend = chart.renderer,
   518                          pie = chart.series[0],
   519                          left = chart.plotLeft + pie.center[0],
   520                          top = chart.plotTop + pie.center[1];
   521                      this.innerText = rend.text(chartopts['data'][0].count, left, top).
   522                      attr({
   523                          'text-anchor': 'middle',
   524                          'font-size': '24px',
   525                          'font-weight': 'bold',
   526                          'fill': chartopts['colors'][0],
   527                          'font-family': 'Helvetica,Arial,sans-serif'
   528                      }).add();
   529                  },
   530                  render: function () {
   531                      this.innerText.attr({
   532                          text: chartopts['data'][0].count
   533                      })
   534                  }
   535              }
   536          },
   537          title: {
   538              text: chartopts['title']
   539          },
   540          plotOptions: {
   541              pie: {
   542                  innerSize: '80%',
   543                  dataLabels: {
   544                      enabled: false
   545                  }
   546              }
   547          },
   548          credits: {
   549              enabled: false
   550          },
   551          tooltip: {
   552              formatter: function () {
   553                  if (this.key == undefined) {
   554                      return false
   555                  }
   556                  return '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '%</b><br/>'
   557              }
   558          },
   559          series: [{
   560              data: chartopts['data'],
   561              colors: chartopts['colors'],
   562          }]
   563      })
   564  }
   565  
   566  /* Updates the bubbles on the map
   567  
   568  @param {campaign.result[]} results - The campaign results to process
   569  */
   570  var updateMap = function (results) {
   571      if (!map) {
   572          return
   573      }
   574      bubbles = []
   575      $.each(campaign.results, function (i, result) {
   576          // Check that it wasn't an internal IP
   577          if (result.latitude == 0 && result.longitude == 0) {
   578              return true;
   579          }
   580          newIP = true
   581          $.each(bubbles, function (i, bubble) {
   582              if (bubble.ip == result.ip) {
   583                  bubbles[i].radius += 1
   584                  newIP = false
   585                  return false
   586              }
   587          })
   588          if (newIP) {
   589              bubbles.push({
   590                  latitude: result.latitude,
   591                  longitude: result.longitude,
   592                  name: result.ip,
   593                  fillKey: "point",
   594                  radius: 2
   595              })
   596          }
   597      })
   598      map.bubbles(bubbles)
   599  }
   600  
   601  /**
   602   * Creates a status label for use in the results datatable
   603   * @param {string} status 
   604   * @param {moment(datetime)} send_date 
   605   */
   606  function createStatusLabel(status, send_date) {
   607      var label = statuses[status].label || "label-default";
   608      var statusColumn = "<span class=\"label " + label + "\">" + status + "</span>"
   609      // Add the tooltip if the email is scheduled to be sent
   610      if (status == "Scheduled" || status == "Retrying") {
   611          var sendDateMessage = "Scheduled to send at " + send_date
   612          statusColumn = "<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"top\" data-html=\"true\" title=\"" + sendDateMessage + "\">" + status + "</span>"
   613      }
   614      return statusColumn
   615  }
   616  
   617  /* poll - Queries the API and updates the UI with the results
   618   *
   619   * Updates:
   620   * * Timeline Chart
   621   * * Email (Donut) Chart
   622   * * Map Bubbles
   623   * * Datatables
   624   */
   625  function poll() {
   626      api.campaignId.results(campaign.id)
   627          .success(function (c) {
   628              campaign = c
   629              /* Update the timeline */
   630              var timeline_series_data = []
   631              $.each(campaign.timeline, function (i, event) {
   632                  var event_date = moment.utc(event.time).local()
   633                  timeline_series_data.push({
   634                      email: event.email,
   635                      x: event_date.valueOf(),
   636                      y: 1
   637                  })
   638              })
   639              var timeline_series_data = []
   640              $.each(campaign.timeline, function (i, event) {
   641                  var event_date = moment.utc(event.time).local()
   642                  timeline_series_data.push({
   643                      email: event.email,
   644                      message: event.message,
   645                      x: event_date.valueOf(),
   646                      y: 1,
   647                      marker: {
   648                          fillColor: statuses[event.message].color
   649                      }
   650                  })
   651              })
   652              var timeline_chart = $("#timeline_chart").highcharts()
   653              timeline_chart.series[0].update({
   654                  data: timeline_series_data
   655              })
   656              /* Update the results donut chart */
   657              var email_series_data = {}
   658              // Load the initial data
   659              Object.keys(statusMapping).forEach(function (k) {
   660                  email_series_data[k] = 0
   661              });
   662              $.each(campaign.results, function (i, result) {
   663                  email_series_data[result.status]++;
   664                  if (result.reported) {
   665                      email_series_data['Email Reported']++
   666                  }
   667                  // Backfill status values
   668                  var step = progressListing.indexOf(result.status)
   669                  for (var i = 0; i < step; i++) {
   670                      email_series_data[progressListing[i]]++
   671                  }
   672              })
   673              $.each(email_series_data, function (status, count) {
   674                  var email_data = []
   675                  if (!(status in statusMapping)) {
   676                      return true
   677                  }
   678                  email_data.push({
   679                      name: status,
   680                      y: Math.floor((count / campaign.results.length) * 100),
   681                      count: count
   682                  })
   683                  email_data.push({
   684                      name: '',
   685                      y: 100 - Math.floor((count / campaign.results.length) * 100)
   686                  })
   687                  var chart = $("#" + statusMapping[status] + "_chart").highcharts()
   688                  chart.series[0].update({
   689                      data: email_data
   690                  })
   691              })
   692  
   693              /* Update the datatable */
   694              resultsTable = $("#resultsTable").DataTable()
   695              resultsTable.rows().every(function (i, tableLoop, rowLoop) {
   696                  var row = this.row(i)
   697                  var rowData = row.data()
   698                  var rid = rowData[0]
   699                  $.each(campaign.results, function (j, result) {
   700                      if (result.id == rid) {
   701                          rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
   702                          rowData[7] = result.reported
   703                          rowData[6] = result.status
   704                          resultsTable.row(i).data(rowData)
   705                          if (row.child.isShown()) {
   706                              $(row.node()).find("#caret").removeClass("fa-caret-right")
   707                              $(row.node()).find("#caret").addClass("fa-caret-down")
   708                              row.child(renderTimeline(row.data()))
   709                          }
   710                          return false
   711                      }
   712                  })
   713              })
   714              resultsTable.draw(false)
   715              /* Update the map information */
   716              updateMap(campaign.results)
   717              $('[data-toggle="tooltip"]').tooltip()
   718              $("#refresh_message").hide()
   719              $("#refresh_btn").show()
   720          })
   721  }
   722  
   723  function load() {
   724      campaign.id = window.location.pathname.split('/').slice(-1)[0]
   725      var use_map = JSON.parse(localStorage.getItem('gophish.use_map'))
   726      api.campaignId.results(campaign.id)
   727          .success(function (c) {
   728              campaign = c
   729              if (campaign) {
   730                  $("title").text(c.name + " - Gophish")
   731                  $("#loading").hide()
   732                  $("#campaignResults").show()
   733                  // Set the title
   734                  $("#page-title").text("Results for " + c.name)
   735                  if (c.status == "Completed") {
   736                      $('#complete_button')[0].disabled = true;
   737                      $('#complete_button').text('Completed!');
   738                      doPoll = false;
   739                  }
   740                  // Setup viewing the details of a result
   741                  $("#resultsTable").on("click", ".timeline-event-details", function () {
   742                      // Show the parameters
   743                      payloadResults = $(this).parent().find(".timeline-event-results")
   744                      if (payloadResults.is(":visible")) {
   745                          $(this).find("i").removeClass("fa-caret-down")
   746                          $(this).find("i").addClass("fa-caret-right")
   747                          payloadResults.hide()
   748                      } else {
   749                          $(this).find("i").removeClass("fa-caret-right")
   750                          $(this).find("i").addClass("fa-caret-down")
   751                          payloadResults.show()
   752                      }
   753                  })
   754                  // Setup the results table
   755                  resultsTable = $("#resultsTable").DataTable({
   756                      destroy: true,
   757                      "order": [
   758                          [2, "asc"]
   759                      ],
   760                      columnDefs: [{
   761                              orderable: false,
   762                              targets: "no-sort"
   763                          }, {
   764                              className: "details-control",
   765                              "targets": [1]
   766                          }, {
   767                              "visible": false,
   768                              "targets": [0, 8]
   769                          },
   770                          {
   771                              "render": function (data, type, row) {
   772                                  return createStatusLabel(data, row[8])
   773                              },
   774                              "targets": [6]
   775                          },
   776                          {
   777                              className: "text-center",
   778                              "render": function (reported, type, row) {
   779                                  if (type == "display") {
   780                                      if (reported) {
   781                                          return "<i class='fa fa-check-circle text-center text-success'></i>"
   782                                      }
   783                                      return "<i class='fa fa-times-circle text-center text-muted'></i>"
   784                                  }
   785                                  return reported
   786                              },
   787                              "targets": [7]
   788                          }
   789                      ]
   790                  });
   791                  resultsTable.clear();
   792                  var email_series_data = {}
   793                  var timeline_series_data = []
   794                  Object.keys(statusMapping).forEach(function (k) {
   795                      email_series_data[k] = 0
   796                  });
   797                  $.each(campaign.results, function (i, result) {
   798                      resultsTable.row.add([
   799                          result.id,
   800                          "<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
   801                          escapeHtml(result.first_name) || "",
   802                          escapeHtml(result.last_name) || "",
   803                          escapeHtml(result.email) || "",
   804                          escapeHtml(result.position) || "",
   805                          result.status,
   806                          result.reported,
   807                          moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
   808                      ])
   809                      email_series_data[result.status]++;
   810                      if (result.reported) {
   811                          email_series_data['Email Reported']++
   812                      }
   813                      // Backfill status values
   814                      var step = progressListing.indexOf(result.status)
   815                      for (var i = 0; i < step; i++) {
   816                          email_series_data[progressListing[i]]++
   817                      }
   818                  })
   819                  resultsTable.draw();
   820                  // Setup tooltips
   821                  $('[data-toggle="tooltip"]').tooltip()
   822                  // Setup the individual timelines
   823                  $('#resultsTable tbody').on('click', 'td.details-control', function () {
   824                      var tr = $(this).closest('tr');
   825                      var row = resultsTable.row(tr);
   826                      if (row.child.isShown()) {
   827                          // This row is already open - close it
   828                          row.child.hide();
   829                          tr.removeClass('shown');
   830                          $(this).find("i").removeClass("fa-caret-down")
   831                          $(this).find("i").addClass("fa-caret-right")
   832                      } else {
   833                          // Open this row
   834                          $(this).find("i").removeClass("fa-caret-right")
   835                          $(this).find("i").addClass("fa-caret-down")
   836                          row.child(renderTimeline(row.data())).show();
   837                          tr.addClass('shown');
   838                      }
   839                  });
   840                  // Setup the graphs
   841                  $.each(campaign.timeline, function (i, event) {
   842                      if (event.message == "Campaign Created") {
   843                          return true
   844                      }
   845                      var event_date = moment.utc(event.time).local()
   846                      timeline_series_data.push({
   847                          email: event.email,
   848                          message: event.message,
   849                          x: event_date.valueOf(),
   850                          y: 1,
   851                          marker: {
   852                              fillColor: statuses[event.message].color
   853                          }
   854                      })
   855                  })
   856                  renderTimelineChart({
   857                      data: timeline_series_data
   858                  })
   859                  $.each(email_series_data, function (status, count) {
   860                      var email_data = []
   861                      if (!(status in statusMapping)) {
   862                          return true
   863                      }
   864                      email_data.push({
   865                          name: status,
   866                          y: Math.floor((count / campaign.results.length) * 100),
   867                          count: count
   868                      })
   869                      email_data.push({
   870                          name: '',
   871                          y: 100 - Math.floor((count / campaign.results.length) * 100)
   872                      })
   873                      var chart = renderPieChart({
   874                          elemId: statusMapping[status] + '_chart',
   875                          title: status,
   876                          name: status,
   877                          data: email_data,
   878                          colors: [statuses[status].color, '#dddddd']
   879                      })
   880                  })
   881  
   882                  if (use_map) {
   883                      $("#resultsMapContainer").show()
   884                      map = new Datamap({
   885                          element: document.getElementById("resultsMap"),
   886                          responsive: true,
   887                          fills: {
   888                              defaultFill: "#ffffff",
   889                              point: "#283F50"
   890                          },
   891                          geographyConfig: {
   892                              highlightFillColor: "#1abc9c",
   893                              borderColor: "#283F50"
   894                          },
   895                          bubblesConfig: {
   896                              borderColor: "#283F50"
   897                          }
   898                      });
   899                  }
   900                  updateMap(campaign.results)
   901              }
   902          })
   903          .error(function () {
   904              $("#loading").hide()
   905              errorFlash(" Campaign not found!")
   906          })
   907  }
   908  
   909  var setRefresh
   910  
   911  function refresh() {
   912      if (!doPoll) {
   913          return;
   914      }
   915      $("#refresh_message").show()
   916      $("#refresh_btn").hide()
   917      poll()
   918      clearTimeout(setRefresh)
   919      setRefresh = setTimeout(refresh, 60000)
   920  };
   921  
   922  
   923  
   924  $(document).ready(function () {
   925      Highcharts.setOptions({
   926          global: {
   927              useUTC: false
   928          }
   929      })
   930      load();
   931  
   932      // Start the polling loop
   933      setRefresh = setTimeout(refresh, 60000)
   934  })