Added paging.
This commit is contained in:
parent
956889be11
commit
87839f4faa
4 changed files with 176 additions and 19 deletions
113
frontend/src/components/Pager.vue
Normal file
113
frontend/src/components/Pager.vue
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, defineProps} from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
itemsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
totalItems: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstItem = computed(() => {
|
||||||
|
console.log(props.totalItems, props.page, props.itemsPerPage);
|
||||||
|
return Math.min(props.totalItems, (props.page - 1) * props.itemsPerPage + 1)
|
||||||
|
});
|
||||||
|
const lastItem = computed(() => Math.min(props.totalItems, firstItem.value + props.itemsPerPage - 1));
|
||||||
|
const lastPage = computed(() => Math.ceil(props.totalItems / props.itemsPerPage));
|
||||||
|
const pages = computed(() => {
|
||||||
|
const pages: number[] = [];
|
||||||
|
if (lastPage.value <= 2) {
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
let p1 = props.page - 1;
|
||||||
|
if (props.page === lastPage.value) {
|
||||||
|
p1 = props.page - 2;
|
||||||
|
}
|
||||||
|
if (p1 <= 1) {
|
||||||
|
p1 = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let p = p1; p <= p1 + 2; p += 1) {
|
||||||
|
if (p < lastPage.value) {
|
||||||
|
pages.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showFirstEllipsis = computed(
|
||||||
|
() => pages.value.length > 0 && pages.value[0] > 2
|
||||||
|
);
|
||||||
|
const showLastEllipsis = computed(
|
||||||
|
() => pages.value.length > 0 && pages.value[pages.value.length - 1] < lastPage.value - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "changePage", page: number): void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function toPage(page: number): void {
|
||||||
|
emit("changePage", page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// noinspection JSIncompatibleTypesComparison
|
||||||
|
const classes = computed(() => (p: number) => p === props.page ? ["current-page"] : []);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav>
|
||||||
|
<span class="total">
|
||||||
|
<strong>{{ firstItem }}</strong>
|
||||||
|
-
|
||||||
|
<strong>{{ lastItem }}</strong>
|
||||||
|
von
|
||||||
|
<strong>{{ totalItems }}</strong>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-if="page > 1" @click="toPage(page - 1)">‹</li>
|
||||||
|
<li :class="classes(1)" @click="toPage(1)">1</li>
|
||||||
|
<li v-if="showFirstEllipsis" class="ellipsis">…</li>
|
||||||
|
<li v-for="page in pages" :class="classes(page)" @click="toPage(page)">{{ page }}</li>
|
||||||
|
<li v-if="showLastEllipsis" class="ellipsis">…</li>
|
||||||
|
<li v-if="lastPage > 1" :class="classes(lastPage)" @click="toPage(lastPage)">{{ lastPage }}</li>
|
||||||
|
<li v-if="page < lastPage" @click="toPage(page + 1)">›</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../scss/variables";
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 2em;
|
||||||
|
height: 1.5em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.current-page {
|
||||||
|
color: $variant-color-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ellipsis {
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,6 +4,8 @@ import {internalApi} from "@/utils/Api";
|
||||||
|
|
||||||
interface NodesStoreState {
|
interface NodesStoreState {
|
||||||
nodes: EnhancedNode[];
|
nodes: EnhancedNode[];
|
||||||
|
page: number;
|
||||||
|
nodesPerPage: number;
|
||||||
totalNodes: number;
|
totalNodes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +14,8 @@ export const useNodesStore = defineStore({
|
||||||
state(): NodesStoreState {
|
state(): NodesStoreState {
|
||||||
return {
|
return {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
page: 1,
|
||||||
|
nodesPerPage: 20,
|
||||||
totalNodes: 0,
|
totalNodes: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -22,16 +26,29 @@ export const useNodesStore = defineStore({
|
||||||
|
|
||||||
getTotalNodes(state: NodesStoreState): number {
|
getTotalNodes(state: NodesStoreState): number {
|
||||||
return state.totalNodes;
|
return state.totalNodes;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
getNodesPerPage(state: NodesStoreState): number {
|
||||||
|
return state.nodesPerPage
|
||||||
|
},
|
||||||
|
|
||||||
|
getPage(state: NodesStoreState): number {
|
||||||
|
return state.page
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async refresh(): Promise<void> {
|
async refresh(page: number, nodesPerPage: number): Promise<void> {
|
||||||
|
// TODO: Handle paging
|
||||||
const result = await internalApi.getPagedList<EnhancedNode>(
|
const result = await internalApi.getPagedList<EnhancedNode>(
|
||||||
"nodes",
|
"nodes",
|
||||||
isEnhancedNode
|
isEnhancedNode,
|
||||||
|
page,
|
||||||
|
nodesPerPage,
|
||||||
);
|
);
|
||||||
this.nodes = result.entries;
|
this.nodes = result.entries;
|
||||||
this.totalNodes = result.total;
|
this.totalNodes = result.total;
|
||||||
|
this.page = page;
|
||||||
|
this.nodesPerPage = nodesPerPage;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,12 +23,22 @@ class Api {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private toURL(path: string): string {
|
private toURL(path: string, queryParams?: object): string {
|
||||||
return this.baseURL + this.apiPrefix + path;
|
let queryString = "";
|
||||||
|
if (queryParams) {
|
||||||
|
const queryStrings: string[] = [];
|
||||||
|
for (const [key, value] of Object.entries(queryParams)) {
|
||||||
|
queryStrings.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||||
|
}
|
||||||
|
if (queryStrings.length > 0) {
|
||||||
|
queryString = `?${queryStrings.join("&")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.baseURL + this.apiPrefix + path + queryString;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doGet<T>(path: string, isT: TypeGuard<T>): Promise<Response<T>> {
|
private async doGet<T>(path: string, isT: TypeGuard<T>, queryParams?: object): Promise<Response<T>> {
|
||||||
const url = this.toURL(path);
|
const url = this.toURL(path, queryParams);
|
||||||
const result = await fetch(url);
|
const result = await fetch(url);
|
||||||
const json = await result.json();
|
const json = await result.json();
|
||||||
|
|
||||||
|
@ -48,8 +58,16 @@ class Api {
|
||||||
return response.result;
|
return response.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPagedList<T>(path: string, isT: TypeGuard<T>): Promise<PagedListResult<T>> {
|
async getPagedList<T>(
|
||||||
const response = await this.doGet(path, toIsArray(isT));
|
path: string,
|
||||||
|
isT: TypeGuard<T>,
|
||||||
|
page: number,
|
||||||
|
itemsPerPage: number,
|
||||||
|
): Promise<PagedListResult<T>> {
|
||||||
|
const response = await this.doGet(path, toIsArray(isT), {
|
||||||
|
_page: page,
|
||||||
|
_perPage: itemsPerPage,
|
||||||
|
});
|
||||||
const totalStr = response.headers.get("x-total-count");
|
const totalStr = response.headers.get("x-total-count");
|
||||||
const total = parseInteger(totalStr, 10);
|
const total = parseInteger(totalStr, 10);
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useNodesStore} from "@/stores/nodes";
|
import {useNodesStore} from "@/stores/nodes";
|
||||||
import {ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import type {EnhancedNode, MAC} from "@/types";
|
import type {EnhancedNode, MAC} from "@/types";
|
||||||
|
import Pager from "@/components/Pager.vue";
|
||||||
|
|
||||||
type NodeRedactField = "nickname" | "email" | "token";
|
type NodeRedactField = "nickname" | "email" | "token";
|
||||||
type NodeRedactFieldsMap = Partial<Record<NodeRedactField, boolean>>;
|
type NodeRedactFieldsMap = Partial<Record<NodeRedactField, boolean>>;
|
||||||
type NodesRedactFieldsMap = Partial<Record<MAC, NodeRedactFieldsMap>>;
|
type NodesRedactFieldsMap = Partial<Record<MAC, NodeRedactFieldsMap>>;
|
||||||
|
|
||||||
const nodes = useNodesStore();
|
const nodes = useNodesStore();
|
||||||
const page = ref(0);
|
|
||||||
const redactFieldsByDefault = ref(true);
|
const redactFieldsByDefault = ref(true);
|
||||||
const nodesRedactFieldsMap = ref({} as NodesRedactFieldsMap)
|
const nodesRedactFieldsMap = ref({} as NodesRedactFieldsMap)
|
||||||
|
|
||||||
function refresh(): void {
|
async function refresh(page: number): Promise<void> {
|
||||||
nodes.refresh();
|
await nodes.refresh(page, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
function redactAllFields(shallRedactFields: boolean): void {
|
function redactAllFields(shallRedactFields: boolean): void {
|
||||||
|
@ -39,11 +39,7 @@ function setRedactField(node: EnhancedNode, field: NodeRedactField, value: boole
|
||||||
redactFieldsMap[field] = value;
|
redactFieldsMap[field] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasOnlineState(node: EnhancedNode): boolean {
|
onMounted(async () => await refresh(1));
|
||||||
return node.onlineState !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -62,6 +58,13 @@ refresh();
|
||||||
Sensible Daten ausblenden
|
Sensible Daten ausblenden
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Pager
|
||||||
|
:page="nodes.getPage"
|
||||||
|
:itemsPerPage="nodes.getNodesPerPage"
|
||||||
|
:totalItems="nodes.getTotalNodes"
|
||||||
|
@changePage="refresh" />
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -82,7 +85,7 @@ refresh();
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="node in nodes.getNodes"
|
v-for="node in nodes.getNodes"
|
||||||
:class="[hasOnlineState ? node.onlineState.toLowerCase() : 'online-state-unknown']">
|
:class="[node.onlineState ? node.onlineState.toLowerCase() : 'online-state-unknown']">
|
||||||
<td>{{node.hostname}}</td>
|
<td>{{node.hostname}}</td>
|
||||||
<td v-if="shallRedactField(node, 'nickname')">
|
<td v-if="shallRedactField(node, 'nickname')">
|
||||||
<span
|
<span
|
||||||
|
@ -175,6 +178,12 @@ refresh();
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<Pager
|
||||||
|
:page="nodes.getPage"
|
||||||
|
:itemsPerPage="nodes.getNodesPerPage"
|
||||||
|
:totalItems="nodes.getTotalNodes"
|
||||||
|
@changePage="refresh" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
Loading…
Reference in a new issue