Skip to content

Commit

Permalink
feat: server-side nodes paginator & feat: nodes controller statistics (
Browse files Browse the repository at this point in the history
  • Loading branch information
maxirmx authored Aug 21, 2024
1 parent 0534fb2 commit cefcfb9
Show file tree
Hide file tree
Showing 13 changed files with 1,109 additions and 460 deletions.
1,156 changes: 808 additions & 348 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dkg-frontend",
"version": "0.6.12",
"version": "0.7.0",
"private": true,
"scripts": {
"dev": "vite",
Expand All @@ -22,7 +22,7 @@
"vite-plugin-vuetify": "^1.0.2",
"vue": "^3.3.4",
"vue-router": "^4.2.2",
"vuetify": "^3.3.9",
"vuetify": "^3.7.0",
"vuetify-use-dialog": "^0.6.2",
"yup": "^1.2.0"
},
Expand Down
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ function getUserName() {
<v-list-item>
<RouterLink to="/nodes" class="link">Nodes</RouterLink>
</v-list-item>
<v-list-item v-if="authStore.user?.isAdmin">
<RouterLink :to="'/statistics'" class="link">Statistics</RouterLink>
</v-list-item>
<v-list-item v-if="!authStore.user?.isAdmin">
<RouterLink :to="'/user/edit/' + authStore.user.id" class="link">Settings</RouterLink>
</v-list-item>
Expand Down
166 changes: 65 additions & 101 deletions src/components/Nodes_List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
// POSSIBILITY OF SUCH DAMAGE.
import { onMounted, onUnmounted } from 'vue'
import { watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useNodesStore } from '@/stores/nodes.store.js'
Expand All @@ -40,8 +41,54 @@ const { alert } = storeToRefs(alertStore)
const authStore = useAuthStore()
const nodesStore = useNodesStore()
const { nodes, nodesU } = storeToRefs(nodesStore)
nodesStore.getAll()
let isUpdating = false
const updatePeriodically = async () => {
if (isUpdating) {
return;
}
isUpdating = true
try {
await nodesStore.fetchFrame({
page: authStore.nodesPage,
itemsPerPage: nodesStore.nodesPerPage,
sortBy: nodesStore.nodesSortBy,
search: nodesStore.nodesSearch,
});
}
catch (error) {
alertStore.error('Fatal error when updating node list: ' + error.message)
}
finally {
isUpdating = false;
}
};
watch(
() => [
nodesStore.nodesPage,
nodesStore.nodesPerPage,
nodesStore.nodesSortBy,
nodesStore.nodesSearch,
],
() => { updatePeriodically(); },
{ immediate: true }
);
let intervalId = null
onMounted(() => {
intervalId = setInterval(updatePeriodically, 5000);
});
onUnmounted(() => {
clearInterval(intervalId);
});
import { useConfirm } from 'vuetify-use-dialog'
const confirm = useConfirm()
Expand Down Expand Up @@ -77,104 +124,21 @@ async function resetNode(item) {
nodesStore
.reset(item.id)
.then(() => {
updateDataGrid()
updatePeriodically()
})
.catch((error) => {
alertStore.error(error)
})
}
}
function filterNodes(value, query, item) {
if (query == null || item == null) {
return false
}
const i = item.raw
if (i == null) {
return false
}
const q = query.toLocaleUpperCase()
if (
i.id.toString().indexOf(q) !== -1 ||
i.name.toLocaleUpperCase().indexOf(q) !== -1 ||
i.public_key.toLocaleUpperCase().indexOf(q) !== -1 ||
(i.round_id != null && i.round_id.toString().indexOf(q) !== -1)
) {
return true
}
return false
}
function formatRound(roundId) {
if (roundId == null) {
return '--'
}
return roundId.toString()
}
let isUpdating = false
const updateDataGrid = async () => {
if (isUpdating) {
return
}
isUpdating = true
try {
await nodesStore.getAllU()
if (!nodesU?.loading && !nodesU?.loading)
{
const oldData = [...nodes.value]
const newItems = []
for (const newItem of nodesU.value) {
const oldItem = nodes.value.find(item => item.id === newItem.id)
if (oldItem) {
if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
Object.assign(oldItem, newItem)
}
}
else {
newItems.push(newItem)
}
}
nodes.value.unshift(...newItems)
for (const oldItem of oldData) {
const newItem = nodesU.value.find(item => item.id === oldItem.id)
if (!newItem) {
const index = nodes.value.indexOf(oldItem)
nodes.value.splice(index, 1)
}
}
}
}
catch {
alertStore.error('Failed to update nodes list')
}
finally {
isUpdating = false
}
}
let intervalId = null
onMounted(() => {
intervalId = setInterval(() => {
updateDataGrid()
}, 10000)
})
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId)
}
})
</script>

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

<v-card>
<v-data-table
v-if="nodes?.length"
v-model:items-per-page="authStore.nodes_per_page"
<v-data-table-server
v-model:items-per-page="nodesStore.nodesPerPage"
items-per-page-text="Nodes per page"
page-text="{0}-{1} of {2}"
v-model:page="authStore.nodes_page"
:page="nodesStore.nodesPage"
:items-per-page-options="itemsPerPageOptions"
:headers="headers"
:items="nodes"
:search="authStore.nodes_search"
v-model:sort-by="authStore.nodes_sort_by"
:custom-filter="filterNodes"
:items="nodesStore.nodesF"
:itemsLength="nodesStore.totalNodes"
:search="nodesStore.nodesSearch"
v-model:sort-by="nodesStore.nodesSortBy"
item-value="id"
class="elevation-1"
@update:options="nodesStore.fetchNodes"
>
<template v-slot:[`item.roundId`]="{ item }">
{{ formatRound(item['roundId']) }}
Expand All @@ -210,23 +174,23 @@ onUnmounted(() => {
<v-tooltip activator="parent">Reset</v-tooltip>
</template>
</v-data-table>
</v-data-table-server>
<v-spacer></v-spacer>
<div v-if="!nodes?.length" class="text-center m-5">No nodes</div>
<div v-if="nodes?.length">
<div v-if="!nodesStore.nodesF?.length" class="text-center m-5">No nodes</div>
<div v-if="nodesStore.totalNodes">
<v-text-field
v-model="authStore.nodes_search"
v-model="nodesStore.nodesSearch"
:append-inner-icon="mdiMagnify"
label="Search any node information"
variant="solo"
hide-details
/>
</div>
</v-card>
<div v-if="nodes?.error" class="text-center m-5">
<div class="text-danger">Failed to load nodes list: {{ nodes.error }}</div>
<div v-if="nodesStore.nodesF?.error" class="text-center m-5">
<div class="text-danger">Failed to load nodes list: {{ nodesF.error }}</div>
</div>
<div v-if="nodes?.loading" class="text-center m-5">
<div v-if="nodesStore.nodesF?.loading" class="text-center m-5">
<span class="spinner-border spinner-border-lg align-center"></span>
</div>
<div v-if="alert" class="alert alert-dismissable mt-3 mb-0" :class="alert.type">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Rounds_List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ const updateDataGrid = async () => {
isUpdating = true
try {
await roundsStore.getAllU()
if (!roundsU?.loading && !roundsU?.loading)
if (!roundsU?.loading && !roundsU?.error)
{
const oldData = [...rounds.value]
const newItems = []
Expand Down
107 changes: 107 additions & 0 deletions src/components/Statistics_List.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script setup>
// Copyright (C) 2024 Maxim [maxirmx] Samsonov (www.sw.consulting)
// All rights reserved.
// This file is a part of Dkg Frontend applcation
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import { onMounted, onUnmounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useStatisticsStore } from '@/stores/statistics.store.js'
const statisticsStore = useStatisticsStore()
const { statistics, statisticsU } = storeToRefs(statisticsStore)
statisticsStore.getStatistics()
import { useAlertStore } from '@/stores/alert.store.js'
const alertStore = useAlertStore()
const { alert } = storeToRefs(alertStore)
const headers = [
{ title: 'Function', align: 'center', key: 'name' },
{ title: 'Number of calls', align: 'center', key: 'count' },
{ title: 'Avg. elapsed time (ms)', align: 'center', key: 'timePerCall' }
]
let intervalId = null
let isUpdating = false
onMounted(() => {
intervalId = setInterval(async () => {
if (isUpdating) {
return
}
isUpdating = true
await statisticsStore.getStatisticsU()
if (!statisticsU?.loading && !statisticsU?.error) {
statistics.value = statistics.value.map(stat => {
const updatedStat = statisticsU.value.find(item => item.name === stat.name)
return updatedStat ? { ...stat, count: updatedStat.count, timePerCall: updatedStat.timePerCall } : stat
})
}
isUpdating = false
}, 10000)
})
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId)
}
})
</script>
<template>
<div class="settings table-3">
<h1 class="orange">Node controller statistics</h1>
<hr class="hr" />
<v-card>
<v-data-table
v-if="statistics?.length"
:headers="headers"
:items="statistics"
item-value="name"
class="elevation-1"
>
<template v-slot:[`item.timePerCall`]="{ item }">
{{ item.timePerCall.toFixed(2) }}
</template>
</v-data-table>
<div v-if="!statistics?.length" class="text-center m-5">No data</div>
</v-card>
<div v-if="statistics?.loading" class="text-center m-5">
<span class="spinner-border spinner-border-lg align-center"></span>
</div>
<div v-if="statistics?.error" class="text-center m-5">
<div class="text-danger">Failed to load data: {{ statistics.error }}</div>
</div>
<div v-if="alert" class="alert alert-dismissable mt-3 mb-0" :class="alert.type">
<button @click="alertStore.clear()" class="btn btn-link close">×</button>
{{ alert.message }}
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion src/helpers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ let apiUrl;
if (import.meta.env.MODE === 'production') {
apiUrl = `https://${window.location.hostname}:8081/api`;
} else {
apiUrl = `https://${import.meta.env.VITE_API_HOST}:8081/api`;
apiUrl = `http://${import.meta.env.VITE_API_HOST}:8080/api`;
}

export { apiUrl };
Expand Down
5 changes: 5 additions & 0 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ const router = createRouter({
name: 'Rounds',
component: () => import('@/views/Rounds_View.vue')
},
{
path: '/statistics',
name: 'Statistics',
component: () => import('@/views/Statistics_View.vue')
},
]
})

Expand Down
Loading

0 comments on commit cefcfb9

Please sign in to comment.