Skip to content

Commit

Permalink
feat(monitoring): add latency & ping delivery rate charts (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
andre8244 authored Nov 19, 2024
1 parent 24a6a7e commit 7cade8c
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/components/charts/BasicBarChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const props = withDefaults(
showValuesOnBars?: boolean
byteFormat?: boolean
}>(),
{ isHorizontal: false, showLegend: false, showValuesOnBars: true, byteFormat: false }
{ height: '', isHorizontal: false, showLegend: false, showValuesOnBars: true, byteFormat: false }
)

const options: any = {
Expand Down
15 changes: 9 additions & 6 deletions src/components/charts/BasicPieChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import { byteFormat1024 } from '@nethesis/vue-components'

const themeStore = useThemeStore()

const props = defineProps<{
labels: string[]
datasets: any[]
height?: string
byteFormat?: boolean
}>()
const props = withDefaults(
defineProps<{
labels: string[]
datasets: any[]
height?: string
byteFormat?: boolean
}>(),
{ height: '', byteFormat: false }
)

const options: any = {
responsive: true,
Expand Down
151 changes: 151 additions & 0 deletions src/components/charts/TimeLineChart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<!--
Copyright (C) 2024 Nethesis S.r.l.
SPDX-License-Identifier: GPL-3.0-or-later
-->

<script setup lang="ts">
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
} from 'chart.js'
import 'chartjs-adapter-date-fns'
import { Line } from 'vue-chartjs'
import { computed } from 'vue'
import { useThemeStore } from '@/stores/theme'
import { formatDateLoc, kbpsFormat } from '@nethesis/vue-components'
import { GRAY_200, GRAY_700, GRAY_800 } from '@/lib/color'

const themeStore = useThemeStore()

const props = withDefaults(
defineProps<{
labels: number[]
datasets: any[]
height?: string
showLegend?: boolean
useKbpsFormat?: boolean
datasetSuffix?: string
}>(),
{ height: '', showLegend: true, useKbpsFormat: false, datasetSuffix: '' }
)

const options: any = {
// turn off animations and data parsing for performance
animation: false,
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
x: {
type: 'time',
time: {
unit: 'minute'
},
ticks: {
source: 'auto',
autoSkip: true,
color: themeStore.isLight ? GRAY_700 : GRAY_200,
callback: function (value: number) {
return formatDateLoc(value, 'HH:mm')
}
},
grid: {
color: themeStore.isLight ? GRAY_200 : GRAY_800
}
},
y: {
ticks: {
callback: function (value: number) {
if (props.useKbpsFormat) {
// format traffic on chart ticks
return kbpsFormat(Math.abs(value))
} else if (props.datasetSuffix) {
// add suffix to y axis ticks
return `${value.toLocaleString()} ${props.datasetSuffix}`
} else {
return value.toLocaleString()
}
},
color: themeStore.isLight ? GRAY_700 : GRAY_200
},
grid: {
color: themeStore.isLight ? GRAY_200 : GRAY_800
}
}
},
plugins: {
decimation: {
enabled: true,
algorithm: 'min-max'
},
tooltip: {
callbacks: {
label: function (context: any) {
let label = context.dataset.label || ''

if (label) {
label += ': '
}
if (context.parsed.y !== null) {
if (props.useKbpsFormat) {
label += kbpsFormat(Math.abs(context.parsed.y))
} else if (props.datasetSuffix) {
label += `${context.parsed.y.toLocaleString()} ${props.datasetSuffix}`
} else {
label += context.parsed.y.toLocaleString()
}
}
return label
},
// format tooltip title
title: function (context: any) {
return formatDateLoc(context[0].parsed.x, 'HH:mm:ss')
}
}
},
legend: {
display: props.showLegend,
labels: {
color: themeStore.isLight ? GRAY_700 : GRAY_200
}
}
},
responsive: true
}

const chartData: any = computed(() => {
return { labels: props.labels, datasets: props.datasets }
})

const chartStyle = computed(() => {
return {
height: props.height || '',
width: '100%',
position: 'relative'
}
})

ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
TimeScale
)
</script>

<template>
<Line :data="chartData" :options="options" :style="chartStyle" />
</template>
91 changes: 90 additions & 1 deletion src/components/standalone/monitoring/ConnectivityMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { ubusCall } from '@/lib/standalone/ubus'
import {
getAxiosErrorMessage,
NeButton,
NeCard,
NeEmptyState,
NeInlineNotification,
Expand All @@ -15,13 +16,17 @@ import {
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import WanEventsCard from './connectivity/WanEventsCard.vue'
import InterfaceTrafficCard from './connectivity/InterfaceTrafficCard.vue'
import { isEmpty } from 'lodash-es'
import type { Policy } from '@/composables/useMwan'
import WanConnectionsCard from './connectivity/WanConnectionsCard.vue'
import { useNetworkDevices } from '@/composables/useNetworkDevices'
import { getIpv4Addresses, getIpv6Addresses, getName, isDeviceUp } from '@/lib/standalone/network'
import { useUciNetworkConfig } from '@/composables/useUciNetworkConfig'
import InterfaceTrafficCard from './connectivity/InterfaceTrafficCard.vue'
import { useLatencyAndQualityReport } from '@/composables/useLatencyAndQualityReport'
import TimeLineChart from '@/components/charts/TimeLineChart.vue'
import { useRouter } from 'vue-router'
import { getStandaloneRoutePrefix } from '@/lib/router'

export type Wan = {
iface: string
Expand All @@ -46,11 +51,19 @@ const {
errorNetworkConfig,
errorNetworkConfigDetails
} = useUciNetworkConfig()
const router = useRouter()

const wans = ref<Wan[]>([])
const mwanEvents = ref<Record<string, any[]>>({})
const mwanPolicies = ref<Policy[]>([])

const {
latencyAndQualityCharts,
loadingLatencyAndQualityReport,
errorLatencyAndQualityReport,
errorLatencyAndQualityReportDetails
} = useLatencyAndQualityReport()

let loading = ref({
listWans: false,
getMwanReport: false,
Expand Down Expand Up @@ -269,6 +282,19 @@ async function getMwanPolicies() {
{{ errorNetworkConfigDetails }}
</template>
</NeInlineNotification>
<!-- latencyAndQualityReport error notification -->
<NeInlineNotification
v-if="errorLatencyAndQualityReport"
kind="error"
:title="t('error.cannot_retrieve_latency_and_quality_report')"
:description="errorLatencyAndQualityReport"
:closeAriaLabel="t('common.close')"
class="mb-4"
>
<template v-if="errorLatencyAndQualityReportDetails" #details>
{{ errorLatencyAndQualityReportDetails }}
</template>
</NeInlineNotification>
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-12">
<!-- skeleton -->
<template v-if="loadingData">
Expand Down Expand Up @@ -320,6 +346,69 @@ async function getMwanPolicies() {
:device="wan.device"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
/>
<!-- latency and quality -->
<NeCard
v-if="loadingLatencyAndQualityReport"
loading
:skeletonLines="7"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
></NeCard>
<NeCard
v-else-if="latencyAndQualityCharts.length == 0"
:title="t('standalone.real_time_monitor.latency_and_packet_delivery_rate')"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
>
<NeEmptyState
:title="t('standalone.real_time_monitor.no_hosts_configured_for_monitoring')"
:icon="['fas', 'chart-line']"
class="bg-white dark:bg-gray-950"
>
<NeButton
kind="secondary"
@click="
() => {
router.push(`${getStandaloneRoutePrefix()}/monitoring/ping-latency-monitor`)
}
"
>
<template #prefix>
<font-awesome-icon
:icon="['fas', 'arrow-right']"
class="h-4 w-4"
aria-hidden="true"
/>
</template>
{{ t('common.go_to_page', { page: t('standalone.ping_latency_monitor.title') }) }}
</NeButton>
</NeEmptyState>
</NeCard>
<template v-else v-for="(chart, index) in latencyAndQualityCharts" :key="index">
<NeCard
:title="
chart.type === 'latency'
? t('standalone.real_time_monitor.ping_host_latency', { pingHost: chart.pingHost })
: t('standalone.real_time_monitor.ping_host_packet_delivery_rate', {
pingHost: chart.pingHost
})
"
class="sm:col-span-12 xl:col-span-6 3xl:col-span-4 7xl:col-span-3"
>
<TimeLineChart
v-if="chart.type === 'latency'"
:labels="chart.labels"
:datasets="chart.datasets"
datasetSuffix="ms"
height="30vh"
/>
<TimeLineChart
v-else-if="chart.type === 'quality'"
:labels="chart.labels"
:datasets="chart.datasets"
datasetSuffix="%"
height="30vh"
/>
</NeCard>
</template>
</template>
</div>
</div>
Expand Down
Loading

0 comments on commit 7cade8c

Please sign in to comment.