Skip to content

Commit f23b267

Browse files
committed
feat(admins): enhance admin retrieval with pagination and statistics support
1 parent ff55029 commit f23b267

File tree

9 files changed

+133
-57
lines changed

9 files changed

+133
-57
lines changed

app/db/crud/admin.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ async def get_admins(
175175
limit: int | None = None,
176176
username: str | None = None,
177177
sort: list[AdminsSortingOptions] | None = None,
178-
) -> list[Admin]:
178+
return_with_count: bool = False,
179+
) -> list[Admin] | tuple[list[Admin], int, int, int]:
179180
"""
180181
Retrieves a list of admins with optional filters and pagination.
181182
@@ -185,14 +186,44 @@ async def get_admins(
185186
limit (Optional[int]): The maximum number of records to return.
186187
username (Optional[str]): The username to filter by.
187188
sort (Optional[list[AdminsSortingOptions]]): Sort options for ordering results.
189+
return_with_count (bool): If True, returns tuple with (admins, total, active, disabled).
188190
189191
Returns:
190-
List[Admin]: A list of admin objects.
192+
List[Admin] | tuple[list[Admin], int, int, int]: A list of admin objects or tuple with counts.
191193
"""
192-
query = select(Admin)
194+
base_query = select(Admin)
193195
if username:
194-
query = query.where(Admin.username.ilike(f"%{username}%"))
195-
196+
base_query = base_query.where(Admin.username.ilike(f"%{username}%"))
197+
198+
total = None
199+
active = None
200+
disabled = None
201+
202+
if return_with_count:
203+
# Get total count
204+
count_stmt = select(func.count(Admin.id))
205+
if username:
206+
count_stmt = count_stmt.where(Admin.username.ilike(f"%{username}%"))
207+
result = await db.execute(count_stmt)
208+
total = result.scalar()
209+
210+
# Get active count (not disabled)
211+
active_stmt = select(func.count(Admin.id))
212+
if username:
213+
active_stmt = active_stmt.where(Admin.username.ilike(f"%{username}%"))
214+
active_stmt = active_stmt.where(Admin.is_disabled == False)
215+
result = await db.execute(active_stmt)
216+
active = result.scalar()
217+
218+
# Get disabled count
219+
disabled_stmt = select(func.count(Admin.id))
220+
if username:
221+
disabled_stmt = disabled_stmt.where(Admin.username.ilike(f"%{username}%"))
222+
disabled_stmt = disabled_stmt.where(Admin.is_disabled == True)
223+
result = await db.execute(disabled_stmt)
224+
disabled = result.scalar()
225+
226+
query = base_query
196227
if sort:
197228
query = query.order_by(*sort)
198229

@@ -206,6 +237,8 @@ async def get_admins(
206237
for admin in admins:
207238
await load_admin_attrs(admin)
208239

240+
if return_with_count:
241+
return admins, total, active, disabled
209242
return admins
210243

211244

app/models/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,11 @@ class AdminValidationResult(BaseModel):
111111
username: str
112112
is_sudo: bool
113113
is_disabled: bool
114+
115+
116+
class AdminsResponse(BaseModel):
117+
"""Response model for admins list with pagination and statistics."""
118+
admins: list[AdminDetails]
119+
total: int
120+
active: int
121+
disabled: int

app/operation/admin.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from app.db.crud.bulk import activate_all_disabled_users, disable_all_active_users
1717
from app.db.crud.user import get_users, remove_users
1818
from app.db.models import Admin as DBAdmin
19-
from app.models.admin import AdminCreate, AdminDetails, AdminModify
19+
from app.models.admin import AdminCreate, AdminDetails, AdminModify, AdminsResponse
2020
from app.node import node_manager
2121
from app.operation import BaseOperation, OperatorType
2222
from app.operation.user import UserOperation
@@ -83,7 +83,7 @@ async def get_admins(
8383
offset: int | None = None,
8484
limit: int | None = None,
8585
sort: str | None = None,
86-
) -> list[DBAdmin]:
86+
) -> AdminsResponse:
8787
sort_list = []
8888
if sort is not None:
8989
opts = sort.strip(",").split(",")
@@ -94,7 +94,16 @@ async def get_admins(
9494
except KeyError:
9595
await self.raise_error(message=f'"{opt}" is not a valid sort option', code=400)
9696

97-
return await get_admins(db, offset, limit, username, sort_list if sort_list else None)
97+
admins, total, active, disabled = await get_admins(
98+
db, offset, limit, username, sort_list if sort_list else None, return_with_count=True
99+
)
100+
101+
return AdminsResponse(
102+
admins=[AdminDetails.model_validate(admin) for admin in admins],
103+
total=total,
104+
active=active,
105+
disabled=disabled,
106+
)
98107

99108
async def get_admins_count(self, db: AsyncSession) -> int:
100109
return await get_admins_count(db)

app/routers/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from app import notification
77
from app.db import AsyncSession, get_db
8-
from app.models.admin import AdminCreate, AdminDetails, AdminModify, Token
8+
from app.models.admin import AdminCreate, AdminDetails, AdminModify, Token, AdminsResponse
99
from app.operation import OperatorType
1010
from app.operation.admin import AdminOperation
1111
from app.utils import responses
@@ -119,7 +119,7 @@ def get_current_admin(admin: AdminDetails = Depends(get_current)):
119119
return admin
120120

121121

122-
@router.get("s", response_model=list[AdminDetails])
122+
@router.get("s", response_model=AdminsResponse)
123123
async def get_admins(
124124
username: str | None = None,
125125
offset: int | None = None,

dashboard/src/components/admins/admin-statistics.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ import { User, UserCheck, UserX } from 'lucide-react'
88
import React, { useEffect, useState } from 'react'
99

1010
interface AdminsStatisticsProps {
11-
data: AdminDetails[]
11+
counts: { total: number; active: number; disabled: number } | null
1212
}
1313

14-
export default function AdminStatisticsSection({ data }: AdminsStatisticsProps) {
14+
export default function AdminStatisticsSection({ counts }: AdminsStatisticsProps) {
1515
const { t } = useTranslation()
1616
const dir = useDirDetection()
1717
const [prevStats, setPrevStats] = useState<{ total: number; active: number; disabled: number } | null>(null)
1818
const [isIncreased, setIsIncreased] = useState<Record<string, boolean>>({})
1919

20-
const total = data.length
21-
const disabled = data.filter(a => a.is_disabled).length
22-
const active = total - disabled
20+
const total = counts?.total || 0
21+
const active = counts?.active || 0
22+
const disabled = counts?.disabled || 0
2323

2424
const currentStats = { total, active, disabled }
2525

@@ -33,7 +33,7 @@ export default function AdminStatisticsSection({ data }: AdminsStatisticsProps)
3333
}
3434
setPrevStats(currentStats)
3535
// eslint-disable-next-line react-hooks/exhaustive-deps
36-
}, [data])
36+
}, [counts])
3737

3838
const stats = [
3939
{

dashboard/src/components/admins/admins-table.tsx

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useGetAdmins, useRemoveAllUsers } from '@/service/api'
44
import { DataTable } from './data-table'
55
import { setupColumns } from './columns'
66
import { Filters } from './filters'
7-
import { useEffect, useState } from 'react'
7+
import { useEffect, useState, useRef } from 'react'
88
import { PaginationControls } from './filters.tsx'
99
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
1010
import { cn } from '@/lib/utils'
@@ -22,11 +22,11 @@ interface AdminFilters {
2222
}
2323

2424
interface AdminsTableProps {
25-
data: AdminDetails[]
2625
onEdit: (admin: AdminDetails) => void
2726
onDelete: (admin: AdminDetails) => void
2827
onToggleStatus: (admin: AdminDetails, checked: boolean) => void
2928
onResetUsage: (adminUsername: string) => void
29+
onTotalAdminsChange?: (counts: { total: number; active: number; disabled: number } | null) => void
3030
}
3131

3232
const DeleteAlertDialog = ({ admin, isOpen, onClose, onConfirm }: { admin: AdminDetails; isOpen: boolean; onClose: () => void; onConfirm: () => void }) => {
@@ -127,11 +127,13 @@ const RemoveAllUsersConfirmationDialog = ({ adminUsername, isOpen, onClose, onCo
127127
)
128128
}
129129

130-
export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetUsage }: AdminsTableProps) {
130+
export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetUsage, onTotalAdminsChange }: AdminsTableProps) {
131131
const { t } = useTranslation()
132132
const [currentPage, setCurrentPage] = useState(0)
133133
const [itemsPerPage, setItemsPerPage] = useState(getAdminsPerPageLimitSize())
134134
const [isChangingPage, setIsChangingPage] = useState(false)
135+
const isFirstLoadRef = useRef(true)
136+
const isAutoRefreshingRef = useRef(false)
135137
const [filters, setFilters] = useState<AdminFilters>({
136138
limit: itemsPerPage,
137139
offset: 0,
@@ -145,8 +147,30 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
145147
const [adminToReset, setAdminToReset] = useState<string | null>(null)
146148
const [adminToRemoveAllUsers, setAdminToRemoveAllUsers] = useState<string | null>(null)
147149

148-
const { data: totalAdmins } = useGetAdmins()
149-
const { data: adminsData, refetch, isLoading, isFetching } = useGetAdmins(filters)
150+
const { data: adminsResponse, isLoading, isFetching } = useGetAdmins(filters, {
151+
query: {
152+
staleTime: 0,
153+
gcTime: 0,
154+
retry: 1,
155+
},
156+
})
157+
158+
const adminsData = adminsResponse?.admins || []
159+
160+
// Expose counts to parent component for statistics
161+
useEffect(() => {
162+
if (onTotalAdminsChange) {
163+
if (adminsResponse) {
164+
onTotalAdminsChange({
165+
total: adminsResponse.total,
166+
active: adminsResponse.active,
167+
disabled: adminsResponse.disabled,
168+
})
169+
} else {
170+
onTotalAdminsChange(null)
171+
}
172+
}
173+
}, [adminsResponse, onTotalAdminsChange])
150174
const removeAllUsersMutation = useRemoveAllUsers()
151175

152176
// Update filters when pagination changes
@@ -158,6 +182,18 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
158182
}))
159183
}, [currentPage, itemsPerPage])
160184

185+
useEffect(() => {
186+
if (adminsData && isFirstLoadRef.current) {
187+
isFirstLoadRef.current = false
188+
}
189+
}, [adminsData])
190+
191+
useEffect(() => {
192+
if (!isFetching && isAutoRefreshingRef.current) {
193+
isAutoRefreshingRef.current = false
194+
}
195+
}, [isFetching])
196+
161197
// When filters change (e.g., search), reset page if needed
162198
const handleFilterChange = (newFilters: Partial<AdminFilters>) => {
163199
setFilters(prev => {
@@ -247,40 +283,20 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
247283
}
248284
}
249285

250-
const handlePageChange = async (newPage: number) => {
286+
const handlePageChange = (newPage: number) => {
251287
if (newPage === currentPage || isChangingPage) return
252288

253289
setIsChangingPage(true)
254290
setCurrentPage(newPage)
255-
256-
try {
257-
// Immediate refetch without delay
258-
await refetch()
259-
} finally {
260-
// Minimal delay for instant response
261-
setTimeout(() => {
262-
setIsChangingPage(false)
263-
}, 50)
264-
}
291+
setIsChangingPage(false)
265292
}
266293

267-
const handleItemsPerPageChange = async (value: number) => {
294+
const handleItemsPerPageChange = (value: number) => {
268295
setIsChangingPage(true)
269296
setItemsPerPage(value)
270297
setCurrentPage(0) // Reset to first page when items per page changes
271-
272-
// Save to localStorage
273298
setAdminsPerPageLimitSize(value.toString())
274-
275-
try {
276-
// Immediate refetch without delay
277-
await refetch()
278-
} finally {
279-
// Minimal delay for instant response
280-
setTimeout(() => {
281-
setIsChangingPage(false)
282-
}, 50)
283-
}
299+
setIsChangingPage(false)
284300
}
285301

286302
const handleSort = (column: string) => {
@@ -312,6 +328,9 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
312328
onRemoveAllUsers: handleRemoveAllUsersClick,
313329
})
314330

331+
const showLoadingSpinner = isLoading && isFirstLoadRef.current
332+
const isPageLoading = isChangingPage
333+
315334
return (
316335
<div>
317336
<Filters filters={filters} onFilterChange={handleFilterChange} />
@@ -324,15 +343,15 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
324343
onResetUsage={handleResetUsersUsageClick}
325344
onRemoveAllUsers={handleRemoveAllUsersClick}
326345
setStatusToggleDialogOpen={setStatusToggleDialogOpen}
327-
isLoading={isLoading}
328-
isFetching={isFetching}
346+
isLoading={showLoadingSpinner}
347+
isFetching={isFetching && !isFirstLoadRef.current && !isAutoRefreshingRef.current}
329348
/>
330349
<PaginationControls
331350
currentPage={currentPage}
332-
totalPages={Math.ceil((totalAdmins?.length || 0) / itemsPerPage)}
351+
totalPages={Math.ceil((adminsResponse?.total || 0) / itemsPerPage)}
333352
itemsPerPage={itemsPerPage}
334-
totalItems={adminsData?.length || 0}
335-
isLoading={isLoading || isFetching}
353+
totalItems={adminsResponse?.total || 0}
354+
isLoading={isPageLoading}
336355
onPageChange={handlePageChange}
337356
onItemsPerPageChange={handleItemsPerPageChange}
338357
/>

dashboard/src/components/admins/filters.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ interface PaginationControlsProps {
9898
itemsPerPage: number
9999
totalItems: number
100100
isLoading: boolean
101-
onPageChange: (page: number) => Promise<void>
102-
onItemsPerPageChange: (value: number) => Promise<void>
101+
onPageChange: (page: number) => void
102+
onItemsPerPageChange: (value: number) => void
103103
}
104104

105105
// Update PaginationControls to use props

dashboard/src/pages/_dashboard.admins.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Separator } from '@/components/ui/separator'
77
import { toast } from 'sonner'
88
import AdminsTable from '@/components/admins/admins-table'
99
import AdminModal, { adminFormSchema, AdminFormValues } from '@/components/dialogs/admin-modal'
10-
import { useActivateAllDisabledUsers, useDisableAllActiveUsers, useGetAdmins, useModifyAdmin, useRemoveAdmin, useResetAdminUsage } from '@/service/api'
10+
import { useActivateAllDisabledUsers, useDisableAllActiveUsers, useModifyAdmin, useRemoveAdmin, useResetAdminUsage } from '@/service/api'
1111
import type { AdminDetails } from '@/service/api'
1212
import AdminsStatistics from '@/components/admins/admin-statistics'
1313
import { zodResolver } from '@hookform/resolvers/zod'
@@ -40,12 +40,12 @@ export default function AdminsPage() {
4040
const { t } = useTranslation()
4141
const [editingAdmin, setEditingAdmin] = useState<Partial<AdminDetails> | null>(null)
4242
const [isDialogOpen, setIsDialogOpen] = useState(false)
43+
const [adminCounts, setAdminCounts] = useState<{ total: number; active: number; disabled: number } | null>(null)
4344
const form = useForm<AdminFormValues>({
4445
resolver: zodResolver(adminFormSchema),
4546
defaultValues: initialDefaultValues,
4647
})
4748

48-
const { data: admins = [] } = useGetAdmins({})
4949
const removeAdminMutation = useRemoveAdmin()
5050
const modifyAdminMutation = useModifyAdmin()
5151
const modifyDisableAllAdminUsers = useDisableAllActiveUsers()
@@ -197,11 +197,11 @@ export default function AdminsPage() {
197197

198198
<div className="w-full px-4 pt-2">
199199
<div className="mb-6 transform-gpu animate-slide-up" style={{ animationDuration: '500ms', animationDelay: '100ms', animationFillMode: 'both' }}>
200-
<AdminsStatistics data={admins} />
200+
<AdminsStatistics counts={adminCounts} />
201201
</div>
202202

203203
<div className="transform-gpu animate-slide-up" style={{ animationDuration: '500ms', animationDelay: '250ms', animationFillMode: 'both' }}>
204-
<AdminsTable data={admins} onEdit={handleEdit} onDelete={handleDelete} onToggleStatus={handleToggleStatus} onResetUsage={resetUsage} />
204+
<AdminsTable onEdit={handleEdit} onDelete={handleDelete} onToggleStatus={handleToggleStatus} onResetUsage={resetUsage} onTotalAdminsChange={setAdminCounts} />
205205
</div>
206206

207207
<AdminModal

0 commit comments

Comments
 (0)