Skip to content

Commit

Permalink
Merge pull request #285 from mohamedsalem401/events-pagination
Browse files Browse the repository at this point in the history
Add pagination for events table
  • Loading branch information
naelob authored Feb 19, 2024
2 parents e902888 + ee29180 commit 7de1818
Show file tree
Hide file tree
Showing 15 changed files with 470 additions and 33 deletions.
1 change: 1 addition & 0 deletions apps/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"sonner": "^1.4.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"use-query-params": "^2.2.1",
"zod": "^3.22.4",
"zustand": "^4.4.7"
},
Expand Down
26 changes: 15 additions & 11 deletions apps/webapp/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import './App.css';
import { ThemeProvider } from '@/components/theme-provider';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import LogsPage from './components/events';
import ConnectionsPage from './components/connections';
import TaskPage from './components/events/EventsTable';
Expand Down Expand Up @@ -33,17 +35,19 @@ function App() {
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme='dark' storageKey='vite-ui-theme'>
<Router>
<Routes>
<Route path='/' element={<RootLayout />}>
<Route index element={<ConnectionsPage />} />
<Route path='/dashboard' element={<DashboardPage />} />
<Route path='/logs' element={<LogsPage />} />
<Route path='/tasks' element={<TaskPage />} />
<Route path='/configuration' element={<ConfigurationPage />} />
<Route path='/connections' element={<ConnectionsPage />} />
<Route path='/api-keys' element={<ApiKeysPage />} />
</Route>
</Routes>
<QueryParamProvider adapter={ReactRouter6Adapter}>
<Routes>
<Route path='/' element={<RootLayout />}>
<Route index element={<ConnectionsPage />} />
<Route path='/dashboard' element={<DashboardPage />} />
<Route path='/logs' element={<LogsPage />} />
<Route path='/tasks' element={<TaskPage />} />
<Route path='/configuration' element={<ConfigurationPage />} />
<Route path='/connections' element={<ConnectionsPage />} />
<Route path='/api-keys' element={<ApiKeysPage />} />
</Route>
</Routes>
</QueryParamProvider>
</Router>
</ThemeProvider>
</QueryClientProvider>
Expand Down
84 changes: 84 additions & 0 deletions apps/webapp/src/components/api-data-table-pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Button } from './ui/button';

import { type UseQueryPaginationReturn } from '@/hooks/use-query-pagination';

interface DataTablePaginationProps extends UseQueryPaginationReturn {
selected: number;
isLoading: boolean;
}

export function ApiDataTablePagination(props: DataTablePaginationProps) {
return (
<div className='flex items-center justify-between px-2'>
<div className='flex-1 text-sm text-muted-foreground'>
{props.selected} of {props.totalItems} row(s) selected.
</div>
<div className='flex items-center space-x-6 lg:space-x-8'>
<div className='flex items-center space-x-2'>
<p className='text-sm font-medium'>Rows per page</p>
<Select
disabled={props.totalItems <= 10 || props.isLoading}
value={`${props.pageSize}`}
onValueChange={(value) => {
props.setPageSize(Number(value));
}}
>
<SelectTrigger className='h-8 w-[70px]'>
<SelectValue placeholder={props.pageSize} />
</SelectTrigger>
<SelectContent side='top'>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='flex w-[100px] items-center justify-center text-sm font-medium'>
Page {props.page} of {props.totalPages}
</div>
<div className='flex items-center space-x-2'>
<Button
variant='outline'
className='hidden h-8 w-8 p-0 lg:flex'
onClick={() => props.resetPage()}
disabled={!props.previousEnabled || props.isLoading}
>
<span className='sr-only'>Go to first page</span>
<DoubleArrowLeftIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='h-8 w-8 p-0'
onClick={() => props.setPreviousPage()}
disabled={!props.previousEnabled || props.isLoading}
>
<span className='sr-only'>Go to previous page</span>
<ChevronLeftIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='h-8 w-8 p-0'
onClick={() => props.setNextPage()}
disabled={!props.nextEnabled || props.isLoading}
>
<span className='sr-only'>Go to next page</span>
<ChevronRightIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='hidden h-8 w-8 p-0 lg:flex'
onClick={() => props.setPage(props.totalPages)}
disabled={!props.nextEnabled || props.isLoading}
>
<span className='sr-only'>Go to last page</span>
<DoubleArrowRightIcon className='h-4 w-4' />
</Button>
</div>
</div>
</div>
);
}
102 changes: 102 additions & 0 deletions apps/webapp/src/components/api-data-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { useState } from 'react';

import { ApiDataTablePagination } from './api-data-table-pagination';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { LoadingSpinner } from './connections/components/LoadingSpinner';

import { type UseQueryPaginationReturn } from '@/hooks/use-query-pagination';

interface DataTableProps<TData, TValue> extends UseQueryPaginationReturn {
columns: ColumnDef<TData, TValue>[];
data: TData[];
isLoading: boolean;
}
export function ApiDataTable<TData, TValue>({
columns,
data,
isLoading,
...paginationProps
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = useState({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [sorting, setSorting] = useState<SortingState>([]);

const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
});

return (
<div className='space-y-4'>
<div className='relative overflow-hidden rounded-md border'>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className='h-24 text-center'>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>

{isLoading && (
<div className='absolute left-0 top-0 flex h-full w-full items-center justify-center bg-background/50 backdrop-blur-sm'>
<LoadingSpinner className='' />
</div>
)}
</div>
<ApiDataTablePagination
selected={table.getSelectedRowModel().rows.length}
isLoading={isLoading}
{...paginationProps}
/>
</div>
);
}
28 changes: 22 additions & 6 deletions apps/webapp/src/components/events/EventsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { columns } from "./components/columns"
import { DataTable } from "../shared/data-table"
import { ApiDataTable } from '../api-data-table';
import useEvents from "@/hooks/useEvents";
import { DataTableLoading } from "../shared/data-table-loading";
import { events as Event } from "api";
import { useEventsCount } from '@/hooks/use-events-count';
import { useQueryPagination } from '@/hooks/use-query-pagination';

export default function EventsTable() {
const { data: events, isLoading, error } = useEvents();
const { data: eventsCount } = useEventsCount();

const pagination = useQueryPagination({ totalItems: eventsCount });

const {
data: events,
isLoading,
isFetching,
error,
} = useEvents({
page: pagination.page,
pageSize: pagination.pageSize,
});

//TODO
const transformedEvents = events?.map((event: Event) => ({
Expand All @@ -17,8 +31,8 @@ export default function EventsTable() {
date: event.timestamp.toLocaleString(), // convert Date to string
}));

const sortedTransformedEvents = transformedEvents?.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

// Already did it at api level
// const sortedTransformedEvents = transformedEvents?.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

if(isLoading){
return (
Expand All @@ -33,7 +47,9 @@ export default function EventsTable() {

return (
<>
{sortedTransformedEvents && <DataTable data={sortedTransformedEvents} columns={columns}/>}
{transformedEvents && (
<ApiDataTable data={transformedEvents} columns={columns} {...pagination} isLoading={isFetching} />
)}
</>
)
);
}
20 changes: 20 additions & 0 deletions apps/webapp/src/hooks/use-events-count.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import config from '@/utils/config';

const fetchEventsCount = async (): Promise<number> => {
const response = await fetch(`${config.API_URL}/events/count`);

if (!response.ok) {
throw new Error('Network response was not ok');
}

return response.json();
};

export const useEventsCount = () => {
return useQuery({
queryKey: ['events count'],
queryFn: fetchEventsCount,
placeholderData: keepPreviousData,
});
};
65 changes: 65 additions & 0 deletions apps/webapp/src/hooks/use-query-pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useCallback, useMemo } from 'react';
import { useQueryParam } from 'use-query-params';

import { NumberParamWithDefault } from '@/lib/utils';

function getTotalPages(totalItems: number, pageSize: number) {
return Math.ceil(totalItems / pageSize);
}

interface UseQueryPaginationProps {
totalItems?: number;
}

const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 10;

export const useQueryPagination = ({ totalItems = 0 }: UseQueryPaginationProps = {}) => {
const [page, setPage] = useQueryParam<number>('page', NumberParamWithDefault(DEFAULT_PAGE));
const [pageSize, setPageSize] = useQueryParam<number>('pageSize', NumberParamWithDefault(DEFAULT_PAGE_SIZE));

const totalPages = useMemo(() => {
return getTotalPages(totalItems, pageSize);
}, [totalItems, pageSize]);

const nextEnabled = useMemo(() => page < totalPages, [page, totalPages]);
const previousEnabled = useMemo(() => page > 1, [page]);

const setNextPage = useCallback(() => {
if (!nextEnabled) return;
setPage((page) => page + 1);
}, [nextEnabled, setPage]);

const setPreviousPage = useCallback(() => {
if (!previousEnabled) return;
setPage((page) => page - 1);
}, [previousEnabled, setPage]);

const resetPage = useCallback(() => {
setPage(DEFAULT_PAGE);
}, [setPage]);

const handlePageSizeChange = useCallback(
(newPageSize: number) => {
setPageSize(newPageSize);
setPage(DEFAULT_PAGE);
},
[setPageSize, setPage]
);

return {
page,
setPage,
resetPage,
pageSize,
setPageSize: handlePageSizeChange,
totalItems,
totalPages: totalPages === 0 ? 1 : totalPages,
nextEnabled,
previousEnabled,
setNextPage,
setPreviousPage,
};
};
export type UseQueryPaginationReturn = ReturnType<typeof useQueryPagination>;

Loading

0 comments on commit 7de1818

Please sign in to comment.