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