2021-08-10 11:34:47 +00:00
< template >
2021-10-18 11:00:39 +00:00
< div >
< div class = "period-options" >
2024-05-19 20:03:32 +00:00
< button
type = "button" class = "btn btn-light dropdown-toggle btn-period-toggle" data - bs - toggle = "dropdown"
aria - expanded = "false"
>
2021-10-23 17:26:56 +00:00
{ { chartPeriodOptions [ chartPeriodHrs ] } } & nbsp ;
< / button >
< ul class = "dropdown-menu dropdown-menu-end" >
< li v-for ="(item, key) in chartPeriodOptions" :key ="key" >
2024-05-19 20:03:32 +00:00
< button
type = "button" class = "dropdown-item" : class = "{ active: chartPeriodHrs == key }"
@ click = "chartPeriodHrs = key"
>
{ { item } }
< / button >
2021-10-23 17:26:56 +00:00
< / li >
< / ul >
2021-10-18 11:00:39 +00:00
< / div >
2024-05-19 20:03:32 +00:00
< div class = "chart-wrapper" : class = "{ loading: loading }" >
2023-03-01 20:47:51 +00:00
< Line :data = "chartData" :options = "chartOptions" / >
2021-10-18 11:00:39 +00:00
< / div >
< / div >
2021-08-10 11:34:47 +00:00
< / template >
2022-09-27 16:20:17 +00:00
< script lang = "js" >
2021-08-11 16:31:21 +00:00
import { BarController , BarElement , Chart , Filler , LinearScale , LineController , LineElement , PointElement , TimeScale , Tooltip } from "chart.js" ;
2023-03-01 20:47:51 +00:00
import "chartjs-adapter-dayjs-4" ;
import { Line } from "vue-chartjs" ;
2024-05-19 20:03:32 +00:00
import { UP , DOWN , PENDING , MAINTENANCE } from "../util.ts" ;
2021-10-18 11:00:39 +00:00
2021-08-11 16:31:21 +00:00
Chart . register ( LineController , BarController , LineElement , PointElement , TimeScale , BarElement , LinearScale , Tooltip , Filler ) ;
2021-08-10 11:34:47 +00:00
export default {
2023-03-01 20:47:51 +00:00
components : { Line } ,
2021-08-10 11:34:47 +00:00
props : {
2022-06-01 23:32:05 +00:00
/** ID of monitor */
2021-08-10 11:34:47 +00:00
monitorId : {
type : Number ,
required : true ,
} ,
} ,
data ( ) {
return {
2021-10-26 04:48:21 +00:00
loading : false ,
2024-05-19 20:03:32 +00:00
// Time period for the chart to display, in hours
// Initial value is 0 as a workaround for triggering a data fetch on created()
chartPeriodHrs : "0" ,
2021-10-18 11:00:39 +00:00
2021-10-23 17:26:56 +00:00
chartPeriodOptions : {
0 : this . $t ( "recent" ) ,
3 : "3h" ,
6 : "6h" ,
24 : "24h" ,
168 : "1w" ,
} ,
2024-05-19 20:03:32 +00:00
chartRawData : null ,
chartDataFetchInterval : null ,
2021-08-10 11:34:47 +00:00
} ;
} ,
computed : {
chartOptions ( ) {
return {
responsive : true ,
2021-08-18 04:21:16 +00:00
maintainAspectRatio : false ,
onResize : ( chart ) => {
chart . canvas . parentNode . style . position = "relative" ;
if ( screen . width < 576 ) {
chart . canvas . parentNode . style . height = "275px" ;
} else if ( screen . width < 768 ) {
chart . canvas . parentNode . style . height = "320px" ;
} else if ( screen . width < 992 ) {
chart . canvas . parentNode . style . height = "300px" ;
} else {
chart . canvas . parentNode . style . height = "250px" ;
}
} ,
2021-08-10 11:34:47 +00:00
layout : {
padding : {
left : 10 ,
right : 30 ,
top : 30 ,
bottom : 10 ,
} ,
} ,
2021-08-11 13:00:33 +00:00
elements : {
point : {
2021-08-24 16:54:52 +00:00
// Hide points on chart unless mouse-over
2021-08-11 13:00:33 +00:00
radius : 0 ,
2021-08-24 16:54:52 +00:00
hitRadius : 100 ,
2021-08-11 13:00:33 +00:00
} ,
} ,
2021-08-10 11:34:47 +00:00
scales : {
x : {
type : "time" ,
time : {
2021-08-16 15:58:02 +00:00
minUnit : "minute" ,
round : "second" ,
tooltipFormat : "YYYY-MM-DD HH:mm:ss" ,
displayFormats : {
minute : "HH:mm" ,
hour : "MM-DD HH:mm" ,
}
2021-08-11 13:00:33 +00:00
} ,
ticks : {
2023-03-01 20:47:51 +00:00
sampleSize : 3 ,
2021-08-11 13:00:33 +00:00
maxRotation : 0 ,
2021-08-16 15:58:02 +00:00
autoSkipPadding : 30 ,
2023-03-01 20:47:51 +00:00
padding : 3 ,
2021-08-11 13:00:33 +00:00
} ,
grid : {
color : this . $root . theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)" ,
2021-08-24 15:34:48 +00:00
offset : false ,
2021-08-10 11:34:47 +00:00
} ,
} ,
y : {
title : {
display : true ,
2021-09-01 19:17:50 +00:00
text : this . $t ( "respTime" ) ,
2021-08-10 11:34:47 +00:00
} ,
2021-08-11 13:00:33 +00:00
offset : false ,
grid : {
color : this . $root . theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)" ,
} ,
} ,
y1 : {
display : false ,
position : "right" ,
grid : {
drawOnChartArea : false ,
} ,
min : 0 ,
max : 1 ,
offset : false ,
} ,
2021-08-10 11:34:47 +00:00
} ,
bounds : "ticks" ,
plugins : {
2021-08-11 13:00:33 +00:00
tooltip : {
2021-08-11 16:31:21 +00:00
mode : "nearest" ,
intersect : false ,
padding : 10 ,
2021-08-24 16:54:52 +00:00
backgroundColor : this . $root . theme === "light" ? "rgba(212,232,222,1.0)" : "rgba(32,42,38,1.0)" ,
bodyColor : this . $root . theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)" ,
titleColor : this . $root . theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)" ,
2021-08-11 13:00:33 +00:00
filter : function ( tooltipItem ) {
2021-08-24 16:54:52 +00:00
return tooltipItem . datasetIndex === 0 ; // Hide tooltip on Bar Chart
2021-08-11 13:00:33 +00:00
} ,
2021-08-11 16:31:21 +00:00
callbacks : {
label : ( context ) => {
2021-10-18 11:00:39 +00:00
return ` ${ new Intl . NumberFormat ( ) . format ( context . parsed . y ) } ms ` ;
2021-08-11 16:31:21 +00:00
} ,
}
2021-08-11 13:00:33 +00:00
} ,
2021-08-10 11:34:47 +00:00
legend : {
display : false ,
} ,
} ,
2021-10-18 11:00:39 +00:00
} ;
2021-08-10 11:34:47 +00:00
} ,
chartData ( ) {
2024-05-19 20:03:32 +00:00
if ( this . chartPeriodHrs === "0" ) {
return this . getChartDatapointsFromHeartbeatList ( ) ;
} else {
return this . getChartDatapointsFromStats ( ) ;
}
} ,
} ,
watch : {
// Update chart data when the selected chart period changes
chartPeriodHrs : function ( newPeriod ) {
if ( this . chartDataFetchInterval ) {
clearInterval ( this . chartDataFetchInterval ) ;
this . chartDataFetchInterval = null ;
}
// eslint-disable-next-line eqeqeq
if ( newPeriod == "0" ) {
this . heartbeatList = null ;
this . $root . storage ( ) . removeItem ( ` chart-period- ${ this . monitorId } ` ) ;
} else {
this . loading = true ;
let period ;
try {
period = parseInt ( newPeriod ) ;
} catch ( e ) {
// Invalid period
period = 24 ;
}
this . $root . getMonitorChartData ( this . monitorId , period , ( res ) => {
if ( ! res . ok ) {
this . $root . toastError ( res . msg ) ;
} else {
this . chartRawData = res . data ;
this . $root . storage ( ) [ ` chart-period- ${ this . monitorId } ` ] = newPeriod ;
}
this . loading = false ;
} ) ;
this . chartDataFetchInterval = setInterval ( ( ) => {
this . $root . getMonitorChartData ( this . monitorId , period , ( res ) => {
if ( res . ok ) {
this . chartRawData = res . data ;
}
} ) ;
} , 5 * 60 * 1000 ) ;
}
}
} ,
created ( ) {
// Load chart period from storage if saved
let period = this . $root . storage ( ) [ ` chart-period- ${ this . monitorId } ` ] ;
if ( period != null ) {
// Has this ever been not a string?
if ( typeof period !== "string" ) {
period = period . toString ( ) ;
}
this . chartPeriodHrs = period ;
} else {
this . chartPeriodHrs = "24" ;
}
} ,
beforeUnmount ( ) {
if ( this . chartDataFetchInterval ) {
clearInterval ( this . chartDataFetchInterval ) ;
}
} ,
methods : {
// Get color of bar chart for this datapoint
getBarColorForDatapoint ( datapoint ) {
if ( datapoint . maintenance != null ) {
// Target is in maintenance
return "rgba(23,71,245,0.41)" ;
} else if ( datapoint . down === 0 ) {
// Target is up, no need to display a bar
return "#000" ;
} else if ( datapoint . up === 0 ) {
// Target is down
return "rgba(220, 53, 69, 0.41)" ;
} else {
// Show yellow for mixed status
return "rgba(245, 182, 23, 0.41)" ;
}
} ,
// push datapoint to chartData
pushDatapoint ( datapoint , avgPingData , minPingData , maxPingData , downData , colorData ) {
const x = this . $root . unixToDateTime ( datapoint . timestamp ) ;
// Show ping values if it was up in this period
avgPingData . push ( {
x ,
y : datapoint . up > 0 && datapoint . avgPing > 0 ? datapoint . avgPing : null ,
} ) ;
minPingData . push ( {
x ,
y : datapoint . up > 0 && datapoint . avgPing > 0 ? datapoint . minPing : null ,
} ) ;
maxPingData . push ( {
x ,
y : datapoint . up > 0 && datapoint . avgPing > 0 ? datapoint . maxPing : null ,
} ) ;
downData . push ( {
x ,
y : datapoint . down + ( datapoint . maintenance || 0 ) ,
} ) ;
colorData . push ( this . getBarColorForDatapoint ( datapoint ) ) ;
} ,
// get the average of a set of datapoints
getAverage ( datapoints ) {
const totalUp = datapoints . reduce ( ( total , current ) => total + current . up , 0 ) ;
const totalDown = datapoints . reduce ( ( total , current ) => total + current . down , 0 ) ;
const totalMaintenance = datapoints . reduce ( ( total , current ) => total + ( current . maintenance || 0 ) , 0 ) ;
const totalPing = datapoints . reduce ( ( total , current ) => total + current . avgPing * current . up , 0 ) ;
const minPing = datapoints . reduce ( ( min , current ) => Math . min ( min , current . minPing ) , Infinity ) ;
const maxPing = datapoints . reduce ( ( max , current ) => Math . max ( max , current . maxPing ) , 0 ) ;
// Find the middle timestamp to use
let midpoint = Math . floor ( datapoints . length / 2 ) ;
return {
timestamp : datapoints [ midpoint ] . timestamp ,
up : totalUp ,
down : totalDown ,
maintenance : totalMaintenance > 0 ? totalMaintenance : undefined ,
avgPing : totalUp > 0 ? totalPing / totalUp : 0 ,
minPing ,
maxPing ,
} ;
} ,
getChartDatapointsFromHeartbeatList ( ) {
// Render chart using heartbeatList
let lastHeartbeatTime ;
const monitorInterval = this . $root . monitorList [ this . monitorId ] ? . interval ;
2021-08-24 16:54:52 +00:00
let pingData = [ ] ; // Ping Data for Line Chart, y-axis contains ping time
2022-01-23 14:22:00 +00:00
let downData = [ ] ; // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
let colorData = [ ] ; // Color Data for Bar Chart
2021-10-22 10:38:41 +00:00
2024-05-19 20:03:32 +00:00
let heartbeatList = ( this . monitorId in this . $root . heartbeatList && this . $root . heartbeatList [ this . monitorId ] ) || [ ] ;
for ( const beat of heartbeatList ) {
const beatTime = this . $root . toDayjs ( beat . time ) ;
const x = beatTime . format ( "YYYY-MM-DD HH:mm:ss" ) ;
// Insert empty datapoint to separate big gaps
if ( lastHeartbeatTime && monitorInterval ) {
const diff = Math . abs ( beatTime . diff ( lastHeartbeatTime ) ) ;
if ( diff > monitorInterval * 1000 * 10 ) {
// Big gap detected
const gapX = [
lastHeartbeatTime . add ( monitorInterval , "second" ) . format ( "YYYY-MM-DD HH:mm:ss" ) ,
beatTime . subtract ( monitorInterval , "second" ) . format ( "YYYY-MM-DD HH:mm:ss" )
] ;
for ( const x of gapX ) {
pingData . push ( {
x ,
y : null ,
} ) ;
downData . push ( {
x ,
y : null ,
} ) ;
colorData . push ( "#000" ) ;
}
}
}
pingData . push ( {
x ,
y : beat . status === UP ? beat . ping : null ,
} ) ;
downData . push ( {
x ,
y : ( beat . status === DOWN || beat . status === MAINTENANCE || beat . status === PENDING ) ? 1 : 0 ,
2021-10-22 10:38:41 +00:00
} ) ;
2024-05-19 20:03:32 +00:00
switch ( beat . status ) {
case MAINTENANCE :
colorData . push ( "rgba(23 ,71, 245, 0.41)" ) ;
break ;
case PENDING :
colorData . push ( "rgba(245, 182, 23, 0.41)" ) ;
break ;
default :
colorData . push ( "rgba(220, 53, 69, 0.41)" ) ;
}
lastHeartbeatTime = beatTime ;
}
2021-10-22 10:38:41 +00:00
2021-08-10 11:34:47 +00:00
return {
datasets : [
{
2021-08-24 16:54:52 +00:00
// Line Chart
data : pingData ,
2021-08-10 11:34:47 +00:00
fill : "origin" ,
tension : 0.2 ,
borderColor : "#5CDD8B" ,
backgroundColor : "#5CDD8B38" ,
2021-08-11 13:00:33 +00:00
yAxisID : "y" ,
2023-03-01 20:47:51 +00:00
label : "ping" ,
2021-08-11 13:00:33 +00:00
} ,
{
2021-08-24 16:54:52 +00:00
// Bar Chart
2021-08-11 13:00:33 +00:00
type : "bar" ,
2021-08-24 16:54:52 +00:00
data : downData ,
2021-08-11 13:00:33 +00:00
borderColor : "#00000000" ,
2022-01-23 14:22:00 +00:00
backgroundColor : colorData ,
2021-08-11 13:00:33 +00:00
yAxisID : "y1" ,
2021-08-24 15:34:48 +00:00
barThickness : "flex" ,
barPercentage : 1 ,
categoryPercentage : 1 ,
2023-03-01 20:47:51 +00:00
inflateAmount : 0.05 ,
label : "status" ,
2021-08-10 11:34:47 +00:00
} ,
] ,
} ;
} ,
2024-05-19 20:03:32 +00:00
getChartDatapointsFromStats ( ) {
// Render chart using UptimeCalculator data
let lastHeartbeatTime ;
const monitorInterval = this . $root . monitorList [ this . monitorId ] ? . interval ;
2022-05-01 09:56:42 +00:00
2024-05-19 20:03:32 +00:00
let avgPingData = [ ] ; // Ping Data for Line Chart, y-axis contains ping time
let minPingData = [ ] ; // Ping Data for Line Chart, y-axis contains ping time
let maxPingData = [ ] ; // Ping Data for Line Chart, y-axis contains ping time
let downData = [ ] ; // Down Data for Bar Chart, y-axis is number of down datapoints in this period
let colorData = [ ] ; // Color Data for Bar Chart
2021-10-26 04:48:21 +00:00
2024-05-19 20:03:32 +00:00
const period = parseInt ( this . chartPeriodHrs ) ;
let aggregatePoints = period > 6 ? 12 : 4 ;
let aggregateBuffer = [ ] ;
if ( this . chartRawData ) {
for ( const datapoint of this . chartRawData ) {
// Empty datapoints are ignored
if ( datapoint . up === 0 && datapoint . down === 0 && datapoint . maintenance === 0 ) {
continue ;
2021-10-22 10:38:41 +00:00
}
2024-05-19 20:03:32 +00:00
const beatTime = this . $root . unixToDayjs ( datapoint . timestamp ) ;
// Insert empty datapoint to separate big gaps
if ( lastHeartbeatTime && monitorInterval ) {
const diff = Math . abs ( beatTime . diff ( lastHeartbeatTime ) ) ;
const oneSecond = 1000 ;
const oneMinute = oneSecond * 60 ;
const oneHour = oneMinute * 60 ;
if ( ( period <= 24 && diff > Math . max ( oneMinute * 10 , monitorInterval * oneSecond * 10 ) ) ||
( period > 24 && diff > Math . max ( oneHour * 10 , monitorInterval * oneSecond * 10 ) ) ) {
// Big gap detected
// Clear the aggregate buffer
if ( aggregateBuffer . length > 0 ) {
const average = this . getAverage ( aggregateBuffer ) ;
this . pushDatapoint ( average , avgPingData , minPingData , maxPingData , downData , colorData ) ;
aggregateBuffer = [ ] ;
}
const gapX = [
lastHeartbeatTime . subtract ( monitorInterval , "second" ) . format ( "YYYY-MM-DD HH:mm:ss" ) ,
this . $root . unixToDateTime ( datapoint . timestamp + 60 ) ,
] ;
for ( const x of gapX ) {
avgPingData . push ( {
x ,
y : null ,
} ) ;
minPingData . push ( {
x ,
y : null ,
} ) ;
maxPingData . push ( {
x ,
y : null ,
} ) ;
downData . push ( {
x ,
y : null ,
} ) ;
colorData . push ( "#000" ) ;
}
}
}
if ( datapoint . up > 0 && this . chartRawData . length > aggregatePoints * 2 ) {
// Aggregate Up data using a sliding window
aggregateBuffer . push ( datapoint ) ;
if ( aggregateBuffer . length === aggregatePoints ) {
const average = this . getAverage ( aggregateBuffer ) ;
this . pushDatapoint ( average , avgPingData , minPingData , maxPingData , downData , colorData ) ;
// Remove the first half of the buffer
aggregateBuffer = aggregateBuffer . slice ( Math . floor ( aggregatePoints / 2 ) ) ;
}
} else {
// datapoint is fully down or too few datapoints, no need to aggregate
// Clear the aggregate buffer
if ( aggregateBuffer . length > 0 ) {
const average = this . getAverage ( aggregateBuffer ) ;
this . pushDatapoint ( average , avgPingData , minPingData , maxPingData , downData , colorData ) ;
aggregateBuffer = [ ] ;
}
this . pushDatapoint ( datapoint , avgPingData , minPingData , maxPingData , downData , colorData ) ;
2021-10-22 11:07:11 +00:00
}
2024-05-19 20:03:32 +00:00
lastHeartbeatTime = beatTime ;
2021-10-22 10:44:11 +00:00
}
2024-05-19 20:03:32 +00:00
// Clear the aggregate buffer if there are still datapoints
if ( aggregateBuffer . length > 0 ) {
const average = this . getAverage ( aggregateBuffer ) ;
this . pushDatapoint ( average , avgPingData , minPingData , maxPingData , downData , colorData ) ;
aggregateBuffer = [ ] ;
}
}
2021-12-06 04:05:26 +00:00
2024-05-19 20:03:32 +00:00
return {
datasets : [
{
// average ping chart
data : avgPingData ,
fill : "origin" ,
tension : 0.2 ,
borderColor : "#5CDD8B" ,
backgroundColor : "#5CDD8B06" ,
yAxisID : "y" ,
label : "avg-ping" ,
} ,
{
// minimum ping chart
data : minPingData ,
fill : "origin" ,
tension : 0.2 ,
borderColor : "#3CBD6B38" ,
backgroundColor : "#5CDD8B06" ,
yAxisID : "y" ,
label : "min-ping" ,
} ,
{
// maximum ping chart
data : maxPingData ,
fill : "origin" ,
tension : 0.2 ,
borderColor : "#7CBD6B38" ,
backgroundColor : "#5CDD8B06" ,
yAxisID : "y" ,
label : "max-ping" ,
} ,
{
// Bar Chart
type : "bar" ,
data : downData ,
borderColor : "#00000000" ,
backgroundColor : colorData ,
yAxisID : "y1" ,
barThickness : "flex" ,
barPercentage : 1 ,
categoryPercentage : 1 ,
inflateAmount : 0.05 ,
label : "status" ,
} ,
] ,
} ;
} ,
2021-10-22 10:38:41 +00:00
}
2021-08-10 11:34:47 +00:00
} ;
< / script >
2021-10-18 11:00:39 +00:00
< style lang = "scss" scoped >
@ import "../assets/vars.scss" ;
. form - select {
width : unset ;
display : inline - flex ;
}
. period - options {
2021-10-26 04:33:46 +00:00
padding : 0.1 em 1 em ;
margin - bottom : - 1.2 em ;
2021-10-18 11:00:39 +00:00
float : right ;
position : relative ;
z - index : 10 ;
2021-10-23 17:26:56 +00:00
. dropdown - menu {
padding : 0 ;
min - width : 50 px ;
font - size : 0.9 em ;
. dark & {
background : $dark - bg ;
}
. dropdown - item {
border - radius : 0.3 rem ;
2022-04-13 16:52:07 +00:00
padding : 2 px 16 px 4 px ;
2021-10-23 17:26:56 +00:00
. dark & {
background : $dark - bg ;
2024-05-19 20:03:32 +00:00
color : $dark - font - color ;
2021-10-23 17:26:56 +00:00
}
. dark & : hover {
background : $dark - font - color ;
2022-04-13 18:17:15 +00:00
color : $dark - font - color2 ;
2021-10-23 17:26:56 +00:00
}
}
. dark & . dropdown - item . active {
background : $primary ;
color : $dark - font - color2 ;
}
}
. btn - period - toggle {
padding : 2 px 15 px ;
background : transparent ;
border : 0 ;
color : $link - color ;
opacity : 0.7 ;
font - size : 0.9 em ;
& : : after {
vertical - align : 0.155 em ;
}
. dark & {
color : $dark - font - color ;
}
}
2021-10-18 11:00:39 +00:00
}
. chart - wrapper {
2021-10-23 17:26:56 +00:00
margin - bottom : 0.5 em ;
2021-10-26 04:48:21 +00:00
& . loading {
filter : blur ( 10 px ) ;
}
2021-10-18 11:00:39 +00:00
}
< / style >