mirror of https://github.com/gophish/gophish
Moved all charts from Chartist to Highcharts. Closes #680.
parent
972c40fd87
commit
75600f5812
31
gulpfile.js
31
gulpfile.js
|
@ -5,11 +5,9 @@
|
|||
*/
|
||||
|
||||
var gulp = require('gulp'),
|
||||
jshint = require('gulp-jshint'),
|
||||
rename = require('gulp-rename'),
|
||||
concat = require('gulp-concat'),
|
||||
uglify = require('gulp-uglify'),
|
||||
wrap = require('gulp-wrap'),
|
||||
cleanCSS = require('gulp-clean-css'),
|
||||
|
||||
js_directory = 'static/js/src/',
|
||||
|
@ -19,17 +17,9 @@ var gulp = require('gulp'),
|
|||
dest_js_directory = 'static/js/dist/',
|
||||
dest_css_directory = 'static/css/dist/';
|
||||
|
||||
gulp.task('default', ['watch']);
|
||||
|
||||
gulp.task('jshint', function() {
|
||||
return gulp.src(js_directory)
|
||||
.pipe(jshint())
|
||||
.pipe(jshint.reporter('jshint-stylish'));
|
||||
});
|
||||
|
||||
gulp.task('build', function() {
|
||||
gulp.task('vendorjs', function () {
|
||||
// Vendor minifying / concat
|
||||
gulp.src([
|
||||
return gulp.src([
|
||||
vendor_directory + 'jquery.js',
|
||||
vendor_directory + 'bootstrap.min.js',
|
||||
vendor_directory + 'moment.min.js',
|
||||
|
@ -47,7 +37,8 @@ gulp.task('build', function() {
|
|||
vendor_directory + 'sweetalert2.min.js',
|
||||
vendor_directory + 'bootstrap-datetime.js',
|
||||
vendor_directory + 'select2.min.js',
|
||||
vendor_directory + 'core.min.js'
|
||||
vendor_directory + 'core.min.js',
|
||||
vendor_directory + 'highcharts.js'
|
||||
])
|
||||
.pipe(concat('vendor.js'))
|
||||
.pipe(rename({
|
||||
|
@ -55,7 +46,9 @@ gulp.task('build', function() {
|
|||
}))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(dest_js_directory));
|
||||
})
|
||||
|
||||
gulp.task('scripts', function () {
|
||||
// Gophish app files
|
||||
gulp.src(app_directory)
|
||||
.pipe(rename({
|
||||
|
@ -65,7 +58,9 @@ gulp.task('build', function() {
|
|||
console.log(e);
|
||||
}))
|
||||
.pipe(gulp.dest(dest_js_directory + 'app/'));
|
||||
})
|
||||
|
||||
gulp.task('styles', function() {
|
||||
return gulp.src([
|
||||
css_directory + 'bootstrap.min.css',
|
||||
css_directory + 'main.css',
|
||||
|
@ -78,13 +73,13 @@ gulp.task('build', function() {
|
|||
css_directory + 'checkbox.css',
|
||||
css_directory + 'sweetalert2.min.css',
|
||||
css_directory + 'select2.min.css',
|
||||
css_directory + 'select2-bootstrap.min.css'
|
||||
css_directory + 'select2-bootstrap.min.css',
|
||||
])
|
||||
.pipe(cleanCSS({ compatibilty: 'ie9' }))
|
||||
.pipe(concat('gophish.css'))
|
||||
.pipe(gulp.dest(dest_css_directory));
|
||||
});
|
||||
})
|
||||
|
||||
gulp.task('watch', function() {
|
||||
gulp.watch('static/js/src/app/**/*.js', ['jshint']);
|
||||
});
|
||||
gulp.task('build', ['vendorjs', 'scripts', 'styles']);
|
||||
|
||||
gulp.task('default', ['build']);
|
|
@ -198,6 +198,7 @@ func (c *Campaign) getDetails() error {
|
|||
}
|
||||
|
||||
// getCampaignStats returns a CampaignStats object for the campaign with the given campaign ID.
|
||||
// It also backfills numbers as appropriate with a running total, so that the values are aggregated.
|
||||
func getCampaignStats(cid int64) (CampaignStats, error) {
|
||||
s := CampaignStats{}
|
||||
query := db.Table("results").Where("campaign_id = ?", cid)
|
||||
|
@ -205,11 +206,7 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
|
|||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
err = query.Where("status=?", EVENT_SENT).Count(&s.EmailsSent).Error
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error
|
||||
query.Where("status=?", EVENT_DATA_SUBMIT).Count(&s.SubmittedData)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
@ -217,10 +214,20 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
|
|||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
query.Where("status=?", EVENT_DATA_SUBMIT).Count(&s.SubmittedData)
|
||||
// Every submitted data event implies they clicked the link
|
||||
s.ClickedLink += s.SubmittedData
|
||||
err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
// Every clicked link event implies they opened the email
|
||||
s.OpenedEmail += s.ClickedLink
|
||||
err = query.Where("status=?", EVENT_SENT).Count(&s.EmailsSent).Error
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
// Every opened email event implies the email was sent
|
||||
s.EmailsSent += s.OpenedEmail
|
||||
err = query.Where("status=?", ERROR).Count(&s.Error).Error
|
||||
return s, err
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "gophish",
|
||||
"version": "0.3.0-dev",
|
||||
"version": "0.4.0-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/gophish/gophish.git"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -541,3 +541,25 @@ table.dataTable{
|
|||
.input-group-btn .btn {
|
||||
line-height:20px !important;
|
||||
}
|
||||
.highcharts-title {
|
||||
font-family: "Source Sans Pro",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
.color-success {
|
||||
font-weight: bold;
|
||||
color: #f05b4f;
|
||||
}
|
||||
.color-sent {
|
||||
font-weight: bold;
|
||||
color: #1abc9c;
|
||||
}
|
||||
.color-opened {
|
||||
font-weight: bold;
|
||||
color: #f9bf3b;
|
||||
}
|
||||
.color-clicked {
|
||||
font-weight: bold;
|
||||
color: #f39c12;
|
||||
}
|
||||
.color-success {
|
||||
color: #f05b4f;
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -4,64 +4,70 @@ var doPoll = true;
|
|||
// statuses is a helper map to point result statuses to ui classes
|
||||
var statuses = {
|
||||
"Email Sent": {
|
||||
slice: "ct-slice-donut-sent",
|
||||
legend: "ct-legend-sent",
|
||||
color: "#1abc9c",
|
||||
label: "label-success",
|
||||
icon: "fa-envelope",
|
||||
point: "ct-point-sent"
|
||||
},
|
||||
"Emails Sent": {
|
||||
color: "#1abc9c",
|
||||
label: "label-success",
|
||||
icon: "fa-envelope",
|
||||
point: "ct-point-sent"
|
||||
},
|
||||
"In progress": {
|
||||
label: "label-primary"
|
||||
},
|
||||
"Queued": {
|
||||
label: "label-info"
|
||||
},
|
||||
"Completed": {
|
||||
label: "label-success"
|
||||
},
|
||||
"Email Opened": {
|
||||
slice: "ct-slice-donut-opened",
|
||||
legend: "ct-legend-opened",
|
||||
color: "#f9bf3b",
|
||||
label: "label-warning",
|
||||
icon: "fa-envelope",
|
||||
point: "ct-point-opened"
|
||||
},
|
||||
"Clicked Link": {
|
||||
slice: "ct-slice-donut-clicked",
|
||||
legend: "ct-legend-clicked",
|
||||
color: "#F39C12",
|
||||
label: "label-clicked",
|
||||
icon: "fa-mouse-pointer",
|
||||
point: "ct-point-clicked"
|
||||
},
|
||||
"Success": {
|
||||
slice: "ct-slice-donut-success",
|
||||
legend: "ct-legend-success",
|
||||
color: "#f05b4f",
|
||||
label: "label-danger",
|
||||
icon: "fa-exclamation",
|
||||
point: "ct-point-clicked"
|
||||
},
|
||||
"Error": {
|
||||
slice: "ct-slice-donut-error",
|
||||
legend: "ct-legend-error",
|
||||
color: "#6c7a89",
|
||||
label: "label-default",
|
||||
icon: "fa-times",
|
||||
point: "ct-point-error"
|
||||
},
|
||||
"Error Sending Email": {
|
||||
slice: "ct-slice-donut-error",
|
||||
legend: "ct-legend-error",
|
||||
color: "#6c7a89",
|
||||
label: "label-default",
|
||||
icon: "fa-times",
|
||||
point: "ct-point-error"
|
||||
},
|
||||
"Submitted Data": {
|
||||
slice: "ct-slice-donut-success",
|
||||
legend: "ct-legend-success",
|
||||
color: "#f05b4f",
|
||||
label: "label-danger",
|
||||
icon: "fa-exclamation",
|
||||
point: "ct-point-clicked"
|
||||
},
|
||||
"Unknown": {
|
||||
slice: "ct-slice-donut-error",
|
||||
legend: "ct-legend-error",
|
||||
color: "#6c7a89",
|
||||
label: "label-default",
|
||||
icon: "fa-question",
|
||||
point: "ct-point-error"
|
||||
},
|
||||
"Sending": {
|
||||
slice: "ct-slice-donut-sending",
|
||||
legend: "ct-legend-sending",
|
||||
color: "#428bca",
|
||||
label: "label-primary",
|
||||
icon: "fa-spinner",
|
||||
point: "ct-point-sending"
|
||||
|
@ -72,6 +78,22 @@ var statuses = {
|
|||
}
|
||||
}
|
||||
|
||||
var statusMapping = {
|
||||
"Email Sent": "sent",
|
||||
"Email Opened": "opened",
|
||||
"Clicked Link": "clicked",
|
||||
"Submitted Data": "submitted_data",
|
||||
}
|
||||
|
||||
// This is an underwhelming attempt at an enum
|
||||
// until I have time to refactor this appropriately.
|
||||
var progressListing = [
|
||||
"Email Sent",
|
||||
"Email Opened",
|
||||
"Clicked Link",
|
||||
"Submitted Data"
|
||||
]
|
||||
|
||||
var campaign = {}
|
||||
var bubbles = []
|
||||
|
||||
|
@ -93,6 +115,7 @@ function deleteCampaign() {
|
|||
confirmButtonColor: "#428bca",
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: false,
|
||||
showLoaderOnConfirm: true,
|
||||
preConfirm: function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
api.campaignId.delete(campaign.id)
|
||||
|
@ -128,6 +151,7 @@ function completeCampaign() {
|
|||
confirmButtonColor: "#428bca",
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: false,
|
||||
showLoaderOnConfirm: true,
|
||||
preConfirm: function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
api.campaignId.complete(campaign.id)
|
||||
|
@ -296,6 +320,131 @@ function renderTimeline(data) {
|
|||
return results
|
||||
}
|
||||
|
||||
var renderTimelineChart = function (chartopts) {
|
||||
return Highcharts.chart('timeline_chart', {
|
||||
chart: {
|
||||
zoomType: 'x',
|
||||
type: 'line',
|
||||
height: "200px"
|
||||
},
|
||||
title: {
|
||||
text: 'Campaign Timeline'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
dateTimeLabelFormats: {
|
||||
second: '%l:%M:%S',
|
||||
minute: '%l:%M',
|
||||
hour: '%l:%M',
|
||||
day: '%b %d, %Y',
|
||||
week: '%b %d, %Y',
|
||||
month: '%b %Y'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
max: 2,
|
||||
visible: false,
|
||||
tickInterval: 1,
|
||||
labels: {
|
||||
enabled: false
|
||||
},
|
||||
title: {
|
||||
text: ""
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function () {
|
||||
return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) +
|
||||
'<br>Event: ' + this.point.message + '<br>Email: <b>' + this.point.email + '</b>'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
enabled: false
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
marker: {
|
||||
enabled: true,
|
||||
symbol: 'circle',
|
||||
radius: 3
|
||||
},
|
||||
cursor: 'pointer',
|
||||
},
|
||||
line: {
|
||||
states: {
|
||||
hover: {
|
||||
lineWidth: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
series: [{
|
||||
data: chartopts['data'],
|
||||
dashStyle: "shortdash",
|
||||
color: "#cccccc",
|
||||
lineWidth: 1
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
/* Renders a pie chart using the provided chartops */
|
||||
var renderPieChart = function (chartopts) {
|
||||
return Highcharts.chart(chartopts['elemId'], {
|
||||
chart: {
|
||||
type: 'pie',
|
||||
events: {
|
||||
load: function () {
|
||||
var chart = this,
|
||||
rend = chart.renderer,
|
||||
pie = chart.series[0],
|
||||
left = chart.plotLeft + pie.center[0],
|
||||
top = chart.plotTop + pie.center[1];
|
||||
this.innerText = rend.text(chartopts['data'][0].y, left, top).
|
||||
attr({
|
||||
'text-anchor': 'middle',
|
||||
'font-size': '24px',
|
||||
'font-weight': 'bold',
|
||||
'fill': chartopts['colors'][0],
|
||||
'font-family': 'Helvetica,Arial,sans-serif'
|
||||
}).add();
|
||||
},
|
||||
render: function () {
|
||||
this.innerText.attr({ text: chartopts['data'][0].y })
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: chartopts['title']
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
innerSize: '80%',
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function () {
|
||||
if (this.key == undefined) {
|
||||
return false
|
||||
}
|
||||
return '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '</b><br/>'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
data: chartopts['data'],
|
||||
colors: chartopts['colors'],
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
/* poll - Queries the API and updates the UI with the results
|
||||
*
|
||||
|
@ -310,51 +459,64 @@ function poll() {
|
|||
.success(function (c) {
|
||||
campaign = c
|
||||
/* Update the timeline */
|
||||
var timeline_data = {
|
||||
series: [{
|
||||
name: "Events",
|
||||
data: []
|
||||
}]
|
||||
}
|
||||
var timeline_series_data = []
|
||||
$.each(campaign.timeline, function (i, event) {
|
||||
timeline_data.series[0].data.push({
|
||||
meta: i,
|
||||
x: new Date(event.time),
|
||||
var event_date = moment(event.time)
|
||||
timeline_series_data.push({
|
||||
email: event.email,
|
||||
x: event_date.valueOf(),
|
||||
y: 1
|
||||
})
|
||||
})
|
||||
var timeline_chart = $("#timeline_chart")
|
||||
if (timeline_chart.get(0).__chartist__) {
|
||||
timeline_chart.get(0).__chartist__.update(timeline_data)
|
||||
var timeline_series_data = []
|
||||
$.each(campaign.timeline, function (i, event) {
|
||||
var event_date = moment(event.time)
|
||||
timeline_series_data.push({
|
||||
email: event.email,
|
||||
message: event.message,
|
||||
x: event_date.valueOf(),
|
||||
y: 1,
|
||||
marker: {
|
||||
fillColor: statuses[event.message].color
|
||||
}
|
||||
})
|
||||
})
|
||||
var timeline_chart = $("#timeline_chart").highcharts()
|
||||
timeline_chart.series[0].update({
|
||||
data: timeline_series_data
|
||||
})
|
||||
/* Update the results donut chart */
|
||||
var email_data = {
|
||||
series: []
|
||||
}
|
||||
var email_series_data = {}
|
||||
// Load the initial data
|
||||
Object.keys(statusMapping).forEach(function (k) {
|
||||
email_series_data[k] = 0
|
||||
});
|
||||
$.each(campaign.results, function (i, result) {
|
||||
if (!email_series_data[result.status]) {
|
||||
email_series_data[result.status] = 1
|
||||
} else {
|
||||
email_series_data[result.status]++;
|
||||
// Backfill status values
|
||||
var step = progressListing.indexOf(result.status)
|
||||
for (var i = 0; i < step; i++) {
|
||||
email_series_data[progressListing[i]]++
|
||||
}
|
||||
})
|
||||
$("#email_chart_legend").html("")
|
||||
$.each(email_series_data, function (status, count) {
|
||||
email_data.series.push({
|
||||
meta: status,
|
||||
value: count
|
||||
})
|
||||
$("#email_chart_legend").append('<li><span class="' + statuses[status].legend + '"></span>' + status + '</li>')
|
||||
})
|
||||
var email_chart = $("#email_chart")
|
||||
if (email_chart.get(0).__chartist__) {
|
||||
email_chart.get(0).__chartist__.on('draw', function(data) {
|
||||
data.element.addClass(statuses[data.meta].slice)
|
||||
})
|
||||
// Update with the latest data
|
||||
email_chart.get(0).__chartist__.update(email_data)
|
||||
var email_data = []
|
||||
if (!(status in statusMapping)) {
|
||||
return true
|
||||
}
|
||||
email_data.push({
|
||||
name: status,
|
||||
y: count
|
||||
})
|
||||
email_data.push({
|
||||
name: '',
|
||||
y: campaign.results.length - count
|
||||
})
|
||||
var chart = $("#" + statusMapping[status] + "_chart").highcharts()
|
||||
chart.series[0].update({
|
||||
data: email_data
|
||||
})
|
||||
})
|
||||
/* Update the datatable */
|
||||
resultsTable = $("#resultsTable").DataTable()
|
||||
resultsTable.rows().every(function (i, tableLoop, rowLoop) {
|
||||
|
@ -436,42 +598,6 @@ function load() {
|
|||
payloadResults.show()
|
||||
}
|
||||
})
|
||||
// Setup our graphs
|
||||
var timeline_data = {
|
||||
series: [{
|
||||
name: "Events",
|
||||
data: []
|
||||
}]
|
||||
}
|
||||
var email_data = {
|
||||
series: []
|
||||
}
|
||||
var email_legend = {}
|
||||
var email_series_data = {}
|
||||
var timeline_opts = {
|
||||
axisX: {
|
||||
showGrid: false,
|
||||
type: Chartist.FixedScaleAxis,
|
||||
divisor: 5,
|
||||
labelInterpolationFnc: function(value) {
|
||||
return moment(value).format('MMMM Do YYYY h:mm a')
|
||||
}
|
||||
},
|
||||
axisY: {
|
||||
type: Chartist.FixedScaleAxis,
|
||||
ticks: [0, 1, 2],
|
||||
low: 0,
|
||||
showLabel: false
|
||||
},
|
||||
showArea: false,
|
||||
plugins: []
|
||||
}
|
||||
var email_opts = {
|
||||
donut: true,
|
||||
donutWidth: 40,
|
||||
chartPadding: 0,
|
||||
showLabel: false
|
||||
}
|
||||
// Setup the results table
|
||||
resultsTable = $("#resultsTable").DataTable({
|
||||
destroy: true,
|
||||
|
@ -490,6 +616,11 @@ function load() {
|
|||
}]
|
||||
});
|
||||
resultsTable.clear();
|
||||
var email_series_data = {}
|
||||
var timeline_series_data = []
|
||||
Object.keys(statusMapping).forEach(function (k) {
|
||||
email_series_data[k] = 0
|
||||
});
|
||||
$.each(campaign.results, function (i, result) {
|
||||
label = statuses[result.status].label || "label-default";
|
||||
resultsTable.row.add([
|
||||
|
@ -501,10 +632,11 @@ function load() {
|
|||
escapeHtml(result.position) || "",
|
||||
"<span class=\"label " + label + "\">" + result.status + "</span>"
|
||||
]).draw()
|
||||
if (!email_series_data[result.status]) {
|
||||
email_series_data[result.status] = 1
|
||||
} else {
|
||||
email_series_data[result.status]++;
|
||||
// Backfill status values
|
||||
var step = progressListing.indexOf(result.status)
|
||||
for (var i = 0; i < step; i++) {
|
||||
email_series_data[progressListing[i]]++
|
||||
}
|
||||
})
|
||||
// Setup the individual timelines
|
||||
|
@ -529,87 +661,41 @@ function load() {
|
|||
});
|
||||
// Setup the graphs
|
||||
$.each(campaign.timeline, function (i, event) {
|
||||
timeline_data.series[0].data.push({
|
||||
meta: i,
|
||||
x: new Date(event.time),
|
||||
y: 1
|
||||
var event_date = moment(event.time)
|
||||
timeline_series_data.push({
|
||||
email: event.email,
|
||||
message: event.message,
|
||||
x: event_date.valueOf(),
|
||||
y: 1,
|
||||
marker: {
|
||||
fillColor: statuses[event.message].color
|
||||
}
|
||||
})
|
||||
})
|
||||
$("#email_chart_legend").html("")
|
||||
renderTimelineChart({
|
||||
data: timeline_series_data
|
||||
})
|
||||
$.each(email_series_data, function (status, count) {
|
||||
email_data.series.push({
|
||||
meta: status,
|
||||
value: count
|
||||
})
|
||||
$("#email_chart_legend").append('<li><span class="' + statuses[status].legend + '"></span>' + status + '</li>')
|
||||
})
|
||||
var timeline_chart = new Chartist.Line('#timeline_chart', timeline_data, timeline_opts)
|
||||
timeline_chart.on('draw', function(data) {
|
||||
if (data.type === "point") {
|
||||
var point_style = statuses[campaign.timeline[data.meta].message].point
|
||||
var circle = new Chartist.Svg("circle", {
|
||||
cx: [data.x],
|
||||
cy: [data.y],
|
||||
r: 5,
|
||||
fill: "#283F50",
|
||||
meta: data.meta,
|
||||
value: 1,
|
||||
}, point_style + ' ct-timeline-point')
|
||||
data.element.replace(circle)
|
||||
var email_data = []
|
||||
if (!(status in statusMapping)) {
|
||||
return true
|
||||
}
|
||||
email_data.push({
|
||||
name: status,
|
||||
y: count
|
||||
})
|
||||
email_data.push({
|
||||
name: '',
|
||||
y: campaign.results.length - count
|
||||
})
|
||||
var chart = renderPieChart({
|
||||
elemId: statusMapping[status] + '_chart',
|
||||
title: status,
|
||||
name: status,
|
||||
data: email_data,
|
||||
colors: [statuses[status].color, '#dddddd']
|
||||
})
|
||||
// Setup the overview chart listeners
|
||||
$chart = $("#timeline_chart")
|
||||
var $toolTip = $chart
|
||||
.append('<div class="chartist-tooltip"></div>')
|
||||
.find('.chartist-tooltip')
|
||||
.hide();
|
||||
$chart.on('mouseenter', '.ct-timeline-point', function() {
|
||||
var $point = $(this)
|
||||
cidx = $point.attr('meta')
|
||||
html = "Event: " + campaign.timeline[cidx].message
|
||||
if (campaign.timeline[cidx].email) {
|
||||
html += '<br>' + "Email: " + escapeHtml(campaign.timeline[cidx].email)
|
||||
}
|
||||
$toolTip.html(html).show()
|
||||
});
|
||||
$chart.on('mouseleave', '.ct-timeline-point', function() {
|
||||
$toolTip.hide();
|
||||
});
|
||||
$chart.on('mousemove', function(event) {
|
||||
$toolTip.css({
|
||||
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
|
||||
top: (event.offsetY + 70 || event.originalEvent.layerY) - $toolTip.height() - 40
|
||||
});
|
||||
});
|
||||
|
||||
var email_chart = new Chartist.Pie("#email_chart", email_data, email_opts)
|
||||
email_chart.on('draw', function(data) {
|
||||
data.element.addClass(statuses[data.meta].slice)
|
||||
})
|
||||
// Setup the average chart listeners
|
||||
$piechart = $("#email_chart")
|
||||
var $pietoolTip = $piechart
|
||||
.append('<div class="chartist-tooltip"></div>')
|
||||
.find('.chartist-tooltip')
|
||||
.hide();
|
||||
|
||||
$piechart.on('mouseenter', '.ct-slice-donut', function() {
|
||||
var $point = $(this)
|
||||
value = $point.attr('ct:value')
|
||||
label = $point.attr('ct:meta')
|
||||
$pietoolTip.html(label + ': ' + value.toString()).show();
|
||||
});
|
||||
|
||||
$piechart.on('mouseleave', '.ct-slice-donut', function() {
|
||||
$pietoolTip.hide();
|
||||
});
|
||||
$piechart.on('mousemove', function(event) {
|
||||
$pietoolTip.css({
|
||||
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
|
||||
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
|
||||
});
|
||||
});
|
||||
if (!map) {
|
||||
map = new Datamap({
|
||||
element: document.getElementById("resultsMap"),
|
||||
|
@ -692,8 +778,12 @@ function refresh() {
|
|||
|
||||
|
||||
$(document).ready(function () {
|
||||
Highcharts.setOptions({
|
||||
global: {
|
||||
useUTC: false
|
||||
}
|
||||
})
|
||||
load();
|
||||
// Start the polling loop
|
||||
|
||||
// Start the polling loop
|
||||
setRefresh = setTimeout(refresh, 60000)
|
||||
|
|
|
@ -3,15 +3,13 @@ var campaigns = []
|
|||
// statuses is a helper map to point result statuses to ui classes
|
||||
var statuses = {
|
||||
"Email Sent": {
|
||||
slice: "ct-slice-donut-sent",
|
||||
legend: "ct-legend-sent",
|
||||
color: "#1abc9c",
|
||||
label: "label-success",
|
||||
icon: "fa-envelope",
|
||||
point: "ct-point-sent"
|
||||
},
|
||||
"Emails Sent": {
|
||||
slice: "ct-slice-donut-sent",
|
||||
legend: "ct-legend-sent",
|
||||
color: "#1abc9c",
|
||||
label: "label-success",
|
||||
icon: "fa-envelope",
|
||||
point: "ct-point-sent"
|
||||
|
@ -26,57 +24,49 @@ var statuses = {
|
|||
label: "label-success"
|
||||
},
|
||||
"Email Opened": {
|
||||
slice: "ct-slice-donut-opened",
|
||||
legend: "ct-legend-opened",
|
||||
color: "#f9bf3b",
|
||||
label: "label-warning",
|
||||
icon: "fa-envelope",
|
||||
point: "ct-point-opened"
|
||||
},
|
||||
"Clicked Link": {
|
||||
slice: "ct-slice-donut-clicked",
|
||||
legend: "ct-legend-clicked",
|
||||
color: "#F39C12",
|
||||
label: "label-clicked",
|
||||
icon: "fa-mouse-pointer",
|
||||
point: "ct-point-clicked"
|
||||
},
|
||||
"Success": {
|
||||
slice: "ct-slice-donut-success",
|
||||
legend: "ct-legend-success",
|
||||
color: "#f05b4f",
|
||||
label: "label-danger",
|
||||
icon: "fa-exclamation",
|
||||
point: "ct-point-clicked"
|
||||
},
|
||||
"Error": {
|
||||
slice: "ct-slice-donut-error",
|
||||
legend: "ct-legend-error",
|
||||
color: "#6c7a89",
|
||||
label: "label-default",
|
||||
icon: "fa-times",
|
||||
point: "ct-point-error"
|
||||
},
|
||||
"Error Sending Email": {
|
||||
slice: "ct-slice-donut-error",
|
||||
legend: "ct-legend-error",
|
||||
color: "#6c7a89",
|
||||
label: "label-default",
|
||||
icon: "fa-times",
|
||||
point: "ct-point-error"
|
||||
},
|
||||
"Submitted Data": {
|
||||
slice: "ct-slice-donut-success",
|
||||
legend: "ct-legend-success",
|
||||
color: "#f05b4f",
|
||||
label: "label-danger",
|
||||
icon: "fa-exclamation",
|
||||
point: "ct-point-clicked"
|
||||
},
|
||||
"Unknown": {
|
||||
slice: "ct-slice-donut-error",
|
||||
legend: "ct-legend-error",
|
||||
color: "#6c7a89",
|
||||
label: "label-default",
|
||||
icon: "fa-question",
|
||||
point: "ct-point-error"
|
||||
},
|
||||
"Sending": {
|
||||
slice: "ct-slice-donut-sending",
|
||||
legend: "ct-legend-sending",
|
||||
color: "#428bca",
|
||||
label: "label-primary",
|
||||
icon: "fa-spinner",
|
||||
point: "ct-point-sending"
|
||||
|
@ -92,7 +82,6 @@ var statsMapping = {
|
|||
"opened": "Email Opened",
|
||||
"clicked": "Clicked Link",
|
||||
"submitted_data": "Submitted Data",
|
||||
"error": "Error"
|
||||
}
|
||||
|
||||
function deleteCampaign(idx) {
|
||||
|
@ -105,17 +94,64 @@ function deleteCampaign(idx) {
|
|||
}
|
||||
}
|
||||
|
||||
function generateStatsPieChart(campaigns) {
|
||||
var stats_opts = {
|
||||
donut: true,
|
||||
donutWidth: 40,
|
||||
chartPadding: 0,
|
||||
showLabel: false
|
||||
/* Renders a pie chart using the provided chartops */
|
||||
function renderPieChart(chartopts) {
|
||||
return Highcharts.chart(chartopts['elemId'], {
|
||||
chart: {
|
||||
type: 'pie',
|
||||
events: {
|
||||
load: function () {
|
||||
var chart = this,
|
||||
rend = chart.renderer,
|
||||
pie = chart.series[0],
|
||||
left = chart.plotLeft + pie.center[0],
|
||||
top = chart.plotTop + pie.center[1];
|
||||
this.innerText = rend.text(chartopts['data'][0].count, left, top).
|
||||
attr({
|
||||
'text-anchor': 'middle',
|
||||
'font-size': '24px',
|
||||
'font-weight': 'bold',
|
||||
'fill': chartopts['colors'][0],
|
||||
'font-family': 'Helvetica,Arial,sans-serif'
|
||||
}).add();
|
||||
},
|
||||
render: function () {
|
||||
this.innerText.attr({ text: chartopts['data'][0].count })
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: chartopts['title']
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
innerSize: '80%',
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function () {
|
||||
if (this.key == undefined) {
|
||||
return false
|
||||
}
|
||||
return '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '%</b><br/>'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
data: chartopts['data'],
|
||||
colors: chartopts['colors'],
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
function generateStatsPieCharts(campaigns) {
|
||||
var stats_data = []
|
||||
var stats_series_data = {}
|
||||
var stats_data = {
|
||||
series: []
|
||||
}
|
||||
var total = 0
|
||||
|
||||
$.each(campaigns, function (i, campaign) {
|
||||
|
@ -134,44 +170,108 @@ function generateStatsPieChart(campaigns) {
|
|||
$.each(stats_series_data, function (status, count) {
|
||||
// I don't like this, but I guess it'll have to work.
|
||||
// Turns submitted_data into Submitted Data
|
||||
if (!(status in statsMapping)) {
|
||||
return true
|
||||
}
|
||||
status_label = statsMapping[status]
|
||||
stats_data.series.push({
|
||||
meta: status_label,
|
||||
value: Math.floor((count / total) * 100)
|
||||
stats_data.push({
|
||||
name: status_label,
|
||||
y: Math.floor((count / total) * 100),
|
||||
count: count
|
||||
})
|
||||
$("#stats_chart_legend").append('<li><span class="' + statuses[status_label].legend + '"></span>' + status_label + '</li>')
|
||||
stats_data.push({
|
||||
name: '',
|
||||
y: 100 - Math.floor((count / total) * 100)
|
||||
})
|
||||
|
||||
var stats_chart = new Chartist.Pie("#stats_chart", stats_data, stats_opts)
|
||||
|
||||
$piechart = $("#stats_chart")
|
||||
var $pietoolTip = $piechart
|
||||
.append('<div class="chartist-tooltip"></div>')
|
||||
.find('.chartist-tooltip')
|
||||
.hide();
|
||||
|
||||
$piechart.get(0).__chartist__.on('draw', function(data) {
|
||||
data.element.addClass(statuses[data.meta].slice)
|
||||
var stats_chart = renderPieChart({
|
||||
elemId: status + '_chart',
|
||||
title: status_label,
|
||||
name: status,
|
||||
data: stats_data,
|
||||
colors: [statuses[status_label].color, "#dddddd"]
|
||||
})
|
||||
// Update with the latest data
|
||||
$piechart.get(0).__chartist__.update(stats_data)
|
||||
stats_data = []
|
||||
});
|
||||
}
|
||||
|
||||
$piechart.on('mouseenter', '.ct-slice-donut', function() {
|
||||
var $point = $(this)
|
||||
value = $point.attr('ct:value')
|
||||
label = $point.attr('ct:meta')
|
||||
$pietoolTip.html(label + ': ' + value.toString() + "%").show();
|
||||
});
|
||||
|
||||
$piechart.on('mouseleave', '.ct-slice-donut', function() {
|
||||
$pietoolTip.hide();
|
||||
});
|
||||
$piechart.on('mousemove', function(event) {
|
||||
$pietoolTip.css({
|
||||
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
|
||||
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
|
||||
});
|
||||
});
|
||||
function generateTimelineChart(campaigns) {
|
||||
var overview_data = []
|
||||
$.each(campaigns, function (i, campaign) {
|
||||
var campaign_date = moment(campaign.created_date)
|
||||
// Add it to the chart data
|
||||
campaign.y = 0
|
||||
// Clicked events also contain our data submitted events
|
||||
campaign.y += campaign.stats.clicked
|
||||
campaign.y = Math.floor((campaign.y / campaign.stats.total) * 100)
|
||||
// Add the data to the overview chart
|
||||
overview_data.push({
|
||||
campaign_id: campaign.id,
|
||||
name: campaign.name,
|
||||
x: campaign_date.valueOf(),
|
||||
y: campaign.y
|
||||
})
|
||||
})
|
||||
Highcharts.chart('overview_chart', {
|
||||
chart: {
|
||||
zoomType: 'x',
|
||||
type: 'areaspline'
|
||||
},
|
||||
title: {
|
||||
text: 'Phishing Success Overview'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
dateTimeLabelFormats: {
|
||||
second: '%l:%M:%S',
|
||||
minute: '%l:%M',
|
||||
hour: '%l:%M',
|
||||
day: '%b %d, %Y',
|
||||
week: '%b %d, %Y',
|
||||
month: '%b %Y'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
title: {
|
||||
text: "% of Success"
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
formatter: function () {
|
||||
return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) +
|
||||
'<br>' + this.point.name + '<br>% Success: <b>' + this.y + '%</b>'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
enabled: false
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
marker: {
|
||||
enabled: true,
|
||||
symbol: 'circle',
|
||||
radius: 3
|
||||
},
|
||||
cursor: 'pointer',
|
||||
point: {
|
||||
events: {
|
||||
click: function (e) {
|
||||
window.location.href = "/campaigns/" + this.campaign_id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
credits: {
|
||||
enabled: false
|
||||
},
|
||||
series: [{
|
||||
data: overview_data,
|
||||
color: "#f05b4f",
|
||||
fillOpacity: 0.5
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
@ -182,26 +282,15 @@ $(document).ready(function() {
|
|||
if (campaigns.length > 0) {
|
||||
$("#dashboard").show()
|
||||
// Create the overview chart data
|
||||
var overview_data = {
|
||||
labels: [],
|
||||
series: [
|
||||
[]
|
||||
]
|
||||
}
|
||||
var overview_opts = {
|
||||
axisX: {
|
||||
showGrid: false
|
||||
},
|
||||
showArea: true,
|
||||
plugins: [],
|
||||
low: 0,
|
||||
high: 100
|
||||
}
|
||||
campaignTable = $("#campaignTable").DataTable({
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}],
|
||||
},
|
||||
{ className: "color-sent", targets: [2] },
|
||||
{ className: "color-opened", targets: [3] },
|
||||
{ className: "color-clicked", targets: [4] },
|
||||
{ className: "color-success", targets: [5] }],
|
||||
order: [
|
||||
[1, "desc"]
|
||||
]
|
||||
|
@ -222,6 +311,10 @@ $(document).ready(function() {
|
|||
campaignTable.row.add([
|
||||
escapeHtml(campaign.name),
|
||||
campaign_date,
|
||||
campaign.stats.sent,
|
||||
campaign.stats.opened,
|
||||
campaign.stats.clicked,
|
||||
campaign.stats.submitted_data,
|
||||
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
|
||||
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
|
||||
<i class='fa fa-bar-chart'></i>\
|
||||
|
@ -231,48 +324,10 @@ $(document).ready(function() {
|
|||
</button></div>"
|
||||
]).draw()
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
// Add it to the chart data
|
||||
campaign.y = 0
|
||||
campaign.y += campaign.stats.clicked + campaign.stats.submitted_data
|
||||
campaign.y = Math.floor((campaign.y / campaign.stats.total) * 100)
|
||||
// Add the data to the overview chart
|
||||
overview_data.labels.push(campaign_date)
|
||||
overview_data.series[0].push({
|
||||
meta: i,
|
||||
value: campaign.y
|
||||
})
|
||||
})
|
||||
// Build the charts
|
||||
generateStatsPieChart(campaigns)
|
||||
var overview_chart = new Chartist.Line('#overview_chart', overview_data, overview_opts)
|
||||
|
||||
// Setup the overview chart listeners
|
||||
$chart = $("#overview_chart")
|
||||
var $toolTip = $chart
|
||||
.append('<div class="chartist-tooltip"></div>')
|
||||
.find('.chartist-tooltip')
|
||||
.hide();
|
||||
|
||||
$chart.on('mouseenter', '.ct-point', function() {
|
||||
var $point = $(this)
|
||||
value = $point.attr('ct:value') || 0
|
||||
cidx = $point.attr('ct:meta')
|
||||
$toolTip.html(campaigns[cidx].name + '<br>' + "Successes: " + value.toString() + "%").show();
|
||||
});
|
||||
|
||||
$chart.on('mouseleave', '.ct-point', function() {
|
||||
$toolTip.hide();
|
||||
});
|
||||
$chart.on('mousemove', function(event) {
|
||||
$toolTip.css({
|
||||
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
|
||||
top: (event.offsetY + 40 || event.originalEvent.layerY) - $toolTip.height() - 40
|
||||
});
|
||||
});
|
||||
$("#overview_chart").on("click", ".ct-point", function(e) {
|
||||
var $cidx = $(this).attr('ct:meta');
|
||||
window.location.href = "/campaigns/" + campaigns[cidx].id
|
||||
});
|
||||
generateStatsPieCharts(campaigns)
|
||||
generateTimelineChart(campaigns)
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
</li>
|
||||
<li><a href="/settings">Settings</a>
|
||||
</li>
|
||||
<li><hr></li>
|
||||
<li>
|
||||
<hr>
|
||||
</li>
|
||||
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
|
||||
</li>
|
||||
<li><a href="/api/">API Documentation</a>
|
||||
|
@ -39,7 +41,8 @@
|
|||
<i class="fa fa-arrow-circle-o-left fa-lg"></i> Back
|
||||
</a>
|
||||
<div class="btn-group">
|
||||
<button type="button" id="exportButton" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<button type="button" id="exportButton" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="true">
|
||||
<i class="fa fa-file-excel-o"></i> Export CSV
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</button>
|
||||
|
@ -65,25 +68,20 @@
|
|||
<div class="row">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active"><a href="#overview" aria-controls="home" role="tab" data-toggle="tab">Overview</a></li>
|
||||
<!--<li><a href="#plugins" aria-controls="profile" role="tab" data-toggle="tab">Plugins</a></li>
|
||||
<li><a href="#demographics" aria-controls="settings" role="tab" data-toggle="tab">Demographics</a></li>-->
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="overview">
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<p style="text-align:center;">Campaign Timeline</p>
|
||||
<br/>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div id="timeline_chart"></div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<p style="text-align:center;">Email Status</p>
|
||||
<div id="email_chart" class="col-lg-7 col-md-7"></div>
|
||||
<div class="col-lg-5 col-md-5">
|
||||
<ul id="email_chart_legend" class="chartist-legend">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@ -93,12 +91,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div role="tabpanel" class="tab-pane" id="plugins">
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="demographics">
|
||||
Demographics here
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@ -122,7 +114,6 @@
|
|||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "scripts"}}
|
||||
{{end}} {{define "scripts"}}
|
||||
<script src="/js/dist/app/campaign_results.min.js"></script>
|
||||
{{end}}
|
|
@ -38,19 +38,15 @@
|
|||
No campaigns created yet. Let's create one!
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashboard" style="display:none;">
|
||||
<div id="dashboard">
|
||||
<div class="row">
|
||||
<div id="overview_chart" class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<p style="text-align:center;">Phishing Success Overview</p>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
|
||||
<p style="text-align:center;">Average Phishing Results</p>
|
||||
<div id="stats_chart" class="col-lg-7 col-md-7"></div>
|
||||
<div class="col-lg-5 col-md-5">
|
||||
<ul id="stats_chart_legend" class="chartist-legend">
|
||||
</ul>
|
||||
</div>
|
||||
<div id="overview_chart" style="height:200px;" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h2>Recent Campaigns</h2>
|
||||
|
@ -63,10 +59,14 @@
|
|||
<table id="campaignTable" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created Date</th>
|
||||
<th>Status</th>
|
||||
<th class="col-md-2 col-sm-2 no-sort"></th>
|
||||
<th class="col-md-2 col-sm-2">Name</th>
|
||||
<th class="col-md-2 col-sm-2">Created Date</th>
|
||||
<th class="col-md-1 col-sm-1"><i class="fa fa-envelope-o"></i></th>
|
||||
<th class="col-md-1 col-sm-1"><i class="fa fa-envelope-open-o"></i></th>
|
||||
<th class="col-md-1 col-sm-1"><i class="fa fa-mouse-pointer"></i></th>
|
||||
<th class="col-md-1 col-sm-1"><i class="fa fa-exclamation-circle"></i></th>
|
||||
<th class="col-md-1 col-sm-1">Status</th>
|
||||
<th class="col-md-2 col-sm-2 no-sort"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
Loading…
Reference in New Issue