feat(frontend): user list pagination
This commit is contained in:
parent
335a97740d
commit
b6bb849e2c
@ -51,6 +51,11 @@
|
||||
button:focus {
|
||||
@apply ring-1 ring-neutral-400 ring-offset-1;
|
||||
}
|
||||
button:disabled {
|
||||
@apply opacity-25;
|
||||
@apply bg-neutral-100;
|
||||
@apply border-2 border-neutral-200;
|
||||
}
|
||||
|
||||
table {
|
||||
@apply border-collapse;
|
||||
|
@ -1,11 +1,7 @@
|
||||
<script lang='ts'>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const errorMessages : String[] = [];
|
||||
let error = false;
|
||||
let loading = true;
|
||||
|
||||
type User = {
|
||||
type Item = {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
@ -13,124 +9,131 @@
|
||||
created: Date;
|
||||
updated: Date;
|
||||
}
|
||||
let currentItems: User[] = [];
|
||||
let totalItemCount = 0;
|
||||
let limit = 10;
|
||||
let currentPage = 1;
|
||||
let skip = 0;
|
||||
$: totalPageCount = Math.ceil(totalItemCount / limit);
|
||||
const _tableColumnCount = 6;
|
||||
|
||||
async function getUsersCount() {
|
||||
const response = await fetch(`/api/users/total/`);
|
||||
const responseJson = await response.json();
|
||||
let currentState: 'finished' | 'loading' | 'error';
|
||||
let errorMessages: string[] = [];
|
||||
|
||||
if (response.ok) {
|
||||
if (responseJson) {
|
||||
if (responseJson.hasOwnProperty('total')) {
|
||||
if (typeof responseJson.total === 'number') {
|
||||
return responseJson.total;
|
||||
}
|
||||
}
|
||||
}
|
||||
error = true;
|
||||
let currentItems: Item[] = [];
|
||||
let totalItemCount: number | null;
|
||||
|
||||
const itemsPerPageOptions = [10, 25, 50, 100];
|
||||
let currentItemsPerPage: number;
|
||||
|
||||
let currentPage: number;
|
||||
let lastPage: number;
|
||||
$: if (totalItemCount) {
|
||||
lastPage = Math.ceil(totalItemCount / currentItemsPerPage) - 1;
|
||||
} else {
|
||||
// Try to parse the error messages
|
||||
if (responseJson) {
|
||||
if (responseJson.hasOwnProperty('detail')) {
|
||||
if (Array.isArray(responseJson.detail)) {
|
||||
for (let errorObj of responseJson.detail) {
|
||||
if (errorObj.hasOwnProperty('msg')) {
|
||||
if (typeof errorObj.msg === 'string') {
|
||||
errorMessages.push(errorObj.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMessages.length === 0) errorMessages.push("Error during API call.")
|
||||
error = true;
|
||||
}
|
||||
lastPage = 0;
|
||||
}
|
||||
|
||||
async function getUsers(skip=0, limit=100) {
|
||||
loading = true;
|
||||
$: currentItemsOffset = currentPage * currentItemsPerPage;
|
||||
|
||||
totalItemCount = await getUsersCount();
|
||||
if (error) return;
|
||||
$: gotoPrevPageDisabled = currentState !== 'finished' || currentPage === 0 || lastPage === 0;
|
||||
async function handleGotoPrevPage() {
|
||||
if (gotoPrevPageDisabled) return;
|
||||
|
||||
const response = await fetch(`/api/users/?skip=${skip}&limit=${limit}`);
|
||||
const responseJson = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentItems = responseJson;
|
||||
loading = false;
|
||||
return responseJson;
|
||||
} else {
|
||||
// Try to parse the error messages
|
||||
if (responseJson) {
|
||||
if (responseJson.hasOwnProperty('detail')) {
|
||||
if (Array.isArray(responseJson.detail)) {
|
||||
for (let errorObj of responseJson.detail) {
|
||||
if (errorObj.hasOwnProperty('msg')) {
|
||||
if (typeof errorObj.msg === 'string') {
|
||||
errorMessages.push(errorObj.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrevClick() {
|
||||
currentPage -= 1;
|
||||
skip -= limit;
|
||||
getUsers(skip, limit);
|
||||
await updateTable();
|
||||
}
|
||||
|
||||
function handleNextClick() {
|
||||
$: gotoNextPageDisabled = currentState !== 'finished' || currentPage === lastPage || lastPage === 0;
|
||||
async function handleGotoNextPage() {
|
||||
if (gotoNextPageDisabled) return;
|
||||
|
||||
currentPage += 1;
|
||||
skip += limit;
|
||||
getUsers(skip, limit);
|
||||
await updateTable();
|
||||
}
|
||||
|
||||
function handleFirstClick() {
|
||||
currentPage = 1;
|
||||
skip = 0;
|
||||
getUsers(skip, limit);
|
||||
$: gotoFirstPageDisabled = currentState !== 'finished' || currentPage === 0 || lastPage === 0;
|
||||
async function handleGotoFirstPage() {
|
||||
if (gotoFirstPageDisabled) return;
|
||||
|
||||
currentPage = 0;
|
||||
await updateTable();
|
||||
}
|
||||
|
||||
function handleLastClick() {
|
||||
currentPage = totalPageCount;
|
||||
skip = limit * (totalPageCount - 1);
|
||||
getUsers(skip, limit);
|
||||
$: gotoLastPageDisabled = currentState !== 'finished' || currentPage === lastPage || lastPage === 0;
|
||||
async function handleGotoLastPage() {
|
||||
if (gotoLastPageDisabled) return;
|
||||
|
||||
currentPage = lastPage;
|
||||
await updateTable();
|
||||
}
|
||||
|
||||
interface ApiError {
|
||||
// FIXME error message parsing doesn't work right now
|
||||
detail: { msg: string; loc?: string[]; type?: string }[]
|
||||
}
|
||||
|
||||
async function getTotalItemCount(): Promise<number> {
|
||||
interface ApiResponse {
|
||||
total: number
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/users/total/`);
|
||||
const json = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
// Capture error messages from response body
|
||||
const apiError = json as ApiError;
|
||||
for (let detail of apiError.detail) {
|
||||
errorMessages.push(detail.msg);
|
||||
}
|
||||
throw new Error(`API request failed with status ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const apiResponse = json as ApiResponse;
|
||||
return apiResponse.total;
|
||||
}
|
||||
|
||||
async function getItems(offset: number = currentItemsOffset, count: number = currentItemsPerPage): Promise<Item[]> {
|
||||
const urlParameters = new URLSearchParams({
|
||||
skip: `${offset}`,
|
||||
limit: `${count}`,
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/users/?${urlParameters}`);
|
||||
const json = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
// Capture error messages from response body
|
||||
const apiError = json as ApiError;
|
||||
for (let detail of apiError.detail) {
|
||||
errorMessages.push(detail.msg);
|
||||
}
|
||||
throw new Error(`API request failed with status ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const apiResponse = json as Item[];
|
||||
return apiResponse;
|
||||
}
|
||||
|
||||
async function updateTable() {
|
||||
try {
|
||||
currentState = 'loading';
|
||||
totalItemCount = await getTotalItemCount();
|
||||
currentItems = await getItems();
|
||||
currentState = 'finished';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
currentState = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
getUsers(skip, limit);
|
||||
// Initialize vars
|
||||
currentItems = [];
|
||||
totalItemCount = null;
|
||||
currentItemsPerPage = itemsPerPageOptions[0];
|
||||
currentPage = 0;
|
||||
currentState = 'finished';
|
||||
|
||||
await updateTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<h1 class="text-center">Users</h1>
|
||||
{#if loading}
|
||||
<div class="min-h-screen flex flex-col items-center justify-center">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
{#if errorMessages.length}
|
||||
{#each errorMessages as errorMessage}
|
||||
<p style="color: red">{errorMessage}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
<p style="color: red">An unknown error occurred.</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<div>
|
||||
<table>
|
||||
<caption>List of users</caption>
|
||||
<thead>
|
||||
@ -144,6 +147,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if currentState === 'finished'}
|
||||
{#each currentItems as user}
|
||||
<tr>
|
||||
<td>{user.id}</td>
|
||||
@ -154,25 +158,37 @@
|
||||
<td>{new Date(user.updated).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if currentState === 'loading'}
|
||||
<tr>
|
||||
<td colspan={_tableColumnCount}>Loading...</td>
|
||||
</tr>
|
||||
{:else if currentState === 'error'}
|
||||
<tr>
|
||||
{#each errorMessages as message}
|
||||
<td colspan={_tableColumnCount} class="text-red-500">{message}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<select bind:value={limit} on:change={getUsers(skip, limit)}>
|
||||
<option value="10">
|
||||
10
|
||||
</option>
|
||||
<option value="25">
|
||||
25
|
||||
</option>
|
||||
<option value="50">
|
||||
50
|
||||
</option>
|
||||
</select>
|
||||
<button on:click={handleFirstClick} disabled={currentPage === 1} class="disabled:opacity-10">first</button>
|
||||
<button on:click={handlePrevClick} disabled={currentPage === 1} class="disabled:opacity-10">prev</button>
|
||||
<button on:click={handleNextClick} disabled={currentPage === totalPageCount} class="disabled:opacity-10">next</button>
|
||||
<button on:click={handleLastClick} disabled={currentPage === totalPageCount} class="disabled:opacity-10">last</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button on:click={handleGotoFirstPage} disabled={gotoFirstPageDisabled}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button on:click={handleGotoPrevPage} disabled={gotoPrevPageDisabled}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button on:click={handleGotoNextPage} disabled={gotoNextPageDisabled}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button on:click={handleGotoLastPage} disabled={gotoLastPageDisabled}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path fill-rule="evenodd" d="M10.21 14.77a.75.75 0 01.02-1.06L14.168 10 10.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
<path fill-rule="evenodd" d="M4.21 14.77a.75.75 0 01.02-1.06L8.168 10 4.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
Loading…
Reference in New Issue
Block a user