Skip to content

Commit 61f8807

Browse files
committed
feat: added filtering in monitoring section
1 parent 3efba1c commit 61f8807

File tree

6 files changed

+322
-22
lines changed

6 files changed

+322
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<script lang="ts" setup>
2+
import NeBreadcrumbs, { type BreadcrumbItem } from '@/components/NeBreadcrumbs.vue'
3+
import { useI18n } from 'vue-i18n'
4+
import { computed, watchEffect } from 'vue'
5+
import { useTrafficStats } from '@/composables/useTrafficStats'
6+
import {
7+
byteFormat1024,
8+
getAxiosErrorMessage,
9+
NeCard,
10+
NeEmptyState,
11+
NeInlineNotification,
12+
NeSkeleton
13+
} from '@nethesis/vue-components'
14+
import SimpleStat from '@/components/charts/SimpleStat.vue'
15+
import { CYAN_500, CYAN_600 } from '@/lib/color'
16+
import { faEmptySet } from '@nethesis/nethesis-solid-svg-icons'
17+
import TrafficByHourChart from '@/components/standalone/monitoring/TrafficByHourChart.vue'
18+
import { useThemeStore } from '@/stores/theme'
19+
import TrafficCard from '@/components/standalone/monitoring/TrafficCard.vue'
20+
import { type AvailableFilters, useTrafficFilter } from '@/composables/useTrafficFilter'
21+
22+
type FilterableBreadcrumbItem = BreadcrumbItem & {
23+
key: string
24+
}
25+
26+
const themeStore = useThemeStore()
27+
const filters = useTrafficFilter()
28+
const { data, error, loading, loadData } = useTrafficStats()
29+
30+
watchEffect(() => {
31+
loadData(filters.get('client'), filters.get('app'))
32+
})
33+
34+
const { t } = useI18n()
35+
36+
const filtersToBreadcrumb = computed<FilterableBreadcrumbItem[]>(() => {
37+
let order = 1
38+
// add a default item to the breadcrumb
39+
const defaultItem: FilterableBreadcrumbItem = {
40+
key: 'default',
41+
order: order++,
42+
label: t('standalone.real_time_monitor.traffic')
43+
}
44+
return filters.list.value
45+
.map((filter) => {
46+
let label = ''
47+
switch (filter.key) {
48+
case 'app':
49+
if (filter.value.startsWith('netify.')) {
50+
label = filter.value.slice(7)
51+
} else {
52+
label = filter.value
53+
}
54+
if (label == 'unknown') {
55+
label = t('common.unknown')
56+
} else {
57+
label = label.charAt(0).toUpperCase() + label.slice(1)
58+
}
59+
break
60+
case 'client':
61+
label = filter.value
62+
break
63+
default:
64+
label = t(`standalone.real_time_monitor.${filter.key}`)
65+
}
66+
return {
67+
key: filter.key as string,
68+
order: order++,
69+
label: label
70+
}
71+
})
72+
.concat(defaultItem)
73+
})
74+
75+
function handleBreadcrumbClick(item: BreadcrumbItem) {
76+
filters.remove(
77+
filtersToBreadcrumb.value
78+
.filter((breadcrumbItem) => breadcrumbItem.order > item.order)
79+
.map((breadcrumbItem) => breadcrumbItem.key) as AvailableFilters[]
80+
)
81+
}
82+
83+
const resolvedHostname = computed(
84+
(): string | undefined =>
85+
data.value.clients.find((client) => client.id == filters.get('client'))?.label ?? undefined
86+
)
87+
88+
const applicationName = computed(
89+
(): string | undefined =>
90+
data.value.applications.find((app) => app.id == filters.get('app'))?.label ?? undefined
91+
)
92+
93+
const hoursLabels = computed(() => {
94+
return data.value.hourly_traffic.map((value) => value.id)
95+
})
96+
97+
const hoursDatasets = computed(() => {
98+
return [
99+
{
100+
label: t('standalone.real_time_monitor.traffic'),
101+
backgroundColor: themeStore.isLight ? CYAN_600 : CYAN_500,
102+
borderColor: themeStore.isLight ? CYAN_600 : CYAN_500,
103+
borderRadius: 6,
104+
borderWidth: 1,
105+
radius: 0,
106+
data: data.value.hourly_traffic.map((value) => value.traffic)
107+
}
108+
]
109+
})
110+
</script>
111+
112+
<template>
113+
<div class="space-y-6">
114+
<NeBreadcrumbs :items="filtersToBreadcrumb" @click="handleBreadcrumbClick" />
115+
<NeSkeleton v-if="loading" :lines="10" />
116+
<NeInlineNotification
117+
v-else-if="error"
118+
:description="t(getAxiosErrorMessage(error.message))"
119+
:title="t('standalone.real_time_monitor.error_fetching_data')"
120+
kind="error"
121+
/>
122+
<template v-else>
123+
<div class="flex flex-col gap-6 xl:flex-row">
124+
<div class="flex flex-col gap-6 sm:w-96">
125+
<NeCard v-if="filters.contains('client')" :title="t('standalone.dashboard.hostname')">
126+
<SimpleStat>
127+
<p v-if="resolvedHostname != filters.get('client')">{{ resolvedHostname }}</p>
128+
<p class="[&:nth-child(2)]:text-lg">{{ filters.get('client') }}</p>
129+
</SimpleStat>
130+
</NeCard>
131+
<NeCard :title="t('standalone.real_time_monitor.daily_total_traffic')">
132+
<SimpleStat>
133+
<span v-if="data.total_traffic != 0">{{ byteFormat1024(data.total_traffic) }}</span>
134+
<span v-else> N/A </span>
135+
</SimpleStat>
136+
</NeCard>
137+
<NeCard v-if="applicationName" :title="t('standalone.real_time_monitor.application')">
138+
<SimpleStat> {{ applicationName }}</SimpleStat>
139+
</NeCard>
140+
</div>
141+
<NeCard
142+
:title="t('standalone.real_time_monitor.recent_traffic')"
143+
class="flex-1 justify-self-start"
144+
>
145+
<TrafficByHourChart
146+
v-if="data.hourly_traffic.length > 0"
147+
:datasets="hoursDatasets"
148+
:labels="hoursLabels"
149+
height="25vh"
150+
/>
151+
<NeEmptyState
152+
v-else
153+
:icon="faEmptySet"
154+
:title="t('standalone.real_time_monitor.no_data_available')"
155+
/>
156+
</NeCard>
157+
</div>
158+
<div class="grid gap-6 xl:grid-cols-2">
159+
<TrafficCard
160+
v-if="filters.misses('client')"
161+
:data="data.clients"
162+
:title="t('standalone.real_time_monitor.local_hosts')"
163+
filterable
164+
filterable-key="client"
165+
/>
166+
<TrafficCard
167+
v-if="filters.misses('app')"
168+
:data="data.applications"
169+
:title="t('standalone.real_time_monitor.applications')"
170+
filterable
171+
filterable-key="app"
172+
/>
173+
<TrafficCard
174+
:data="data.remote_hosts"
175+
:title="t('standalone.real_time_monitor.remote_hosts')"
176+
/>
177+
<TrafficCard :data="data.protocols" :title="t('standalone.real_time_monitor.protocols')" />
178+
</div>
179+
</template>
180+
</div>
181+
</template>

src/components/standalone/monitoring/TrafficCard.vue

+17-5
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,25 @@ import { useThemeStore } from '@/stores/theme'
3131
import TrafficTable from '@/components/standalone/monitoring/TrafficTable.vue'
3232
import { useI18n } from 'vue-i18n'
3333
import { faEmptySet } from '@nethesis/nethesis-solid-svg-icons'
34+
import type { AvailableFilters } from '@/composables/useTrafficFilter'
3435
3536
const themeStore = useThemeStore()
3637
3738
const { t } = useI18n()
3839
39-
const { title, data } = defineProps<{
40+
const {
41+
title,
42+
data,
43+
filterable = false
44+
} = defineProps<{
4045
title: string
41-
data: TrafficRecord[]
46+
data?: TrafficRecord[]
47+
filterable?: boolean
48+
filterableKey?: AvailableFilters
4249
}>()
4350
4451
const dataLimited = computed(() => {
45-
return data.slice(0, 5) ?? []
52+
return data?.slice(0, 5) ?? []
4653
})
4754
4855
const pieDatasets = computed(() => {
@@ -85,10 +92,15 @@ const pieLabels = computed(() => {
8592
</script>
8693

8794
<template>
88-
<NeCard :title="title">
95+
<NeCard v-if="data" :title="title">
8996
<div v-if="data.length > 0" class="space-y-4">
9097
<BasicPieChart :datasets="pieDatasets" :labels="pieLabels" byteFormat height="25vh" />
91-
<TrafficTable :title="title" :traffic-entries="data" />
98+
<TrafficTable
99+
:filterable="filterable"
100+
:filterable-key="filterableKey"
101+
:title="title"
102+
:traffic-entries="data"
103+
/>
92104
</div>
93105
<NeEmptyState
94106
v-else

src/components/standalone/monitoring/TrafficMonitor.vue

+20-5
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,30 @@ import {
1515
} from '@nethesis/vue-components'
1616
import SimpleStat from '@/components/charts/SimpleStat.vue'
1717
import TrafficByHourChart from '@/components/standalone/monitoring/TrafficByHourChart.vue'
18-
import { computed } from 'vue'
18+
import { computed, watchEffect } from 'vue'
1919
import { CYAN_500, CYAN_600 } from '@/lib/color'
2020
import { useThemeStore } from '@/stores/theme'
2121
import { useTrafficStats } from '@/composables/useTrafficStats'
2222
import TrafficCard from '@/components/standalone/monitoring/TrafficCard.vue'
2323
import { faEmptySet } from '@nethesis/nethesis-solid-svg-icons'
24+
import FilteredTraffic from '@/components/standalone/monitoring/FilteredTraffic.vue'
25+
import { useTrafficFilter } from '@/composables/useTrafficFilter'
2426

2527
const { t } = useI18n()
2628

27-
const { loading, error, data } = useTrafficStats()
29+
const filters = useTrafficFilter()
30+
31+
const { loading, error, data, loadData } = useTrafficStats()
2832
const themeStore = useThemeStore()
2933

34+
watchEffect(() => {
35+
if (!filters.active.value) {
36+
loadData()
37+
}
38+
})
39+
3040
const hoursLabels = computed(() => {
31-
return data.value?.hourly_traffic.map((value) => value.id) ?? []
41+
return data.value.hourly_traffic.map((value) => value.id)
3242
})
3343

3444
const hoursDatasets = computed(() => {
@@ -40,14 +50,15 @@ const hoursDatasets = computed(() => {
4050
borderRadius: 6,
4151
borderWidth: 1,
4252
radius: 0,
43-
data: data.value?.hourly_traffic.map((value) => value.traffic)
53+
data: data.value.hourly_traffic.map((value) => value.traffic)
4454
}
4555
]
4656
})
4757
</script>
4858

4959
<template>
50-
<div class="space-y-12">
60+
<FilteredTraffic v-if="filters.active" />
61+
<div v-else class="space-y-12">
5162
<NeSkeleton v-if="loading" :lines="10" />
5263
<NeInlineNotification
5364
v-else-if="error"
@@ -89,10 +100,14 @@ const hoursDatasets = computed(() => {
89100
<TrafficCard
90101
:title="t('standalone.real_time_monitor.local_hosts')"
91102
:data="data?.clients ?? []"
103+
filterable
104+
filterableKey="client"
92105
/>
93106
<TrafficCard
94107
:title="t('standalone.real_time_monitor.applications')"
95108
:data="data?.applications ?? []"
109+
filterable
110+
filterable-key="app"
96111
/>
97112
<TrafficCard
98113
:title="t('standalone.real_time_monitor.remote_hosts')"

src/components/standalone/monitoring/TrafficTable.vue

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" setup>
2-
import { faSearch } from '@fortawesome/free-solid-svg-icons'
2+
import { faSearch, faTable } from '@fortawesome/free-solid-svg-icons'
33
import {
44
byteFormat1024,
55
NeEmptyState,
@@ -11,21 +11,27 @@ import {
1111
NeTableHeadCell,
1212
NeTableRow,
1313
NeTextInput,
14-
useItemPagination
14+
useItemPagination,
15+
NeLink
1516
} from '@nethesis/vue-components'
1617
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
1718
import { computed, ref } from 'vue'
1819
import { refDebounced } from '@vueuse/core'
1920
import { useI18n } from 'vue-i18n'
2021
import type { TrafficRecord } from '@/composables/useTrafficStats'
22+
import { type AvailableFilters, useTrafficFilter } from '@/composables/useTrafficFilter'
2123
2224
const { trafficEntries, title } = defineProps<{
2325
trafficEntries: TrafficRecord[]
2426
title: string
27+
filterable?: boolean
28+
filterableKey?: AvailableFilters
2529
}>()
2630
2731
const { t } = useI18n()
2832
33+
const filters = useTrafficFilter()
34+
2935
const filter = ref('')
3036
const filterDebounced = refDebounced(filter, 400)
3137
@@ -59,15 +65,28 @@ const { currentPage, paginatedItems } = useItemPagination(() => trafficEntriesFi
5965
<NeTableRow v-if="!paginatedItems.length">
6066
<NeTableCell colspan="9">
6167
<NeEmptyState
62-
:icon="['fas', 'table']"
68+
:icon="faTable"
6369
:title="t('common.no_data_available')"
6470
class="bg-white dark:bg-gray-950"
6571
/>
6672
</NeTableCell>
6773
</NeTableRow>
6874
<NeTableRow v-for="(item, index) in paginatedItems as TrafficRecord[]" v-else :key="index">
6975
<NeTableCell :data-label="title">
70-
{{ item?.label ?? item.id }}
76+
<template v-if="filterable">
77+
<NeLink
78+
@click="
79+
filters.push({
80+
key: filterableKey!,
81+
value: item.id
82+
})
83+
"
84+
>{{ item?.label ?? item.id }}</NeLink
85+
>
86+
</template>
87+
<template v-else>
88+
{{ item?.label ?? item.id }}
89+
</template>
7190
</NeTableCell>
7291
<NeTableCell :data-label="t('standalone.real_time_monitor.traffic')">
7392
{{ byteFormat1024(item.traffic) }}

0 commit comments

Comments
 (0)