Skip to content

Commit f389de1

Browse files
feat: add sorting options for users table and update localization files
1 parent 84419ea commit f389de1

File tree

7 files changed

+164
-5
lines changed

7 files changed

+164
-5
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,8 +620,24 @@
620620
"deleteUser.title": "Remove User",
621621
"disabled": "disabled",
622622
"expire": "Expire",
623+
"expireDate": "Expire Date",
623624
"expires": "Expires in {{time}}",
624625
"expired": "Expired {{time}}",
626+
"sortOptions": "Sort Options",
627+
"sort": {
628+
"username": {
629+
"asc": "Username (A-Z)",
630+
"desc": "Username (Z-A)"
631+
},
632+
"expire": {
633+
"oldest": "Expire Date (Oldest First)",
634+
"newest": "Expire Date (Newest First)"
635+
},
636+
"usage": {
637+
"low": "Data Usage (Low to High)",
638+
"high": "Data Usage (High to Low)"
639+
}
640+
},
625641
"header.donation": "Donation",
626642
"header.hostSettings": "Host Settings",
627643
"header.logout": "Log out",

dashboard/public/statics/locales/fa.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,8 +508,24 @@
508508
"deleteUser.title": "حذف کاربر",
509509
"disabled": "غیر فعال",
510510
"expire": "انقضا",
511+
"expireDate": "تاریخ انقضا",
511512
"expired": "{{time}} منقضی شده",
512513
"expires": "پایان در {{time}}",
514+
"sortOptions": "گزینه‌های مرتب‌سازی",
515+
"sort": {
516+
"username": {
517+
"asc": "نام کاربری (الف-ی)",
518+
"desc": "نام کاربری (ی-الف)"
519+
},
520+
"expire": {
521+
"oldest": "تاریخ انقضا (قدیمی‌ترین اول)",
522+
"newest": "تاریخ انقضا (جدیدترین اول)"
523+
},
524+
"usage": {
525+
"low": "مصرف داده (کم به زیاد)",
526+
"high": "مصرف داده (زیاد به کم)"
527+
}
528+
},
513529
"header.donation": "کمک مالی",
514530
"header.hostSettings": "تنظیمات هاست",
515531
"header.logout": "خروج",

dashboard/public/statics/locales/ru.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,8 +593,24 @@
593593
"deleteUser.title": "Удалить пользователя",
594594
"disabled": "disabled",
595595
"expire": "Истекает",
596+
"expireDate": "Дата истечения",
596597
"expired": "Истёк {{time}}",
597598
"expires": "Истекает через {{time}}",
599+
"sortOptions": "Параметры сортировки",
600+
"sort": {
601+
"username": {
602+
"asc": "Имя пользователя (А-Я)",
603+
"desc": "Имя пользователя (Я-А)"
604+
},
605+
"expire": {
606+
"oldest": "Дата истечения (сначала старые)",
607+
"newest": "Дата истечения (сначала новые)"
608+
},
609+
"usage": {
610+
"low": "Использование данных (от малого к большому)",
611+
"high": "Использование данных (от большого к малому)"
612+
}
613+
},
598614
"header.donation": "Пожертвование",
599615
"header.hostSettings": "Настройки хоста",
600616
"header.logout": "Выйти",

dashboard/public/statics/locales/zh.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,8 +619,24 @@
619619
"deleteUser.title": "删除用户",
620620
"disabled": "禁用",
621621
"expire": "过期",
622+
"expireDate": "到期日期",
622623
"expired": "{{time}}已过期",
623624
"expires": "将在 {{time}} 后过期",
625+
"sortOptions": "排序选项",
626+
"sort": {
627+
"username": {
628+
"asc": "用户名 (A-Z)",
629+
"desc": "用户名 (Z-A)"
630+
},
631+
"expire": {
632+
"oldest": "到期日期 (最旧的优先)",
633+
"newest": "到期日期 (最新的优先)"
634+
},
635+
"usage": {
636+
"low": "数据使用量 (从低到高)",
637+
"high": "数据使用量 (从高到低)"
638+
}
639+
},
624640
"header.donation": "捐赠",
625641
"header.hostSettings": "设置",
626642
"header.logout": "退出",

dashboard/src/components/users/columns.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export const setupColumns = ({
1717
t: (key: string) => string
1818
handleSort: (column: string) => void
1919
filters: { sort: string; status?: string }
20-
handleStatusFilter: (value: any) => void
21-
dir: any
20+
handleStatusFilter: (value: string) => void
21+
dir: string
2222
}): ColumnDef<UserResponse>[] => [
2323
{
2424
accessorKey: 'username',
@@ -59,7 +59,7 @@ export const setupColumns = ({
5959
accessorKey: 'status',
6060
header: () => (
6161
<div className="flex items-center">
62-
<Select dir={dir || ''} onValueChange={handleStatusFilter} value={filters.status || '0'}>
62+
<Select dir={dir as 'ltr' | 'rtl'} onValueChange={handleStatusFilter} value={filters.status || '0'}>
6363
<SelectTrigger icon={false} className="ring-none w-fit max-w-28 border-none p-0 sm:px-1">
6464
<span className="px-0 text-xs capitalize">{t('usersTable.status')}</span>
6565
</SelectTrigger>
@@ -74,6 +74,7 @@ export const setupColumns = ({
7474
<SelectItem value="expired">{t('hostsDialog.status.expired')}</SelectItem>
7575
</SelectContent>
7676
</Select>
77+
{/* Desktop expire sorting */}
7778
<div className="hidden items-center sm:flex">
7879
<span>/</span>
7980
<button className="flex w-full items-center gap-1 px-2 py-3" onClick={handleSort.bind(null, 'expire')}>

dashboard/src/components/users/filters.tsx

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, Pagi
44
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
55
import { Badge } from '@/components/ui/badge'
66
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
7+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuLabel } from '@/components/ui/dropdown-menu'
78
import useDirDetection from '@/hooks/use-dir-detection'
89
import { cn } from '@/lib/utils'
910
import { debounce } from 'es-toolkit'
10-
import { RefreshCw, SearchIcon, Filter, X } from 'lucide-react'
11+
import { RefreshCw, SearchIcon, Filter, X, ArrowUpDown, User, Calendar, ChartPie, ChevronDown } from 'lucide-react'
1112
import { useCallback, useState } from 'react'
1213
import { useTranslation } from 'react-i18next'
1314
import { useGetUsers, UserStatus } from '@/service/api'
@@ -29,9 +30,10 @@ interface FiltersProps {
2930
advanceSearchOnOpen: (status: boolean) => void
3031
advanceSearchForm?: UseFormReturn<any>
3132
onClearAdvanceSearch?: () => void
33+
handleSort?: (column: string) => void
3234
}
3335

34-
export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen, advanceSearchForm, onClearAdvanceSearch }: FiltersProps) => {
36+
export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen, advanceSearchForm, onClearAdvanceSearch, handleSort }: FiltersProps) => {
3537
const { t } = useTranslation()
3638
const dir = useDirDetection()
3739
const [search, setSearch] = useState(filters.search || '')
@@ -132,6 +134,97 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
132134
</Popover>
133135
)}
134136
</div>
137+
{/* Sort Button */}
138+
{handleSort && (
139+
<div className="flex h-full items-center gap-1">
140+
<DropdownMenu>
141+
<DropdownMenuTrigger asChild>
142+
<Button
143+
size="icon-md"
144+
variant="ghost"
145+
className="relative flex items-center gap-2 border"
146+
aria-label={t('sortOptions', { defaultValue: 'Sort Options' })}
147+
>
148+
<ArrowUpDown className="h-4 w-4" />
149+
{filters.sort && filters.sort !== '-created_at' && (
150+
<div className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-primary" />
151+
)}
152+
</Button>
153+
</DropdownMenuTrigger>
154+
<DropdownMenuContent align="end" className="w-56">
155+
{/* Username Section */}
156+
<DropdownMenuLabel className="flex items-center gap-2 text-xs text-muted-foreground">
157+
<User className="h-3 w-3" />
158+
{t('username')}
159+
</DropdownMenuLabel>
160+
<DropdownMenuItem
161+
onClick={() => handleSort && handleSort('username')}
162+
className={filters.sort === 'username' ? 'bg-accent' : ''}
163+
>
164+
<User className="mr-2 h-4 w-4" />
165+
{t('sort.username.asc')}
166+
{filters.sort === 'username' && <ChevronDown className="ml-auto h-4 w-4 rotate-180" />}
167+
</DropdownMenuItem>
168+
<DropdownMenuItem
169+
onClick={() => handleSort && handleSort('-username')}
170+
className={filters.sort === '-username' ? 'bg-accent' : ''}
171+
>
172+
<User className="mr-2 h-4 w-4" />
173+
{t('sort.username.desc')}
174+
{filters.sort === '-username' && <ChevronDown className="ml-auto h-4 w-4" />}
175+
</DropdownMenuItem>
176+
177+
<DropdownMenuSeparator />
178+
179+
{/* Expire Date Section */}
180+
<DropdownMenuLabel className="flex items-center gap-2 text-xs text-muted-foreground">
181+
<Calendar className="h-3 w-3" />
182+
{t('expireDate')}
183+
</DropdownMenuLabel>
184+
<DropdownMenuItem
185+
onClick={() => handleSort && handleSort('expire')}
186+
className={filters.sort === 'expire' ? 'bg-accent' : ''}
187+
>
188+
<Calendar className="mr-2 h-4 w-4" />
189+
{t('sort.expire.oldest')}
190+
{filters.sort === 'expire' && <ChevronDown className="ml-auto h-4 w-4 rotate-180" />}
191+
</DropdownMenuItem>
192+
<DropdownMenuItem
193+
onClick={() => handleSort && handleSort('-expire')}
194+
className={filters.sort === '-expire' ? 'bg-accent' : ''}
195+
>
196+
<Calendar className="mr-2 h-4 w-4" />
197+
{t('sort.expire.newest')}
198+
{filters.sort === '-expire' && <ChevronDown className="ml-auto h-4 w-4" />}
199+
</DropdownMenuItem>
200+
201+
<DropdownMenuSeparator />
202+
203+
{/* Data Usage Section */}
204+
<DropdownMenuLabel className="flex items-center gap-2 text-xs text-muted-foreground">
205+
<ChartPie className="h-3 w-3" />
206+
{t('dataUsage')}
207+
</DropdownMenuLabel>
208+
<DropdownMenuItem
209+
onClick={() => handleSort && handleSort('used_traffic')}
210+
className={filters.sort === 'used_traffic' ? 'bg-accent' : ''}
211+
>
212+
<ChartPie className="mr-2 h-4 w-4" />
213+
{t('sort.usage.low')}
214+
{filters.sort === 'used_traffic' && <ChevronDown className="ml-auto h-4 w-4 rotate-180" />}
215+
</DropdownMenuItem>
216+
<DropdownMenuItem
217+
onClick={() => handleSort && handleSort('-used_traffic')}
218+
className={filters.sort === '-used_traffic' ? 'bg-accent' : ''}
219+
>
220+
<ChartPie className="mr-2 h-4 w-4" />
221+
{t('sort.usage.high')}
222+
{filters.sort === '-used_traffic' && <ChevronDown className="ml-auto h-4 w-4" />}
223+
</DropdownMenuItem>
224+
</DropdownMenuContent>
225+
</DropdownMenu>
226+
</div>
227+
)}
135228
{/* Refresh Button */}
136229
<div className="flex h-full items-center gap-2">
137230
<Button size="icon-md" onClick={handleRefreshClick} variant="ghost" className="flex items-center gap-2 border" disabled={isRefreshing}>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ const UsersTable = memo(() => {
253253
advanceSearchOnOpen={setIsAdvanceSearchOpen}
254254
refetch={handleManualRefresh}
255255
advanceSearchForm={advanceSearchForm}
256+
handleSort={handleSort}
256257
onClearAdvanceSearch={() => {
257258
advanceSearchForm.reset({
258259
is_username: true,

0 commit comments

Comments
 (0)