Skip to content

Commit f026a95

Browse files
authored
Merge pull request #20 from CESNET/frontend_history_showcase
feat(frontend): new time picker and split views for latest and historical data
2 parents a4b4e0f + ae60f5a commit f026a95

13 files changed

Lines changed: 1197 additions & 588 deletions

frontend/package-lock.json

Lines changed: 378 additions & 303 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,29 @@
1010
"format": "prettier --write src/"
1111
},
1212
"dependencies": {
13-
"axios": "^1.7.7",
13+
"axios": "^1.8.2",
1414
"bootstrap": "^5.3.3",
15-
"chart.js": "^4.4.6",
15+
"chart.js": "^4.4.8",
1616
"chartjs-adapter-date-fns": "^3.0.0",
17+
"chartjs-plugin-annotation": "^3.1.0",
1718
"dayjs": "^1.11.13",
1819
"floating-vue": "^5.2.2",
1920
"font-awesome": "^4.7.0",
20-
"pinia": "^2.2.6",
21-
"vue": "^3.5.12",
21+
"pinia": "^3.0.1",
22+
"vue": "^3.5.13",
2223
"vue-axios": "^3.5.2",
2324
"vue-chartjs": "^5.3.2",
24-
"vue-router": "^4.4.5",
25+
"vue-router": "^4.5.0",
2526
"vuejs-paginate-next": "^1.0.2"
2627
},
2728
"devDependencies": {
28-
"@eslint/js": "^9.14.0",
29-
"@vitejs/plugin-vue": "^5.1.4",
30-
"@vue/eslint-config-prettier": "^10.1.0",
31-
"eslint": "^9.14.0",
32-
"eslint-plugin-vue": "^9.30.0",
33-
"prettier": "^3.3.3",
34-
"vite": "^5.4.14",
35-
"vite-plugin-vue-devtools": "^7.5.4"
29+
"@eslint/js": "^9.20.0",
30+
"@vitejs/plugin-vue": "^5.2.1",
31+
"@vue/eslint-config-prettier": "^10.2.0",
32+
"eslint": "^9.20.1",
33+
"eslint-plugin-vue": "^9.32.0",
34+
"prettier": "^3.5.1",
35+
"vite": "^6.1.0",
36+
"vite-plugin-vue-devtools": "^7.7.2"
3637
}
3738
}

frontend/src/components/ActivityTimeline.vue

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22
import { computed } from 'vue'
33
import { Line } from 'vue-chartjs'
44
import { Chart as ChartJS, registerables } from 'chart.js'
5+
import annotationPlugin from 'chartjs-plugin-annotation'
56
import 'chartjs-adapter-date-fns'
67
7-
ChartJS.register(...registerables)
8+
import {
9+
CHART_COMMON_OPTIONS,
10+
CHART_SCALE_X_OPTIONS,
11+
resampleTimedData,
12+
setChartDatetimeRange,
13+
} from '@/utils/commonCharts.js'
14+
15+
ChartJS.register(...registerables, annotationPlugin)
816
ChartJS.defaults.color = '#dee2e6'
917
ChartJS.defaults.borderColor = '#495057'
1018
@@ -13,39 +21,39 @@ const props = defineProps({
1321
type: Array,
1422
required: true,
1523
},
24+
timePickerState: {
25+
type: Object,
26+
required: true,
27+
},
28+
pickedSnapshotTs: {
29+
type: Date,
30+
required: true,
31+
},
32+
resampleUnitCount: {
33+
type: Number,
34+
required: true,
35+
},
36+
resampleUnit: {
37+
type: String,
38+
required: true,
39+
},
1640
})
1741
1842
const CHART_UNITS = ['pkt', 'flw', 'B']
1943
const CHART_COLORS = ['#0DCAF0', '#198754', '#FFC107']
2044
2145
const chartOptions = computed(() => {
22-
let scaleXOptions = {
23-
type: 'time',
24-
time: {
25-
displayFormats: {
26-
millisecond: 'HH:mm:ss.SSS',
27-
second: 'HH:mm:ss',
28-
minute: 'HH:mm',
29-
hour: 'HH:mm',
30-
},
31-
tooltipFormat: 'dd.MM. HH:mm',
32-
},
33-
ticks: {
34-
autoSkip: false,
35-
maxRotation: 0,
36-
major: {
37-
enabled: true,
38-
},
39-
},
40-
}
46+
let scaleXOptions = { ...CHART_SCALE_X_OPTIONS }
4147
42-
if (props.activity.length > 0) {
43-
// + 'Z' to treat as UTC
44-
scaleXOptions.min = new Date(props.activity[0].t2 + 'Z')
45-
scaleXOptions.max = new Date(props.activity[props.activity.length - 1].t2 + 'Z')
46-
}
48+
// Set the time range of the chart
49+
setChartDatetimeRange(scaleXOptions, props.timePickerState.from, props.timePickerState.to)
4750
4851
return {
52+
...CHART_COMMON_OPTIONS,
53+
interaction: {
54+
intersect: false,
55+
mode: 'index',
56+
},
4957
scales: {
5058
x: scaleXOptions,
5159
packets: {
@@ -85,16 +93,24 @@ const chartOptions = computed(() => {
8593
return `${item.formattedValue} ${unit}`
8694
},
8795
},
96+
usePointStyle: true,
97+
},
98+
annotation: {
99+
annotations: {
100+
line1: {
101+
type: 'line',
102+
xMin: props.pickedSnapshotTs,
103+
xMax: props.pickedSnapshotTs,
104+
borderColor: '#d980fa',
105+
borderWidth: 2,
106+
},
107+
},
88108
},
89109
},
90-
animation: {
91-
duration: 0,
92-
},
93-
maintainAspectRatio: false,
94110
}
95111
})
96112
const chartData = computed(() => {
97-
const data = props.activity.map((dp) => {
113+
let data = props.activity.map((dp) => {
98114
return {
99115
t: new Date(dp.t2 + 'Z'),
100116
packets: dp.v.packets,
@@ -103,6 +119,24 @@ const chartData = computed(() => {
103119
}
104120
})
105121
122+
// Resample data to avoid too many points
123+
data = resampleTimedData(
124+
data,
125+
't',
126+
props.resampleUnitCount,
127+
props.resampleUnit,
128+
(bucketData, bucketDt) => {
129+
return [
130+
{
131+
t: bucketDt,
132+
packets: bucketData.reduce((acc, dp) => acc + dp.packets, 0),
133+
flows: bucketData.reduce((acc, dp) => acc + dp.flows, 0),
134+
bytes: bucketData.reduce((acc, dp) => acc + dp.bytes, 0),
135+
},
136+
]
137+
},
138+
)
139+
106140
return {
107141
datasets: [
108142
{
@@ -114,8 +148,10 @@ const chartData = computed(() => {
114148
yAxisKey: 'packets',
115149
},
116150
borderColor: CHART_COLORS[0],
117-
pointRadius: 4,
118-
pointHitRadius: 5,
151+
pointRadius: 6,
152+
pointHoverRadius: 6,
153+
pointHitRadius: 7,
154+
pointStyle: 'rect',
119155
},
120156
{
121157
label: 'Flows',
@@ -126,8 +162,10 @@ const chartData = computed(() => {
126162
yAxisKey: 'flows',
127163
},
128164
borderColor: CHART_COLORS[1],
129-
pointRadius: 4,
130-
pointHitRadius: 5,
165+
pointRadius: 6,
166+
pointHoverRadius: 6,
167+
pointHitRadius: 7,
168+
pointStyle: 'circle',
131169
},
132170
{
133171
label: 'Bytes',
@@ -138,8 +176,10 @@ const chartData = computed(() => {
138176
yAxisKey: 'bytes',
139177
},
140178
borderColor: CHART_COLORS[2],
141-
pointRadius: 4,
142-
pointHitRadius: 5,
179+
pointRadius: 6,
180+
pointHoverRadius: 6,
181+
pointHitRadius: 7,
182+
pointStyle: 'star',
143183
},
144184
],
145185
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script setup>
2+
import { ref } from 'vue'
3+
4+
const props = defineProps({
5+
permalink: {
6+
type: String,
7+
required: true,
8+
},
9+
text: {
10+
type: String,
11+
default: 'Copy permalink',
12+
},
13+
defaultColorClass: {
14+
type: String,
15+
default: 'btn-primary',
16+
},
17+
})
18+
19+
const copied = ref(false)
20+
21+
/**
22+
* Copies supplied permalink
23+
*/
24+
function copyPermalink() {
25+
navigator.clipboard.writeText(props.permalink)
26+
27+
// Animate successful copy
28+
copied.value = true
29+
setTimeout(() => (copied.value = false), 1000)
30+
}
31+
</script>
32+
33+
<template>
34+
<div
35+
class="btn"
36+
:class="{ [defaultColorClass]: !copied, 'btn-success': copied }"
37+
@click="copyPermalink"
38+
>
39+
<i class="fa" :class="{ 'fa-clone': !copied, 'fa-check': copied }"></i>
40+
{{ text }}
41+
</div>
42+
</template>

0 commit comments

Comments
 (0)