Skip to content

Commit de19e63

Browse files
Hari KiranHari Kiran
authored andcommitted
Fix #26 Add pagination for database page
1 parent bd29ae4 commit de19e63

File tree

2 files changed

+191
-78
lines changed

2 files changed

+191
-78
lines changed

src/pages/DatasetPage.tsx

Lines changed: 188 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
fetchDbInfo,
33
loadPaginatedData,
44
} from "../redux/neurojson/neurojson.action";
5+
import { Row } from "../redux/neurojson/types/neurojson.interface";
56
import {
67
Box,
78
Typography,
@@ -33,51 +34,89 @@ const DatasetPage: React.FC = () => {
3334
const { loading, error, data, limit, hasMore } = useAppSelector(
3435
(state: { neurojson: any }) => state.neurojson
3536
);
36-
const [currentOffset, setCurrentOffset] = useState(0);
37+
const [currentPage, setCurrentPage] = useState(1);
3738
const [pageSize, setPageSize] = useState(10);
39+
const totalPages = Math.ceil(limit / pageSize);
40+
const [searchQuery, setSearchQuery] = useState("");
3841

3942
useEffect(() => {
4043
if (dbName) {
4144
dispatch(fetchDbInfo(dbName.toLowerCase()));
4245
dispatch(
4346
loadPaginatedData({
4447
dbName: dbName.toLowerCase(),
45-
offset: 0,
48+
offset: (currentPage - 1) * pageSize,
4649
limit: pageSize,
4750
})
4851
);
4952
}
50-
}, [dbName, dispatch, pageSize]);
53+
}, [dbName, dispatch, currentPage, pageSize]);
5154

52-
const loadMoreData = () => {
53-
if (dbName && !loading) {
54-
const nextOffset = currentOffset + pageSize;
55-
setCurrentOffset(nextOffset);
56-
dispatch(
57-
loadPaginatedData({
58-
dbName: dbName.toLowerCase(),
59-
offset: nextOffset,
60-
limit: pageSize,
61-
})
62-
);
63-
}
55+
const handlePageChange = (page: number) => {
56+
if (!dbName || loading) return;
57+
58+
setCurrentPage(page);
59+
dispatch(loadPaginatedData({
60+
dbName: dbName.toLowerCase(),
61+
offset: (page - 1) * pageSize,
62+
limit: pageSize,
63+
}));
6464
};
6565

6666
const handlePageSizeChange = (event: any) => {
6767
setPageSize(event.target.value);
68-
setCurrentOffset(0); // Reset offset when changing page size
68+
setCurrentPage(1); // Reset offset when changing page size
69+
};
70+
71+
const getVisiblePageNumbers = () => {
72+
const visiblePages: (number | string)[] = [];
73+
const maxVisible = 6;
74+
75+
if (totalPages <= maxVisible + 2) {
76+
for (let i = 1; i <= totalPages; i++) visiblePages.push(i);
77+
} else {
78+
const start = Math.max(2, currentPage - 2);
79+
const end = Math.min(totalPages - 1, currentPage + 2);
80+
81+
visiblePages.push(1);
82+
if (start > 2) visiblePages.push("...");
83+
84+
for (let i = start; i <= end; i++) visiblePages.push(i);
85+
if (end < totalPages - 1) visiblePages.push("...");
86+
87+
visiblePages.push(totalPages);
88+
}
89+
90+
return visiblePages;
91+
};
92+
93+
const handlePrevNextPage = (direction: "prev" | "next") => {
94+
if (direction === "prev" && currentPage > 1) {
95+
handlePageChange(currentPage - 1);
96+
} else if (direction === "next" && currentPage < totalPages) {
97+
handlePageChange(currentPage + 1);
98+
}
6999
};
70100

101+
const filteredData = data.filter((doc: Row) =>
102+
(doc.value.name || "")
103+
.toLowerCase()
104+
.includes(searchQuery.toLowerCase())
105+
);
106+
71107
return (
72108
<Box sx={{ padding: { xs: 2, md: 4 } }}>
73109
<Box
74110
sx={{
75111
display: "flex",
112+
flexWrap: "wrap",
113+
justifyContent: "space-between",
76114
alignItems: "center",
77115
gap: 2,
78-
justifyContent: "space-between",
116+
mb: 3,
79117
}}
80118
>
119+
{/* Left: Title */}
81120
<Typography
82121
variant="h1"
83122
gutterBottom
@@ -90,67 +129,146 @@ const DatasetPage: React.FC = () => {
90129
Database: {dbName || "N/A"}
91130
</Typography>
92131

93-
<Box sx={{ mb: 3, display: "flex", alignItems: "center" }}>
132+
{/* Right: Total + Dropdown + Pagination */}
133+
<Box sx={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 3 }}>
134+
{/* Left: Total datasets */}
135+
<Typography
136+
sx={{
137+
fontWeight: 600,
138+
fontSize: "1.2rem",
139+
color: Colors.white,
140+
}}
141+
>
142+
Total datasets: {limit}
143+
</Typography>
144+
145+
{/* Search in page input */}
146+
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
147+
<Typography sx={{ fontWeight: 500, fontSize: "1rem", color: Colors.white }}>
148+
Search in page:
149+
</Typography>
150+
<input
151+
type="text"
152+
placeholder="Filter results in this page"
153+
value={searchQuery}
154+
onChange={(e) => setSearchQuery(e.target.value)}
155+
style={{
156+
padding: "6px 10px",
157+
borderRadius: "4px",
158+
border: `2px solid ${Colors.primary.main}`,
159+
fontSize: "0.95rem",
160+
minWidth: "200px",
161+
}}
162+
/>
163+
</Box>
164+
165+
{/* Right: Label + Select in one line */}
166+
<Box
167+
sx={{
168+
display: "flex",
169+
alignItems: "center",
170+
gap: 1.5,
171+
}}
172+
>
173+
</Box>
174+
<Typography sx={{ fontWeight: 500, fontSize: "1rem", color: Colors.white, minWidth: "150px", }}>
175+
Dataset per page:
176+
</Typography>
177+
{/* Dataset per page dropdown */}
94178
<FormControl
95179
size="small"
96180
sx={{
97-
minWidth: 150,
181+
minWidth: 160,
98182
backgroundColor: Colors.white,
99183
borderRadius: 1,
100184
boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
101185
"& .MuiInputLabel-root": {
102186
color: Colors.textSecondary,
103187
fontWeight: 500,
188+
zIndex: 1, // ✅ ensures label is above the select box
104189
},
105190
"& .MuiOutlinedInput-root": {
106-
transition: "all 0.2s ease-in-out",
107191
"& fieldset": {
108192
borderColor: Colors.primary.main,
109-
borderWidth: 2,
110-
},
111-
"&:hover fieldset": {
112-
borderColor: Colors.primary.dark,
113-
borderWidth: 2,
114193
},
115-
"&.Mui-focused fieldset": {
116-
borderColor: Colors.primary.dark,
117-
borderWidth: 2,
118-
},
119-
},
194+
},
120195
}}
121-
>
122-
<InputLabel>Items per page</InputLabel>
196+
>
123197
<Select
124198
value={pageSize}
125-
label="Items per page"
199+
label="Dataset per page"
126200
onChange={handlePageSizeChange}
127201
sx={{
128-
color: Colors.textPrimary,
129202
fontWeight: 500,
130203
"& .MuiSelect-icon": {
131204
color: Colors.primary.main,
132-
transition: "transform 0.2s ease-in-out",
133-
},
134-
"&:hover .MuiSelect-icon": {
135-
transform: "rotate(180deg)",
136-
color: Colors.primary.dark,
137205
},
138206
}}
139207
>
140-
<MenuItem value={10} sx={{ fontWeight: 500 }}>
141-
10 items
142-
</MenuItem>
143-
<MenuItem value={25} sx={{ fontWeight: 500 }}>
144-
25 items
145-
</MenuItem>
146-
<MenuItem value={50} sx={{ fontWeight: 500 }}>
147-
50 items
148-
</MenuItem>
149-
<MenuItem value={100} sx={{ fontWeight: 500 }}>
150-
100 items
151-
</MenuItem>
208+
<MenuItem value={10}>10</MenuItem>
209+
<MenuItem value={25}>25</MenuItem>
210+
<MenuItem value={50}>50</MenuItem>
211+
<MenuItem value={100}>100</MenuItem>
152212
</Select>
153213
</FormControl>
214+
215+
{/* Pagination buttons */}
216+
{!loading && (
217+
<Box sx={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1 }}>
218+
<Button
219+
onClick={() => handlePrevNextPage("prev")}
220+
disabled={currentPage === 1}
221+
sx={{
222+
minWidth: "36px",
223+
backgroundColor: Colors.primary.main,
224+
color: "white",
225+
"&:disabled": { backgroundColor: "#ccc" },
226+
}}
227+
>
228+
&lt;
229+
</Button>
230+
231+
{getVisiblePageNumbers().map((item, idx) =>
232+
item === "..." ? (
233+
<Typography
234+
key={idx}
235+
sx={{ px: 1.5, fontSize: "1rem", color: Colors.textSecondary }}
236+
>
237+
...
238+
</Typography>
239+
) : (
240+
<Button
241+
key={item}
242+
variant={item === currentPage ? "contained" : "outlined"}
243+
onClick={() => handlePageChange(Number(item))}
244+
sx={{
245+
minWidth: "36px",
246+
padding: "4px 8px",
247+
fontWeight: item === currentPage ? "bold" : "normal",
248+
backgroundColor: item === currentPage ? Colors.primary.main : "white",
249+
color: item === currentPage ? "white" : Colors.primary.main,
250+
borderColor: Colors.primary.main,
251+
}}
252+
>
253+
{item}
254+
</Button>
255+
)
256+
)}
257+
258+
<Button
259+
onClick={() => handlePrevNextPage("next")}
260+
disabled={currentPage === totalPages}
261+
sx={{
262+
minWidth: "36px",
263+
backgroundColor: Colors.primary.main,
264+
color: "white",
265+
"&:disabled": { backgroundColor: "#ccc" },
266+
}}
267+
>
268+
&gt;
269+
</Button>
270+
</Box>
271+
)}
154272
</Box>
155273
</Box>
156274

@@ -178,21 +296,36 @@ const DatasetPage: React.FC = () => {
178296

179297
{!loading && !error && data.length > 0 && (
180298
<Grid container spacing={3}>
181-
{data.map((doc: any) => (
299+
{filteredData.map((doc: any, index: number ) => {
300+
const datasetIndex = (currentPage - 1) * pageSize + index + 1;
301+
return (
182302
<Grid item xs={12} sm={6} key={doc.id}>
183303
<Card
184304
sx={{
305+
position: "relative", // ✅ allows absolute positioning of the number
185306
backgroundColor: Colors.white,
186307
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
187308
height: "100%",
188309
display: "flex",
189310
flexDirection: "column",
190311
}}
191312
>
313+
{/* Dataset index number on top-right corner */}
314+
<Box
315+
sx={{
316+
position: "absolute",
317+
top: 8,
318+
right: 12,
319+
fontSize: "2rem",
320+
fontWeight: "bold",
321+
color: "rgba(0, 0, 0, 0.36)",
322+
}}
323+
>
324+
{datasetIndex}
325+
</Box>
192326
<CardContent sx={{ flex: 1 }}>
193327
<Button
194328
onClick={() =>
195-
// navigate(`${RoutesEnum.DATABASES}/${dbName}/${doc.id}`)
196329
navigate(`${RoutesEnum.DATABASES}/${encodeURIComponent(dbName ?? '')}/${encodeURIComponent(doc.id ?? '')}`)
197330
}
198331
sx={{
@@ -282,7 +415,8 @@ const DatasetPage: React.FC = () => {
282415
</CardContent>
283416
</Card>
284417
</Grid>
285-
))}
418+
)
419+
})}
286420
</Grid>
287421
)}
288422

@@ -296,27 +430,6 @@ const DatasetPage: React.FC = () => {
296430
No database information available.
297431
</Typography>
298432
)}
299-
300-
{!loading && (
301-
<Box sx={{ textAlign: "center", mt: 3 }}>
302-
<Button
303-
variant="contained"
304-
onClick={loadMoreData}
305-
// disabled={data.length >= limit}
306-
disabled={!hasMore}
307-
sx={{
308-
backgroundColor: Colors.primary.main,
309-
color: Colors.white,
310-
"&:hover": {
311-
backgroundColor: Colors.primary.dark,
312-
},
313-
}}
314-
>
315-
Load More ({data.length} of {limit} items)
316-
{data.length >= limit && " - Limit Reached"}
317-
</Button>
318-
</Box>
319-
)}
320433
</Box>
321434
);
322435
};

src/redux/neurojson/neurojson.slice.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const neurojsonSlice = createSlice({
5050
// Check if we received fewer items than the limit, indicating we've reached the end
5151
console.log(action.payload.total_rows);
5252
state.limit = action.payload.total_rows;
53-
const reachedEnd = action.payload.rows.length < state.limit;
53+
const reachedEnd = action.payload.offset + action.payload.rows.length >= action.payload.total_rows;
5454

5555
// Filter out duplicates while preserving order
5656
const uniqueEntries = action.payload.rows.filter(
@@ -62,8 +62,8 @@ const neurojsonSlice = createSlice({
6262

6363
if (uniqueEntries.length > 0) {
6464
// Append new unique entries to existing data
65-
state.data = [...state.data, ...uniqueEntries];
66-
state.offset += uniqueEntries.length;
65+
state.data = uniqueEntries; // ✅ Replace instead of append
66+
state.offset = action.payload.offset; // ✅ Track current offset
6767
// Only set hasMore to true if we haven't reached the end
6868
state.hasMore = !reachedEnd;
6969
} else {

0 commit comments

Comments
 (0)