Skip to content

Commit 627f7f7

Browse files
committed
feat(admin): add delete all users in ui
1 parent 07fea94 commit 627f7f7

File tree

8 files changed

+156
-6
lines changed

8 files changed

+156
-6
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,9 @@
575575
"resetUsersUsage": "Admin Reset Users Usage",
576576
"resetUsageSuccess": "Admin «{{name}}» user usage has been reset successfully",
577577
"resetUsageFailed": "Failed to reset admin «{{name}}» user usage",
578+
"removeAllUsers": "Remove All Users",
579+
"removeAllUsersSuccess": "All users under admin «{{name}}» have been removed successfully",
580+
"removeAllUsersFailed": "Failed to remove all users under admin «{{name}}»",
578581
"reset": "Reset usage",
579582
"discordId": "Discord ID",
580583
"lifetime.used.traffic": "Total",
@@ -598,6 +601,7 @@
598601
"comingSoon": "Coming Soon"
599602
},
600603
"resetUsersUsage.prompt": "Are you sure you want to reset the <b>{{name}}</b> admin users usage?",
604+
"removeAllUsers.prompt": "Are you sure you want to remove all users under admin <b>{{name}}</b>? This action cannot be undone.",
601605
"admin.disable": "Disabled Admin",
602606
"admin.enable": "Enabled Admin",
603607
"deleteAdmin.prompt": "Are you sure you want to remove the <b>{{name}}</b> admin?",

dashboard/public/statics/locales/fa.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,9 @@
464464
"resetUsersUsage": "ریست مصرف کاربران توسط ادمین",
465465
"resetUsageSuccess": "مصرف کاربر «{{name}}» با موفقیت توسط ادمین ریست شد",
466466
"resetUsageFailed": "ریست مصرف کاربر «{{name}}» توسط ادمین ناموفق بود",
467+
"removeAllUsers": "حذف همه کاربران",
468+
"removeAllUsersSuccess": "همه کاربران تحت مدیریت ادمین «{{name}}» با موفقیت حذف شدند",
469+
"removeAllUsersFailed": "حذف همه کاربران تحت مدیریت ادمین «{{name}}» ناموفق بود",
467470
"reset": "ریست مصرف",
468471
"discordId": "شناسه دیسکورد",
469472
"lifetime.used.traffic": "مجموع کل",
@@ -487,6 +490,7 @@
487490
"comingSoon": "به زودی"
488491
},
489492
"resetUsersUsage.prompt": "آیا مطمئن هستید که می‌خواهید مصرف کاربر ادمین <b>{{name}}</b> را ریست کنید؟",
493+
"removeAllUsers.prompt": "آیا مطمئن هستید که می‌خواهید همه کاربران تحت مدیریت ادمین <b>{{name}}</b> را حذف کنید؟ این عمل قابل بازگشت نیست.",
490494
"admin.disable": "غیر فعالسازی مدیر",
491495
"admin.enable": "فعالسازی مدیر",
492496
"deleteAdmin.prompt": "آیا مطمئن هستید که می‌خواهید مدیر <b>{{name}}</b> را حذف کنید؟",

dashboard/public/statics/locales/ru.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,9 @@
561561
"resetUsersUsage": "Админ сбросить использование пользователей",
562562
"resetUsageSuccess": "Использование пользователя «{{name}}» успешно сброшено администратором",
563563
"resetUsageFailed": "Не удалось сбросить использование пользователя «{{name}}» администратором",
564+
"removeAllUsers": "Удалить всех пользователей",
565+
"removeAllUsersSuccess": "Все пользователи под управлением администратора «{{name}}» успешно удалены",
566+
"removeAllUsersFailed": "Не удалось удалить всех пользователей под управлением администратора «{{name}}»",
564567
"reset": "Сброс использования",
565568
"discordId": "Discord ID",
566569
"lifetime.used.traffic": "Всего",
@@ -584,6 +587,7 @@
584587
"comingSoon": "Скоро"
585588
},
586589
"resetUsersUsage.prompt": "Вы уверены, что хотите сбросить использование пользователя-админа <b>{{name}}</b>?",
590+
"removeAllUsers.prompt": "Вы уверены, что хотите удалить всех пользователей под управлением администратора <b>{{name}}</b>? Это действие нельзя отменить.",
587591
"admin.disable": "Администратор отключён",
588592
"admin.enable": "Администратор включён",
589593
"deleteAdmin.prompt": "Вы уверены, что хотите удалить администратора <b>{{name}}</b>?",

dashboard/public/statics/locales/zh.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,9 @@
574574
"resetUsersUsage": "管理员重置用户使用情况",
575575
"resetUsageSuccess": "管理员已成功重置用户「{{name}}」的使用情况",
576576
"resetUsageFailed": "管理员重置用户「{{name}}」的使用情况失败",
577+
"removeAllUsers": "删除所有用户",
578+
"removeAllUsersSuccess": "管理员「{{name}}」下的所有用户已成功删除",
579+
"removeAllUsersFailed": "删除管理员「{{name}}」下的所有用户失败",
577580
"reset": "重置使用情况",
578581
"discordId": "Discord ID",
579582
"lifetime.used.traffic": "总计",
@@ -597,6 +600,7 @@
597600
"comingSoon": "即将推出"
598601
},
599602
"resetUsersUsage.prompt": "你确定要重置管理员用户<b>{{name}}</b>的使用情况吗?",
603+
"removeAllUsers.prompt": "你确定要删除管理员<b>{{name}}</b>下的所有用户吗?此操作无法撤销。",
600604
"admin.disable": "管理员已禁用",
601605
"admin.enable": "管理员已启用",
602606
"deleteAdmin.prompt": "您确定要删除管理员<b>{{name}}</b>吗?",

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useTranslation } from 'react-i18next'
22
import type { AdminDetails } from '@/service/api'
3-
import { useGetAdmins } from '@/service/api'
3+
import { useGetAdmins, useRemoveAllUsers } from '@/service/api'
44
import { DataTable } from './data-table'
55
import { setupColumns } from './columns'
66
import { Filters } from './filters'
@@ -11,6 +11,8 @@ import { cn } from '@/lib/utils'
1111
import useDirDetection from '@/hooks/use-dir-detection'
1212
import { Checkbox } from '@/components/ui/checkbox.tsx'
1313
import { getAdminsPerPageLimitSize, setAdminsPerPageLimitSize } from '@/utils/userPreferenceStorage'
14+
import { toast } from 'sonner'
15+
import { queryClient } from '@/utils/query-client'
1416

1517
interface AdminFilters {
1618
sort?: string
@@ -103,6 +105,28 @@ const ResetUsersUsageConfirmationDialog = ({ adminUsername, isOpen, onClose, onC
103105
)
104106
}
105107

108+
const RemoveAllUsersConfirmationDialog = ({ adminUsername, isOpen, onClose, onConfirm }: { adminUsername: string; isOpen: boolean; onClose: () => void; onConfirm: () => void }) => {
109+
const { t } = useTranslation()
110+
const dir = useDirDetection()
111+
112+
return (
113+
<AlertDialog open={isOpen} onOpenChange={onClose}>
114+
<AlertDialogContent>
115+
<AlertDialogHeader className={cn(dir === 'rtl' && 'sm:text-right')}>
116+
<AlertDialogTitle>{t('admins.removeAllUsers')}</AlertDialogTitle>
117+
<AlertDialogDescription className="flex items-center gap-2">
118+
<span dir={dir} dangerouslySetInnerHTML={{ __html: t('removeAllUsers.prompt', { name: adminUsername }) }} />
119+
</AlertDialogDescription>
120+
</AlertDialogHeader>
121+
<AlertDialogFooter>
122+
<AlertDialogCancel onClick={onClose}>{t('cancel')}</AlertDialogCancel>
123+
<AlertDialogAction variant="destructive" onClick={onConfirm}>{t('confirm')}</AlertDialogAction>
124+
</AlertDialogFooter>
125+
</AlertDialogContent>
126+
</AlertDialog>
127+
)
128+
}
129+
106130
export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetUsage }: AdminsTableProps) {
107131
const { t } = useTranslation()
108132
const [currentPage, setCurrentPage] = useState(0)
@@ -115,12 +139,15 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
115139
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
116140
const [statusToggleDialogOpen, setStatusToggleDialogOpen] = useState(false)
117141
const [resetUsersUsageDialogOpen, setResetUsersUsageDialogOpen] = useState(false)
142+
const [removeAllUsersDialogOpen, setRemoveAllUsersDialogOpen] = useState(false)
118143
const [adminToDelete, setAdminToDelete] = useState<AdminDetails | null>(null)
119144
const [adminToToggleStatus, setAdminToToggleStatus] = useState<AdminDetails | null>(null)
120145
const [adminToReset, setAdminToReset] = useState<string | null>(null)
146+
const [adminToRemoveAllUsers, setAdminToRemoveAllUsers] = useState<string | null>(null)
121147

122148
const { data: totalAdmins } = useGetAdmins()
123149
const { data: adminsData, refetch, isLoading, isFetching } = useGetAdmins(filters)
150+
const removeAllUsersMutation = useRemoveAllUsers()
124151

125152
// Update filters when pagination changes
126153
useEffect(() => {
@@ -173,6 +200,37 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
173200
setAdminToReset(null)
174201
}
175202
}
203+
204+
const handleRemoveAllUsersClick = (adminUsername: string) => {
205+
setAdminToRemoveAllUsers(adminUsername)
206+
setRemoveAllUsersDialogOpen(true)
207+
}
208+
209+
const handleConfirmRemoveAllUsers = async () => {
210+
if (adminToRemoveAllUsers) {
211+
try {
212+
await removeAllUsersMutation.mutateAsync({
213+
username: adminToRemoveAllUsers,
214+
})
215+
toast.success(t('success', { defaultValue: 'Success' }), {
216+
description: t('admins.removeAllUsersSuccess', {
217+
name: adminToRemoveAllUsers,
218+
defaultValue: `All users under admin "{name}" have been removed successfully`,
219+
}),
220+
})
221+
queryClient.invalidateQueries({ queryKey: ['/api/admins'] })
222+
setRemoveAllUsersDialogOpen(false)
223+
setAdminToRemoveAllUsers(null)
224+
} catch (error) {
225+
toast.error(t('error', { defaultValue: 'Error' }), {
226+
description: t('admins.removeAllUsersFailed', {
227+
name: adminToRemoveAllUsers,
228+
defaultValue: `Failed to remove all users under admin "{name}"`,
229+
}),
230+
})
231+
}
232+
}
233+
}
176234
const handleConfirmDelete = async () => {
177235
if (adminToDelete) {
178236
onDelete(adminToDelete)
@@ -251,6 +309,7 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
251309
onDelete: handleDeleteClick,
252310
toggleStatus: handleStatusToggleClick,
253311
onResetUsage: handleResetUsersUsageClick,
312+
onRemoveAllUsers: handleRemoveAllUsersClick,
254313
})
255314

256315
return (
@@ -263,6 +322,7 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
263322
onDelete={handleDeleteClick}
264323
onToggleStatus={handleStatusToggleClick}
265324
onResetUsage={handleResetUsersUsageClick}
325+
onRemoveAllUsers={handleRemoveAllUsersClick}
266326
setStatusToggleDialogOpen={setStatusToggleDialogOpen}
267327
isLoading={isLoading}
268328
isFetching={isFetching}
@@ -288,6 +348,14 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
288348
onClose={() => setResetUsersUsageDialogOpen(false)}
289349
/>
290350
)}
351+
{adminToRemoveAllUsers && (
352+
<RemoveAllUsersConfirmationDialog
353+
adminUsername={adminToRemoveAllUsers}
354+
onConfirm={handleConfirmRemoveAllUsers}
355+
isOpen={removeAllUsersDialogOpen}
356+
onClose={() => setRemoveAllUsersDialogOpen(false)}
357+
/>
358+
)}
291359
</div>
292360
)
293361
}

dashboard/src/components/admins/columns.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AdminDetails } from '@/service/api'
22
import { ColumnDef } from '@tanstack/react-table'
3-
import { ChartPie, ChevronDown, MoreVertical, Pen, Power, PowerOff, RefreshCw, Trash2, User } from 'lucide-react'
3+
import { ChartPie, ChevronDown, MoreVertical, Pen, Power, PowerOff, RefreshCw, Trash2, User, UserX } from 'lucide-react'
44
import { Button } from '@/components/ui/button'
55
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu.tsx'
66
import { formatBytes } from '@/utils/formatByte.ts'
@@ -14,6 +14,7 @@ interface ColumnSetupProps {
1414
onDelete: (admin: AdminDetails) => void
1515
toggleStatus: (admin: AdminDetails) => void
1616
onResetUsage: (adminUsername: string) => void
17+
onRemoveAllUsers: (adminUsername: string) => void
1718
}
1819

1920
const createSortButton = (
@@ -42,7 +43,7 @@ const createSortButton = (
4243
)
4344
}
4445

45-
export const setupColumns = ({ t, handleSort, filters, onEdit, onDelete, toggleStatus, onResetUsage }: ColumnSetupProps): ColumnDef<AdminDetails>[] => [
46+
export const setupColumns = ({ t, handleSort, filters, onEdit, onDelete, toggleStatus, onResetUsage, onRemoveAllUsers }: ColumnSetupProps): ColumnDef<AdminDetails>[] => [
4647
{
4748
accessorKey: 'username',
4849
header: () => createSortButton('username', 'username', t, handleSort, filters),
@@ -157,6 +158,17 @@ export const setupColumns = ({ t, handleSort, filters, onEdit, onDelete, toggleS
157158
<RefreshCw className="mr-2 h-4 w-4" />
158159
{t('admins.reset')}
159160
</DropdownMenuItem>
161+
<DropdownMenuItem
162+
className="text-destructive"
163+
onSelect={e => {
164+
e.preventDefault()
165+
e.stopPropagation()
166+
onRemoveAllUsers(row.original.username)
167+
}}
168+
>
169+
<UserX className="mr-2 h-4 w-4" />
170+
{t('admins.removeAllUsers')}
171+
</DropdownMenuItem>
160172
<DropdownMenuItem
161173
className="text-destructive"
162174
onSelect={e => {

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
33
import { cn } from '@/lib/utils'
44
import useDirDetection from '@/hooks/use-dir-detection'
55
import React, { useState, useMemo, memo, useCallback } from 'react'
6-
import { ChevronDown, Edit2, Power, PowerOff, RefreshCw, Trash2, User, UserRound, LoaderCircle } from 'lucide-react'
6+
import { ChevronDown, Edit2, Power, PowerOff, RefreshCw, Trash2, User, UserRound, UserX, LoaderCircle } from 'lucide-react'
77
import { Button } from '@/components/ui/button'
88
import { AdminDetails } from '@/service/api'
99
import { useTranslation } from 'react-i18next'
@@ -19,6 +19,7 @@ interface DataTableProps<TData extends AdminDetails> {
1919
onToggleStatus: (admin: AdminDetails) => void
2020
setStatusToggleDialogOpen: (isOpen: boolean) => void
2121
onResetUsage: (adminUsername: string) => void
22+
onRemoveAllUsers?: (adminUsername: string) => void
2223
isLoading?: boolean
2324
isFetching?: boolean
2425
}
@@ -30,12 +31,14 @@ const ExpandedRowContent = memo(
3031
onDelete,
3132
onToggleStatus,
3233
onResetUsage,
34+
onRemoveAllUsers,
3335
}: {
3436
row: AdminDetails
3537
onEdit: (admin: AdminDetails) => void
3638
onDelete: (admin: AdminDetails) => void
3739
onToggleStatus: (admin: AdminDetails) => void
3840
onResetUsage: (adminUsername: string) => void
41+
onRemoveAllUsers?: (adminUsername: string) => void
3942
}) => {
4043
const { t } = useTranslation()
4144
const isMobile = useIsMobile()
@@ -71,6 +74,11 @@ const ExpandedRowContent = memo(
7174
<Button variant="ghost" size="icon" onClick={() => onResetUsage(row.username)} title={t('admins.resetUsersUsage')}>
7275
<RefreshCw className="h-4 w-4" />
7376
</Button>
77+
{onRemoveAllUsers && (
78+
<Button variant="ghost" size="icon" onClick={() => onRemoveAllUsers(row.username)} title={t('admins.removeAllUsers')}>
79+
<UserX className="h-4 w-4 text-destructive" />
80+
</Button>
81+
)}
7482
<Button variant="ghost" size="icon" onClick={() => onDelete(row)} title={t('delete')}>
7583
<Trash2 className="h-4 w-4 text-destructive" />
7684
</Button>
@@ -80,7 +88,7 @@ const ExpandedRowContent = memo(
8088
},
8189
)
8290

83-
export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, onDelete, onToggleStatus, onResetUsage, isLoading = false, isFetching = false }: DataTableProps<TData>) {
91+
export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, onDelete, onToggleStatus, onResetUsage, onRemoveAllUsers, isLoading = false, isFetching = false }: DataTableProps<TData>) {
8492
const [expandedRow, setExpandedRow] = useState<string | null>(null)
8593
const { t } = useTranslation()
8694
const table = useReactTable({
@@ -202,7 +210,7 @@ export function DataTable<TData extends AdminDetails>({ columns, data, onEdit, o
202210
{expandedRow === row.id && (
203211
<TableRow className="border-b hover:!bg-inherit md:hidden">
204212
<TableCell colSpan={columns.length} className="p-0 text-sm">
205-
<ExpandedRowContent row={row.original} onEdit={onEdit} onDelete={onDelete} onToggleStatus={onToggleStatus} onResetUsage={onResetUsage} />
213+
<ExpandedRowContent row={row.original} onEdit={onEdit} onDelete={onDelete} onToggleStatus={onToggleStatus} onResetUsage={onResetUsage} onRemoveAllUsers={onRemoveAllUsers} />
206214
</TableCell>
207215
</TableRow>
208216
)}

dashboard/src/service/api/index.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2555,6 +2555,52 @@ export const useActivateAllDisabledUsers = <
25552555
return useMutation(mutationOptions)
25562556
}
25572557

2558+
/**
2559+
* Remove all users under a specific admin.
2560+
* @summary Remove All Users
2561+
*/
2562+
export const removeAllUsers = (username: string) => {
2563+
return orvalFetcher<unknown>({ url: `/api/admin/${username}/users`, method: 'DELETE' })
2564+
}
2565+
2566+
export const getRemoveAllUsersMutationOptions = <
2567+
TData = Awaited<ReturnType<typeof removeAllUsers>>,
2568+
TError = ErrorType<Unauthorized | Forbidden | NotFound | HTTPValidationError>,
2569+
TContext = unknown,
2570+
>(options?: {
2571+
mutation?: UseMutationOptions<TData, TError, { username: string }, TContext>
2572+
}) => {
2573+
const mutationKey = ['removeAllUsers']
2574+
const { mutation: mutationOptions } = options
2575+
? options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey
2576+
? options
2577+
: { ...options, mutation: { ...options.mutation, mutationKey } }
2578+
: { mutation: { mutationKey } }
2579+
2580+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof removeAllUsers>>, { username: string }> = props => {
2581+
const { username } = props ?? {}
2582+
2583+
return removeAllUsers(username)
2584+
}
2585+
2586+
return { mutationFn, ...mutationOptions } as UseMutationOptions<TData, TError, { username: string }, TContext>
2587+
}
2588+
2589+
export type RemoveAllUsersMutationResult = NonNullable<Awaited<ReturnType<typeof removeAllUsers>>>
2590+
2591+
export type RemoveAllUsersMutationError = ErrorType<Unauthorized | Forbidden | NotFound | HTTPValidationError>
2592+
2593+
/**
2594+
* @summary Remove All Users
2595+
*/
2596+
export const useRemoveAllUsers = <TData = Awaited<ReturnType<typeof removeAllUsers>>, TError = ErrorType<Unauthorized | Forbidden | NotFound | HTTPValidationError>, TContext = unknown>(options?: {
2597+
mutation?: UseMutationOptions<TData, TError, { username: string }, TContext>
2598+
}): UseMutationResult<TData, TError, { username: string }, TContext> => {
2599+
const mutationOptions = getRemoveAllUsersMutationOptions(options)
2600+
2601+
return useMutation(mutationOptions)
2602+
}
2603+
25582604
/**
25592605
* Resets usage of admin.
25602606
* @summary Reset Admin Usage

0 commit comments

Comments
 (0)