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 })