Added paging.

This commit is contained in:
baldo 2022-06-02 15:02:31 +02:00
parent 956889be11
commit 87839f4faa
4 changed files with 176 additions and 19 deletions

View 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>

View file

@ -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;
}, },
}, },
}); });

View file

@ -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);

View file

@ -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>