From bf42e61b8e03881cc7a749910f393732aeb8ad14 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 18 Dec 2024 20:28:34 +0100 Subject: [PATCH] Implement pagination --- frontend/src/page/admin/GuildDetails.tsx | 13 ++++++++---- frontend/src/page/admin/Guilds.tsx | 26 ++++++++++++++++-------- frontend/src/service/api.ts | 26 +++++++++++++++--------- frontend/src/util.ts | 14 +++++++++++++ lib/src/web_app/api_server.dart | 12 ++++++++--- lib/src/web_app/mapper/tags_mapper.dart | 4 ++-- 6 files changed, 68 insertions(+), 27 deletions(-) diff --git a/frontend/src/page/admin/GuildDetails.tsx b/frontend/src/page/admin/GuildDetails.tsx index d5c89e7..3775808 100644 --- a/frontend/src/page/admin/GuildDetails.tsx +++ b/frontend/src/page/admin/GuildDetails.tsx @@ -18,6 +18,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import {parseISO} from "date-fns"; import {format} from "date-fns/format"; import {useDebounce} from "use-debounce"; +import useUpdateEffect from "../../util"; interface GuildDetailsDataProps { dataPromise: Promise @@ -54,14 +55,18 @@ function TagsDataPaper({dataPromise}: GuildDetailsDataProps) { const [tags, setTags] = useState(data.tags); const [searchQuery, setSearchQuery] = useState(null); const [searchQueryDebounced] = useDebounce(searchQuery, 500); + const [paginationModel, setPaginationModel] = useState({ + pageSize: 5, + page: 0, + }); - useEffect(() => { + useUpdateEffect(() => { if (searchQueryDebounced == null) { return; } - fetchGuildTags({id: data.id, query: searchQueryDebounced}).then((t) => setTags(t)); - }, [searchQueryDebounced]); + fetchGuildTags({id: data.id, query: searchQueryDebounced, page: paginationModel.page, perPage: paginationModel.pageSize}).then((t) => setTags(t)); + }, [searchQueryDebounced, paginationModel]); const columns: GridColDef[] = [ { field: 'name', headerName: 'Name' }, @@ -75,7 +80,7 @@ function TagsDataPaper({dataPromise}: GuildDetailsDataProps) { Tags setSearchQuery(e.target.value)} /> - + ; } diff --git a/frontend/src/page/admin/Guilds.tsx b/frontend/src/page/admin/Guilds.tsx index 474d20b..59d78c6 100644 --- a/frontend/src/page/admin/Guilds.tsx +++ b/frontend/src/page/admin/Guilds.tsx @@ -1,15 +1,13 @@ -import React, {Suspense, use} from 'react'; +import React, {Suspense, use, useEffect, useState} from 'react'; import {Base} from "../../component/Base"; -import {fetchGuilds, GuildSummary} from "../../service/api"; +import {fetchGuilds, fetchGuildTags, GuildSummary} from "../../service/api"; import {DataGrid, GridActionsCellItem, GridColDef, GridRowParams} from "@mui/x-data-grid"; -import {Alert, Avatar, Stack, Tooltip, Typography} from "@mui/material"; -import {getGuildIcon} from "../../constants"; +import {Alert, Stack, Tooltip, Typography} from "@mui/material"; import {GridCell} from "../../component/GridCell"; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import {getUser} from "../../service/auth"; import OpenInFullIcon from '@mui/icons-material/OpenInFull'; import {useNavigate} from "react-router-dom"; -import {NavigateFunction} from "react-router/dist/development"; import {getGuildNameElement} from "../../guildUtil"; interface GuildRowDef { @@ -47,8 +45,20 @@ const guildDataPromise = fetchGuilds().then(guilds => { }); function Grid() { - const rows = use(guildDataPromise); const navigate = useNavigate(); + const initialRows = use(guildDataPromise); + + const [paginationModel, setPaginationModel] = useState({ + pageSize: 25, + page: 0, + }); + const [rows, setRows] = useState(initialRows); + + useEffect(() => { + fetchGuilds({page: paginationModel.page, perPage: paginationModel.pageSize}).then(guilds => { + return mapApiDataToRows(guilds); + }).then((r) => setRows(r)); + }, [paginationModel]); const columns: GridColDef[] = [ { field: 'name', headerName: 'Name', flex: 1, renderCell: params => params.value}, @@ -83,7 +93,7 @@ function Grid() { ]; return ( - + ); } @@ -93,7 +103,7 @@ export default function Guilds() { Table represents cached data that is available for bot at the moment Loading...}> - + diff --git a/frontend/src/service/api.ts b/frontend/src/service/api.ts index 4eea930..696c9e8 100644 --- a/frontend/src/service/api.ts +++ b/frontend/src/service/api.ts @@ -124,26 +124,32 @@ export interface GuildDetails { tags: Tag[], } +interface PaginationParameters { + perPage?: number, + page?: number, +} + +interface FetchGuildTagsParameters extends PaginationParameters{ + id: string, + query?: string, +} + export async function fetchBotInfo(): Promise { return await request({path: "/api/server-info"}); } -export async function fetchGuilds(): Promise { - return await request({path: "/api/guilds", auth: true}); +export async function fetchGuilds({perPage = 25, page = 0}: PaginationParameters = {}): Promise { + const params = [["perPage", perPage.toString()], ["page", (page + 1).toString()]]; + + return await request({path: "/api/guilds", auth: true, searchParams: params}); } export async function fetchGuildDetails(id: string): Promise { return await request({path: `/api/guilds/${id}`, auth: true}); } -interface FetchGuildTags { - id: string, - perPage?: number, - query?: string, -} - -export async function fetchGuildTags({id, perPage = 5, query}: FetchGuildTags): Promise { - const params = [["perPage", perPage.toString()]]; +export async function fetchGuildTags({id, perPage = 5, page = 0, query}: FetchGuildTagsParameters): Promise { + const params = [["perPage", perPage.toString()], ["page", (page + 1).toString()]]; if (query != null && query !== '') { params.push(["query", query]) } diff --git a/frontend/src/util.ts b/frontend/src/util.ts index 243d185..4ec38c4 100644 --- a/frontend/src/util.ts +++ b/frontend/src/util.ts @@ -1,3 +1,17 @@ +import {useEffect, useRef} from "react"; + export function containsAll(haystack: T[], required: T[]): boolean { return required.every(ai => haystack.includes(ai)); } + +export default function useUpdateEffect(effect: Function, dependencies = []) { + const isInitialMount = useRef(true); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + } else { + return effect(); + } + }, dependencies); +} diff --git a/lib/src/web_app/api_server.dart b/lib/src/web_app/api_server.dart index 9c2254e..950cbc6 100644 --- a/lib/src/web_app/api_server.dart +++ b/lib/src/web_app/api_server.dart @@ -30,7 +30,12 @@ class WebServer { Future _handleGuilds(shelf.Request request) async { final client = Injector.appInstance.get(); - final guildData = await mapGuildsToGuildReducedData(client.guilds.cache.values).toList(); + final perPage = int.tryParse(request.requestedUri.queryParameters['perPage'] ?? '10') ?? 10; + final page = int.tryParse(request.requestedUri.queryParameters['page'] ?? '1') ?? 1; + + final guilds = client.guilds.cache.values.skip(perPage * (page - 1)).take(page); + + final guildData = await mapGuildsToGuildReducedData(guilds).toList(); return createOkResponse(guildData); } @@ -42,10 +47,11 @@ class WebServer { } final searchQuery = request.requestedUri.queryParameters['query']; - final tagsLimit = int.tryParse(request.requestedUri.queryParameters['perPage'] ?? '5') ?? 5; + final perPage = int.tryParse(request.requestedUri.queryParameters['perPage'] ?? '5') ?? 5; + final page = int.tryParse(request.requestedUri.queryParameters['page'] ?? '1') ?? 1; return createOkResponse( - await mapGuildTagsToData(Snowflake.parse(guildParam), tagsLimit, searchQuery: searchQuery).toList(), + await mapGuildTagsToData(Snowflake.parse(guildParam), perPage, searchQuery: searchQuery, page: page).toList(), ); } diff --git a/lib/src/web_app/mapper/tags_mapper.dart b/lib/src/web_app/mapper/tags_mapper.dart index ff8b056..ef4c642 100644 --- a/lib/src/web_app/mapper/tags_mapper.dart +++ b/lib/src/web_app/mapper/tags_mapper.dart @@ -3,7 +3,7 @@ import 'package:nyxx/nyxx.dart'; import 'package:running_on_dart/src/modules/tag.dart'; import 'package:running_on_dart/src/web_app/utils.dart'; -Stream mapGuildTagsToData(Snowflake guildId, int tagsLimit, {String? searchQuery}) async* { +Stream mapGuildTagsToData(Snowflake guildId, int tagsLimit, {String? searchQuery, int page = 1}) async* { final tagsModule = Injector.appInstance.get(); var tags = tagsModule.getGuildTags(guildId); @@ -11,7 +11,7 @@ Stream mapGuildTagsToData(Snowflake guildId, int tagsLimit, {St tags = tags.where((tag) => tag.name.contains(searchQuery)); } - for (final tag in tags.take(tagsLimit)) { + for (final tag in tags.skip(tagsLimit * (page - 1)).take(tagsLimit)) { yield { 'id': tag.id, 'name': tag.name,