Skip to content

Commit cefcfb9

Browse files
authored
feat: server-side nodes paginator & feat: nodes controller statistics (#15)
1 parent 0534fb2 commit cefcfb9

13 files changed

Lines changed: 1109 additions & 460 deletions

package-lock.json

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

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dkg-frontend",
3-
"version": "0.6.12",
3+
"version": "0.7.0",
44
"private": true,
55
"scripts": {
66
"dev": "vite",
@@ -22,7 +22,7 @@
2222
"vite-plugin-vuetify": "^1.0.2",
2323
"vue": "^3.3.4",
2424
"vue-router": "^4.2.2",
25-
"vuetify": "^3.3.9",
25+
"vuetify": "^3.7.0",
2626
"vuetify-use-dialog": "^0.6.2",
2727
"yup": "^1.2.0"
2828
},

src/App.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ function getUserName() {
7979
<v-list-item>
8080
<RouterLink to="/nodes" class="link">Nodes</RouterLink>
8181
</v-list-item>
82+
<v-list-item v-if="authStore.user?.isAdmin">
83+
<RouterLink :to="'/statistics'" class="link">Statistics</RouterLink>
84+
</v-list-item>
8285
<v-list-item v-if="!authStore.user?.isAdmin">
8386
<RouterLink :to="'/user/edit/' + authStore.user.id" class="link">Settings</RouterLink>
8487
</v-list-item>

src/components/Nodes_List.vue

Lines changed: 65 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
// POSSIBILITY OF SUCH DAMAGE.
2626
2727
import { onMounted, onUnmounted } from 'vue'
28+
import { watch } from 'vue'
2829
2930
import { storeToRefs } from 'pinia'
3031
import { useNodesStore } from '@/stores/nodes.store.js'
@@ -40,8 +41,54 @@ const { alert } = storeToRefs(alertStore)
4041
const authStore = useAuthStore()
4142
4243
const nodesStore = useNodesStore()
43-
const { nodes, nodesU } = storeToRefs(nodesStore)
44-
nodesStore.getAll()
44+
45+
let isUpdating = false
46+
47+
const updatePeriodically = async () => {
48+
if (isUpdating) {
49+
return;
50+
}
51+
52+
isUpdating = true
53+
54+
try {
55+
await nodesStore.fetchFrame({
56+
page: authStore.nodesPage,
57+
itemsPerPage: nodesStore.nodesPerPage,
58+
sortBy: nodesStore.nodesSortBy,
59+
search: nodesStore.nodesSearch,
60+
});
61+
62+
}
63+
catch (error) {
64+
alertStore.error('Fatal error when updating node list: ' + error.message)
65+
}
66+
finally {
67+
isUpdating = false;
68+
}
69+
};
70+
71+
watch(
72+
() => [
73+
nodesStore.nodesPage,
74+
nodesStore.nodesPerPage,
75+
nodesStore.nodesSortBy,
76+
nodesStore.nodesSearch,
77+
],
78+
() => { updatePeriodically(); },
79+
{ immediate: true }
80+
);
81+
82+
let intervalId = null
83+
84+
onMounted(() => {
85+
intervalId = setInterval(updatePeriodically, 5000);
86+
});
87+
88+
onUnmounted(() => {
89+
clearInterval(intervalId);
90+
});
91+
4592
4693
import { useConfirm } from 'vuetify-use-dialog'
4794
const confirm = useConfirm()
@@ -77,104 +124,21 @@ async function resetNode(item) {
77124
nodesStore
78125
.reset(item.id)
79126
.then(() => {
80-
updateDataGrid()
127+
updatePeriodically()
81128
})
82129
.catch((error) => {
83130
alertStore.error(error)
84131
})
85132
}
86133
}
87134
88-
89-
function filterNodes(value, query, item) {
90-
if (query == null || item == null) {
91-
return false
92-
}
93-
94-
const i = item.raw
95-
if (i == null) {
96-
return false
97-
}
98-
99-
const q = query.toLocaleUpperCase()
100-
101-
if (
102-
i.id.toString().indexOf(q) !== -1 ||
103-
i.name.toLocaleUpperCase().indexOf(q) !== -1 ||
104-
i.public_key.toLocaleUpperCase().indexOf(q) !== -1 ||
105-
(i.round_id != null && i.round_id.toString().indexOf(q) !== -1)
106-
) {
107-
return true
108-
}
109-
110-
return false
111-
}
112-
113135
function formatRound(roundId) {
114136
if (roundId == null) {
115137
return '--'
116138
}
117139
return roundId.toString()
118140
}
119141
120-
let isUpdating = false
121-
122-
const updateDataGrid = async () => {
123-
if (isUpdating) {
124-
return
125-
}
126-
127-
isUpdating = true
128-
try {
129-
await nodesStore.getAllU()
130-
if (!nodesU?.loading && !nodesU?.loading)
131-
{
132-
const oldData = [...nodes.value]
133-
const newItems = []
134-
135-
for (const newItem of nodesU.value) {
136-
const oldItem = nodes.value.find(item => item.id === newItem.id)
137-
if (oldItem) {
138-
if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
139-
Object.assign(oldItem, newItem)
140-
}
141-
}
142-
else {
143-
newItems.push(newItem)
144-
}
145-
}
146-
nodes.value.unshift(...newItems)
147-
148-
for (const oldItem of oldData) {
149-
const newItem = nodesU.value.find(item => item.id === oldItem.id)
150-
if (!newItem) {
151-
const index = nodes.value.indexOf(oldItem)
152-
nodes.value.splice(index, 1)
153-
}
154-
}
155-
}
156-
}
157-
catch {
158-
alertStore.error('Failed to update nodes list')
159-
}
160-
finally {
161-
isUpdating = false
162-
}
163-
}
164-
165-
let intervalId = null
166-
167-
onMounted(() => {
168-
intervalId = setInterval(() => {
169-
updateDataGrid()
170-
}, 10000)
171-
})
172-
173-
onUnmounted(() => {
174-
if (intervalId) {
175-
clearInterval(intervalId)
176-
}
177-
})
178142
179143
</script>
180144

@@ -184,20 +148,20 @@ onUnmounted(() => {
184148
<hr class="hr" />
185149

186150
<v-card>
187-
<v-data-table
188-
v-if="nodes?.length"
189-
v-model:items-per-page="authStore.nodes_per_page"
151+
<v-data-table-server
152+
v-model:items-per-page="nodesStore.nodesPerPage"
190153
items-per-page-text="Nodes per page"
191154
page-text="{0}-{1} of {2}"
192-
v-model:page="authStore.nodes_page"
155+
:page="nodesStore.nodesPage"
193156
:items-per-page-options="itemsPerPageOptions"
194157
:headers="headers"
195-
:items="nodes"
196-
:search="authStore.nodes_search"
197-
v-model:sort-by="authStore.nodes_sort_by"
198-
:custom-filter="filterNodes"
158+
:items="nodesStore.nodesF"
159+
:itemsLength="nodesStore.totalNodes"
160+
:search="nodesStore.nodesSearch"
161+
v-model:sort-by="nodesStore.nodesSortBy"
199162
item-value="id"
200163
class="elevation-1"
164+
@update:options="nodesStore.fetchNodes"
201165
>
202166
<template v-slot:[`item.roundId`]="{ item }">
203167
{{ formatRound(item['roundId']) }}
@@ -210,23 +174,23 @@ onUnmounted(() => {
210174
<v-tooltip activator="parent">Reset</v-tooltip>
211175

212176
</template>
213-
</v-data-table>
177+
</v-data-table-server>
214178
<v-spacer></v-spacer>
215-
<div v-if="!nodes?.length" class="text-center m-5">No nodes</div>
216-
<div v-if="nodes?.length">
179+
<div v-if="!nodesStore.nodesF?.length" class="text-center m-5">No nodes</div>
180+
<div v-if="nodesStore.totalNodes">
217181
<v-text-field
218-
v-model="authStore.nodes_search"
182+
v-model="nodesStore.nodesSearch"
219183
:append-inner-icon="mdiMagnify"
220184
label="Search any node information"
221185
variant="solo"
222186
hide-details
223187
/>
224188
</div>
225189
</v-card>
226-
<div v-if="nodes?.error" class="text-center m-5">
227-
<div class="text-danger">Failed to load nodes list: {{ nodes.error }}</div>
190+
<div v-if="nodesStore.nodesF?.error" class="text-center m-5">
191+
<div class="text-danger">Failed to load nodes list: {{ nodesF.error }}</div>
228192
</div>
229-
<div v-if="nodes?.loading" class="text-center m-5">
193+
<div v-if="nodesStore.nodesF?.loading" class="text-center m-5">
230194
<span class="spinner-border spinner-border-lg align-center"></span>
231195
</div>
232196
<div v-if="alert" class="alert alert-dismissable mt-3 mb-0" :class="alert.type">

src/components/Rounds_List.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ const updateDataGrid = async () => {
193193
isUpdating = true
194194
try {
195195
await roundsStore.getAllU()
196-
if (!roundsU?.loading && !roundsU?.loading)
196+
if (!roundsU?.loading && !roundsU?.error)
197197
{
198198
const oldData = [...rounds.value]
199199
const newItems = []

src/components/Statistics_List.vue

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<script setup>
2+
// Copyright (C) 2024 Maxim [maxirmx] Samsonov (www.sw.consulting)
3+
// All rights reserved.
4+
// This file is a part of Dkg Frontend applcation
5+
//
6+
// Redistribution and use in source and binary forms, with or without
7+
// modification, are permitted provided that the following conditions
8+
// are met:
9+
// 1. Redistributions of source code must retain the above copyright
10+
// notice, this list of conditions and the following disclaimer.
11+
// 2. Redistributions in binary form must reproduce the above copyright
12+
// notice, this list of conditions and the following disclaimer in the
13+
// documentation and/or other materials provided with the distribution.
14+
//
15+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16+
// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
17+
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18+
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
19+
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20+
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21+
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22+
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23+
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24+
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25+
// POSSIBILITY OF SUCH DAMAGE.
26+
27+
import { onMounted, onUnmounted } from 'vue'
28+
29+
import { storeToRefs } from 'pinia'
30+
import { useStatisticsStore } from '@/stores/statistics.store.js'
31+
32+
const statisticsStore = useStatisticsStore()
33+
const { statistics, statisticsU } = storeToRefs(statisticsStore)
34+
statisticsStore.getStatistics()
35+
36+
import { useAlertStore } from '@/stores/alert.store.js'
37+
const alertStore = useAlertStore()
38+
const { alert } = storeToRefs(alertStore)
39+
40+
41+
const headers = [
42+
{ title: 'Function', align: 'center', key: 'name' },
43+
{ title: 'Number of calls', align: 'center', key: 'count' },
44+
{ title: 'Avg. elapsed time (ms)', align: 'center', key: 'timePerCall' }
45+
]
46+
47+
48+
let intervalId = null
49+
let isUpdating = false
50+
51+
onMounted(() => {
52+
intervalId = setInterval(async () => {
53+
if (isUpdating) {
54+
return
55+
}
56+
57+
isUpdating = true
58+
await statisticsStore.getStatisticsU()
59+
if (!statisticsU?.loading && !statisticsU?.error) {
60+
statistics.value = statistics.value.map(stat => {
61+
const updatedStat = statisticsU.value.find(item => item.name === stat.name)
62+
return updatedStat ? { ...stat, count: updatedStat.count, timePerCall: updatedStat.timePerCall } : stat
63+
})
64+
}
65+
isUpdating = false
66+
}, 10000)
67+
})
68+
69+
onUnmounted(() => {
70+
if (intervalId) {
71+
clearInterval(intervalId)
72+
}
73+
})
74+
75+
</script>
76+
77+
<template>
78+
<div class="settings table-3">
79+
<h1 class="orange">Node controller statistics</h1>
80+
<hr class="hr" />
81+
82+
<v-card>
83+
<v-data-table
84+
v-if="statistics?.length"
85+
:headers="headers"
86+
:items="statistics"
87+
item-value="name"
88+
class="elevation-1"
89+
>
90+
<template v-slot:[`item.timePerCall`]="{ item }">
91+
{{ item.timePerCall.toFixed(2) }}
92+
</template>
93+
</v-data-table>
94+
<div v-if="!statistics?.length" class="text-center m-5">No data</div>
95+
</v-card>
96+
<div v-if="statistics?.loading" class="text-center m-5">
97+
<span class="spinner-border spinner-border-lg align-center"></span>
98+
</div>
99+
<div v-if="statistics?.error" class="text-center m-5">
100+
<div class="text-danger">Failed to load data: {{ statistics.error }}</div>
101+
</div>
102+
<div v-if="alert" class="alert alert-dismissable mt-3 mb-0" :class="alert.type">
103+
<button @click="alertStore.clear()" class="btn btn-link close">×</button>
104+
{{ alert.message }}
105+
</div>
106+
</div>
107+
</template>

src/helpers/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ let apiUrl;
2828
if (import.meta.env.MODE === 'production') {
2929
apiUrl = `https://${window.location.hostname}:8081/api`;
3030
} else {
31-
apiUrl = `https://${import.meta.env.VITE_API_HOST}:8081/api`;
31+
apiUrl = `http://${import.meta.env.VITE_API_HOST}:8080/api`;
3232
}
3333

3434
export { apiUrl };

src/router/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ const router = createRouter({
8282
name: 'Rounds',
8383
component: () => import('@/views/Rounds_View.vue')
8484
},
85+
{
86+
path: '/statistics',
87+
name: 'Statistics',
88+
component: () => import('@/views/Statistics_View.vue')
89+
},
8590
]
8691
})
8792

0 commit comments

Comments
 (0)