1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-05 15:57:23 +01:00

Add IMAP connection reporting

Fix binsizes and barwidths on timeseries charts
Fix timezone issue in timeseries scales
This commit is contained in:
downtownallday
2021-04-08 12:53:32 -04:00
parent ac811bcbd1
commit 721dd1273f
18 changed files with 358 additions and 123 deletions

View File

@@ -57,7 +57,7 @@ Vue.component('chart-multi-line-timeseries', {
.text("no data");
}
this.xscale = d3.scaleUtc()
this.xscale = d3.scaleTime()
.domain(d3.extent(this.tsdata.dates))
.nice()
.range([this.margin.left, this.width - this.margin.right])

View File

@@ -101,13 +101,14 @@ Vue.component('chart-stacked-bar-timeseries', {
.text("no data");
}
this.xscale = d3.scaleUtc()
this.xscale = d3.scaleTime()
.domain(d3.extent(this.tsdata.dates))
.nice()
.range([this.margin.left, this.width - this.margin.right])
var barwidth = this.tsdata.barwidth(this.xscale, 1);
var padding = barwidth / 2;
var barwidth = this.tsdata.barwidth(this.xscale);
var padding_x = barwidth / 2;
var padding_y = ChartVue.get_yAxisLegendBounds(this.tsdata).height + 2;
this.yscale = d3.scaleLinear()
.domain([
@@ -115,28 +116,30 @@ Vue.component('chart-stacked-bar-timeseries', {
d3.sum(this.tsdata.series, s => d3.max(s.values))
])
.range([
this.height - this.margin.bottom,
this.height - this.margin.bottom - padding_y,
this.margin.top,
]);
svg.append("g")
.call(this.xAxis.bind(this, padding))
var g = svg.append("g")
.attr("transform", `translate(0, ${padding_y})`);
g.append("g")
.call(this.xAxis.bind(this, padding_x))
.attr("font-size", ChartPrefs.axis_font_size);
svg.append("g")
.call(this.yAxis.bind(this))
g.append("g")
.call(this.yAxis.bind(this, padding_y))
.attr("font-size", ChartPrefs.axis_font_size);
for (var s_idx=0; s_idx<this.tsdata.series.length; s_idx++) {
svg.append("g")
g.append("g")
.datum(s_idx)
.attr("fill", this.colors[s_idx])
.selectAll("rect")
.data(this.stacked[s_idx])
.join("rect")
.attr("x", d => this.xscale(d.data.date) - barwidth/2 + padding)
.attr("y", d => this.yscale(d[1]))
.attr("x", d => this.xscale(d.data.date) - barwidth/2 + padding_x)
.attr("y", d => this.yscale(d[1]) + padding_y)
.attr("height", d => this.yscale(d[0]) - this.yscale(d[1]))
.attr("width", barwidth)
.call( hover.bind(this) )
@@ -146,7 +149,13 @@ Vue.component('chart-stacked-bar-timeseries', {
;
}
var hovinfo = svg.append("g");
g.append("g")
.attr("transform", `translate(${this.margin.left}, 0)`)
.call(
g => ChartVue.add_yAxisLegend(g, this.tsdata, this.colors)
);
var hovinfo = g.append("g");
function hover(rect) {
if ("ontouchstart" in document) rect
@@ -165,10 +174,11 @@ Vue.component('chart-stacked-bar-timeseries', {
var s_name = this.tsdata.series[s_idx].name;
var v = d.data[s_name];
var x = Number(rect.attr('x')) + barwidth/2;
//var y = Number(rect.attr('y')) + Number(rect.attr('height'))/2;
var y = Number(rect.attr('y'));
hovinfo.attr(
"transform",
`translate( ${x}, ${rect.attr('y')} )`)
`translate( ${x}, ${y} )`)
.append('text')
.attr("font-family", ChartPrefs.default_font_family)
.attr("font-size", ChartPrefs.default_font_size)
@@ -203,18 +213,16 @@ Vue.component('chart-stacked-bar-timeseries', {
return x;
},
yAxis: function(g) {
yAxis: function(padding, g) {
var y = g.attr(
"transform",
`translate(${this.margin.left},0)`
`translate(${this.margin.left},${padding})`
).call(
d3.axisLeft(this.yscale)
.ticks(this.height/50)
).call(g =>
g.select(".domain").remove()
).call(g => {
ChartVue.add_yAxisLegend(g, this.tsdata, this.colors);
});
).call(
g => g.select(".domain").remove()
);
return y;
},

View File

@@ -748,6 +748,15 @@ class ChartVue {
return svg;
}
static get_yAxisLegendBounds(data) {
const h = ChartPrefs.axis_font_size;
return {
width: h + 6,
height: h * data.series.length
};
}
static add_yAxisLegend(g, data, colors) {
//var gtick = g.select(".tick:last-of-type").append("g");
const h = ChartPrefs.axis_font_size;
@@ -853,9 +862,8 @@ class TimeseriesData {
}
static binsizeOfRange(range) {
// target 100-120 datapoints
const target = 100;
const tolerance = 0.2; // 20%
// target roughly 75 datapoints
const target = 75;
if (typeof range[0] == 'string') {
var parser = d3.utcParse('%Y-%m-%d %H:%M:%S');
@@ -865,27 +873,46 @@ class TimeseriesData {
const span_min = Math.ceil(
(range[1].getTime() - range[0].getTime()) / (1000*60*target)
);
const bin_days = Math.floor(span_min / (24*60));
const bin_hours = Math.floor((span_min - bin_days*24*60) / 60);
var bin_days = Math.floor(span_min / (24*60));
var bin_hours = Math.floor((span_min - bin_days*24*60) / 60);
if (bin_days >= 1) {
return bin_days * 24 * 60 +
(bin_hours > (24 * tolerance) ? bin_hours*60: 0);
if (bin_hours > 18) {
bin_days += 1;
bin_hours = 0;
}
else if (bin_hours > 6) {
bin_hours = 12;
}
else {
bin_hours = 0;
}
return bin_days * 24 * 60 + bin_hours*60;
}
const bin_mins = span_min - bin_days*24*60 - bin_hours*60;
if (bin_hours >= 1) {
return bin_hours * 60 +
(bin_mins > (60 * tolerance) ? bin_mins: 0 );
var bin_mins = span_min - bin_days*24*60 - bin_hours*60;
if (bin_mins > 45) {
bin_hours += 1
bin_mins = 0;
}
return bin_mins;
else if (bin_mins > 15) {
bin_mins = 30;
}
else {
bin_mins = 0;
}
return bin_hours * 60 + bin_mins;
}
barwidth(xscale, barspacing) {
barwidth(xscale, barspacing, max_width) {
/* get the width of a bar in a bar chart */
var start = this.range[0];
var end = this.range[1];
var bins = (end.getTime() - start.getTime()) / (1000 * this.binsizeTimespan());
return Math.max(1, (xscale.range()[1] - xscale.range()[0])/bins - (barspacing || 0));
if (this.dates.length == 0) return 0; // no data
barspacing = (barspacing === undefined) ? 2 : barspacing;
max_width = (max_width === undefined) ? 75 : max_width;
var first_date = this.dates[0];
var last_date = this.dates[this.dates.length-1];
var bins = (last_date.getTime() - first_date.getTime()) / (1000 * 60 * this.binsize);
return Math.min(max_width, Math.max(1, (xscale(last_date) - xscale(first_date))/bins - barspacing));
}
formatDateTimeLong(d) {

View File

@@ -50,7 +50,7 @@ Vue.component('panel-messages-sent', function(resolve, reject) {
},
height_recip: function() {
return this.height / 2;
return (this.height / 3) *2;
},
radius_recip_pie: function() {

View File

@@ -11,7 +11,7 @@
<b-button variant="primary" @click="change_user">Change user</b-button>
</b-input-group-append>
</b-input-group>
<b-alert variant="warning" class="ml-2" :show="sent_mail && sent_mail.items.length>=get_row_limit() || received_mail && received_mail.items.length>=get_row_limit()"><sup>*</sup> Tables limited to {{ get_row_limit() }} rows <router-link to="/settings"><b-icon icon="gear-fill"></b-icon></router-link></b-alert>
<b-alert variant="warning" class="ml-2" :show="sent_mail && sent_mail.items.length>=get_row_limit() || received_mail && received_mail.items.length>=get_row_limit() || imap_details && imap_details.items.length>=get_row_limit()"><sup>*</sup> Tables limited to {{ get_row_limit() }} rows <router-link to="/settings"><b-icon icon="gear-fill"></b-icon></router-link></b-alert>
<b-form-checkbox class="ml-auto" v-model="show_only_flagged" @change="show_only_flagged_change()">Flagged only</b-form-checkbox>
</b-form>
@@ -42,7 +42,7 @@
</b-table>
</b-tab>
<b-tab :title="`Received mail (${received_mail.items.length})`">
<b-tab>
<template #title>
Received mail<sup v-if="received_mail.items.length >= get_row_limit()">*</sup> ({{received_mail.items.length}})
</template>
@@ -72,5 +72,31 @@
</template>
</b-table>
</b-tab>
<b-tab>
<template #title>
IMAP Connections<sup v-if="imap_details.items.length >= get_row_limit()">*</sup> ({{imap_details.items.length}})
</template>
<b-table
class="sticky-table-header-0 bg-light"
small
:filter="show_only_flagged_filter"
:filter-function="table_filter_cb"
tbody-tr-class="cursor-pointer"
details-td-class="cursor-default"
@row-clicked="row_clicked"
:items="imap_details.items"
:fields="imap_details.fields">
<template #row-details="row">
<b-card>
<div><strong>Connection disposition</strong>: {{ disposition_formatter(row.item.disposition) }}</div>
<div><strong>Connection security</strong> {{ row.item.connection_security }}</div>
<div><strong>Disconnect reason</strong> {{ row.item.disconnect_reason }}</div>
</b-card>
</template>
</b-table>
</b-tab>
</b-tabs>
</div>

View File

@@ -28,6 +28,7 @@ Vue.component('panel-user-activity', function(resolve, reject) {
data_date_range: null, /* date range for active table data */
sent_mail: null,
received_mail: null,
imap_details: null,
all_users: [],
disposition_formatter: ConnectionDisposition.formatter,
};
@@ -147,6 +148,15 @@ Vue.component('panel-user-activity', function(resolve, reject) {
f.label = 'Envelope From (user)';
},
combine_imap_details_fields: function() {
// remove these fields
this.imap_details.combine_fields([
'disconnect_reason',
'connection_security',
]);
},
get_row_limit: function() {
return UserSettings.get().row_limit;
},
@@ -239,7 +249,18 @@ Vue.component('panel-user-activity', function(resolve, reject) {
this.received_mail
.flag_fields()
.get_field('connect_time')
.add_tdClass('text-nowrap');
.add_tdClass('text-nowrap');
/* setup imap_details */
this.imap_details = new MailBvTable(
response.data.imap_details, {
_showDetails: true
});
this.combine_imap_details_fields();
this.imap_details
.flag_fields()
.get_field('connect_time')
.add_tdClass('text-nowrap');
}).catch(error => {
this.$root.handleError(error);