Skip to content

Commit 4b87265

Browse files
committed
feat(user): add sort by online_at
1 parent 01c4c2b commit 4b87265

File tree

7 files changed

+84
-30
lines changed

7 files changed

+84
-30
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@
690690
"disabled": "disabled",
691691
"expire": "Expire",
692692
"expireDate": "Expire Date",
693+
"lastOnline": "Last Online",
693694
"expires": "Expires in {{time}}",
694695
"expired": "Expired {{time}}",
695696
"sortOptions": "Sort Options",
@@ -705,6 +706,10 @@
705706
"usage": {
706707
"low": "Data Usage (Low to High)",
707708
"high": "Data Usage (High to Low)"
709+
},
710+
"online": {
711+
"oldest": "Last Online (Oldest First)",
712+
"newest": "Last Online (Newest First)"
708713
}
709714
},
710715
"header.donation": "Donation",

dashboard/public/statics/locales/fa.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@
576576
"disabled": "غیر فعال",
577577
"expire": "انقضا",
578578
"expireDate": "تاریخ انقضا",
579+
"lastOnline": "آخرین آنلاین",
579580
"expired": "{{time}} منقضی شده",
580581
"expires": "پایان در {{time}}",
581582
"sortOptions": "گزینه‌های مرتب‌سازی",
@@ -591,6 +592,10 @@
591592
"usage": {
592593
"low": "مصرف داده (کم به زیاد)",
593594
"high": "مصرف داده (زیاد به کم)"
595+
},
596+
"online": {
597+
"oldest": "آخرین آنلاین (قدیمی‌ترین اول)",
598+
"newest": "آخرین آنلاین (جدیدترین اول)"
594599
}
595600
},
596601
"header.donation": "کمک مالی",

dashboard/public/statics/locales/ru.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@
676676
"disabled": "disabled",
677677
"expire": "Истекает",
678678
"expireDate": "Дата истечения",
679+
"lastOnline": "Последний онлайн",
679680
"expired": "Истёк {{time}}",
680681
"expires": "Истекает через {{time}}",
681682
"sortOptions": "Параметры сортировки",
@@ -691,6 +692,10 @@
691692
"usage": {
692693
"low": "Использование данных (от малого к большому)",
693694
"high": "Использование данных (от большого к малому)"
695+
},
696+
"online": {
697+
"oldest": "Последний онлайн (сначала старые)",
698+
"newest": "Последний онлайн (сначала новые)"
694699
}
695700
},
696701
"header.donation": "Пожертвование",

dashboard/public/statics/locales/zh.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@
689689
"disabled": "禁用",
690690
"expire": "过期",
691691
"expireDate": "到期日期",
692+
"lastOnline": "最后在线",
692693
"expired": "{{time}}已过期",
693694
"expires": "将在 {{time}} 后过期",
694695
"sortOptions": "排序选项",
@@ -704,6 +705,10 @@
704705
"usage": {
705706
"low": "数据使用量 (从低到高)",
706707
"high": "数据使用量 (从高到低)"
708+
},
709+
"online": {
710+
"oldest": "最后在线 (最旧的优先)",
711+
"newest": "最后在线 (最新的优先)"
707712
}
708713
},
709714
"header.donation": "捐赠",

dashboard/src/components/users/columns.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UserResponse } from '@/service/api'
1+
import { UserResponse, UserStatus } from '@/service/api'
22
import { ColumnDef } from '@tanstack/react-table'
33
import { ChevronDown } from 'lucide-react'
44
import ActionButtons from '../ActionButtons'
@@ -16,8 +16,8 @@ export const setupColumns = ({
1616
}: {
1717
t: (key: string) => string
1818
handleSort: (column: string, fromDropdown?: boolean) => void
19-
filters: { sort: string; status?: string }
20-
handleStatusFilter: (value: string) => void
19+
filters: { sort: string; status?: UserStatus | null; [key: string]: unknown }
20+
handleStatusFilter: (value: string | UserStatus) => void
2121
dir: string
2222
}): ColumnDef<UserResponse>[] => [
2323
{
@@ -59,7 +59,7 @@ export const setupColumns = ({
5959
accessorKey: 'status',
6060
header: () => (
6161
<div className="flex items-center">
62-
<Select dir={dir as 'ltr' | 'rtl'} onValueChange={handleStatusFilter} value={filters.status || '0'}>
62+
<Select dir={dir as 'ltr' | 'rtl'} onValueChange={handleStatusFilter} value={(filters.status as string) || '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>

dashboard/src/components/users/filters.tsx

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
88
import useDirDetection from '@/hooks/use-dir-detection'
99
import { cn } from '@/lib/utils'
1010
import { debounce } from 'es-toolkit'
11-
import { RefreshCw, SearchIcon, Filter, X, ArrowUpDown, User, Calendar, ChartPie, ChevronDown, Check } from 'lucide-react'
11+
import { RefreshCw, SearchIcon, Filter, X, ArrowUpDown, User, Calendar, ChartPie, ChevronDown, Check, Clock } from 'lucide-react'
1212
import { useState, useRef, useEffect, useCallback } from 'react'
1313
import { useTranslation } from 'react-i18next'
1414
import { useGetUsers, UserStatus } from '@/service/api'
@@ -24,26 +24,35 @@ const sortSections = [
2424
icon: User,
2525
label: 'username',
2626
items: [
27-
{ value: 'username', label: 'sort.username.asc' },
2827
{ value: '-username', label: 'sort.username.desc' },
28+
{ value: 'username', label: 'sort.username.asc' },
2929
],
3030
},
3131
{
3232
key: 'expire',
3333
icon: Calendar,
3434
label: 'expireDate',
3535
items: [
36-
{ value: 'expire', label: 'sort.expire.oldest' },
3736
{ value: '-expire', label: 'sort.expire.newest' },
37+
{ value: 'expire', label: 'sort.expire.oldest' },
3838
],
3939
},
4040
{
4141
key: 'usage',
4242
icon: ChartPie,
4343
label: 'dataUsage',
4444
items: [
45-
{ value: 'used_traffic', label: 'sort.usage.low' },
4645
{ value: '-used_traffic', label: 'sort.usage.high' },
46+
{ value: 'used_traffic', label: 'sort.usage.low' },
47+
],
48+
},
49+
{
50+
key: 'onlineAt',
51+
icon: Clock,
52+
label: 'lastOnline',
53+
items: [
54+
{ value: '-online_at', label: 'sort.online.newest' },
55+
{ value: 'online_at', label: 'sort.online.oldest' },
4756
],
4857
},
4958
] as const
@@ -242,25 +251,24 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
242251
{filters.sort && filters.sort !== '-created_at' && <div className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-primary" />}
243252
</Button>
244253
</DropdownMenuTrigger>
245-
<DropdownMenuContent align="end" className="w-52 md:w-56">
254+
<DropdownMenuContent align="end" className="w-44">
246255
{sortSections.map((section, sectionIndex) => (
247256
<div key={section.key}>
248257
{/* Section Label */}
249-
<DropdownMenuLabel className="flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground md:gap-2 md:px-3 md:py-2">
250-
<section.icon className="h-3 w-3" />
251-
<span className="text-xs md:text-sm">{t(section.label)}</span>
258+
<DropdownMenuLabel className="flex items-center gap-1 px-1.5 py-1 text-[10px] text-muted-foreground">
259+
<section.icon className="h-2.5 w-2.5" />
260+
<span className="text-[10px]">{t(section.label)}</span>
252261
</DropdownMenuLabel>
253262

254263
{/* Section Items */}
255264
{section.items.map(item => (
256265
<DropdownMenuItem
257266
key={item.value}
258267
onClick={() => handleSort && handleSort(item.value, true)}
259-
className={`whitespace-nowrap px-2 py-1.5 text-xs md:px-3 md:py-2 ${filters.sort === item.value ? 'bg-accent' : ''}`}
268+
className={`whitespace-nowrap px-1.5 py-1 text-[11px] ${filters.sort === item.value ? 'bg-accent' : ''}`}
260269
>
261-
<section.icon className="mr-1.5 h-3 w-3 flex-shrink-0 md:mr-2 md:h-4 md:w-4" />
262270
<span className="truncate">{t(item.label)}</span>
263-
{filters.sort === item.value && <ChevronDown className={`ml-auto h-3 w-3 flex-shrink-0 md:h-4 md:w-4 ${item.value.startsWith('-') ? '' : 'rotate-180'}`} />}
271+
{filters.sort === item.value && <ChevronDown className={`ml-auto h-2.5 w-2.5 flex-shrink-0 ${item.value.startsWith('-') ? '' : 'rotate-180'}`} />}
264272
</DropdownMenuItem>
265273
))}
266274

@@ -298,20 +306,20 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
298306
<ChevronDown className="h-3 w-3" />
299307
</Button>
300308
</DropdownMenuTrigger>
301-
<DropdownMenuContent align="end" className="w-52 md:w-56">
302-
<DropdownMenuLabel className="flex flex-col gap-0.5 px-2 py-1.5 text-xs text-muted-foreground md:px-3 md:py-2">
309+
<DropdownMenuContent align="end" className="w-44">
310+
<DropdownMenuLabel className="flex flex-col gap-0.5 px-1.5 py-1 text-[10px] text-muted-foreground">
303311
<span>{t('autoRefresh.label')}</span>
304-
<span className="text-[11px] md:text-xs">{t('autoRefresh.currentSelection', { value: currentAutoRefreshDescription })}</span>
312+
<span className="text-[9px]">{t('autoRefresh.currentSelection', { value: currentAutoRefreshDescription })}</span>
305313
</DropdownMenuLabel>
306314
<DropdownMenuSeparator />
307315
<DropdownMenuItem
308316
onSelect={() => void handleRefreshClick()}
309317
disabled={isRefreshing}
310-
className="flex items-center gap-2 px-2 py-1.5 text-xs md:px-3 md:py-2"
318+
className="flex items-center gap-1.5 px-1.5 py-1 text-[11px]"
311319
>
312-
<RefreshCw className={cn('h-3.5 w-3.5 flex-shrink-0', isRefreshing && 'animate-spin')} />
320+
<RefreshCw className={cn('h-2.5 w-2.5 flex-shrink-0', isRefreshing && 'animate-spin')} />
313321
<span className="truncate">{t('autoRefresh.refreshNow')}</span>
314-
{isRefreshing && <LoaderCircle className="ml-auto h-3.5 w-3.5 animate-spin" />}
322+
{isRefreshing && <LoaderCircle className="ml-auto h-2.5 w-2.5 animate-spin" />}
315323
</DropdownMenuItem>
316324
<DropdownMenuSeparator />
317325
{autoRefreshOptions.map(option => {
@@ -320,10 +328,10 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
320328
<DropdownMenuItem
321329
key={option.value}
322330
onSelect={() => handleAutoRefreshChange(option.value)}
323-
className={cn('flex items-center gap-2 whitespace-nowrap px-2 py-1.5 text-xs md:px-3 md:py-2', isActive && 'bg-accent')}
331+
className={cn('flex items-center gap-1.5 whitespace-nowrap px-1.5 py-1 text-[11px]', isActive && 'bg-accent')}
324332
>
325333
<span>{t(option.labelKey)}</span>
326-
{isActive && <Check className="ml-auto h-3 w-3 flex-shrink-0" />}
334+
{isActive && <Check className="ml-auto h-2.5 w-2.5 flex-shrink-0" />}
327335
</DropdownMenuItem>
328336
)
329337
})}

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DataTable } from '@/components/users/data-table'
33
import { Filters } from '@/components/users/filters'
44
import useDirDetection from '@/hooks/use-dir-detection'
55
import { UseEditFormValues } from '@/pages/_dashboard.users'
6-
import { useGetUsers, UserResponse } from '@/service/api'
6+
import { useGetUsers, UserResponse, UserStatus } from '@/service/api'
77
import { useAdmin } from '@/hooks/use-admin'
88
import { getUsersPerPageLimitSize, setUsersPerPageLimitSize } from '@/utils/userPreferenceStorage'
99
import { useQueryClient } from '@tanstack/react-query'
@@ -28,14 +28,28 @@ const UsersTable = memo(() => {
2828
const { admin } = useAdmin()
2929
const isSudo = admin?.is_sudo || false
3030

31-
const [filters, setFilters] = useState({
31+
const [filters, setFilters] = useState<{
32+
limit: number
33+
sort: string
34+
load_sub: boolean
35+
offset: number
36+
search?: string
37+
proxy_id?: string
38+
is_protocol: boolean
39+
status?: UserStatus | null
40+
admin?: string[]
41+
group?: number[]
42+
}>({
3243
limit: itemsPerPage,
3344
sort: '-created_at',
3445
load_sub: true,
3546
offset: 0,
36-
search: undefined as string | undefined,
37-
proxy_id: undefined as string | undefined, // add proxy_id
38-
is_protocol: false, // add is_protocol
47+
search: undefined,
48+
proxy_id: undefined,
49+
is_protocol: false,
50+
status: undefined,
51+
admin: undefined,
52+
group: undefined,
3953
})
4054

4155
const advanceSearchForm = useForm<AdvanceSearchFormValue>({
@@ -108,6 +122,15 @@ const UsersTable = memo(() => {
108122
}))
109123
}, [currentPage, itemsPerPage])
110124

125+
// Sync advance search form with current filters when modal opens
126+
useEffect(() => {
127+
if (isAdvanceSearchOpen) {
128+
advanceSearchForm.setValue('status', filters.status || '0')
129+
advanceSearchForm.setValue('admin', filters.admin || [])
130+
advanceSearchForm.setValue('group', filters.group || [])
131+
}
132+
}, [isAdvanceSearchOpen, filters.status, filters.admin, filters.group, advanceSearchForm])
133+
111134
const {
112135
data: usersData,
113136
refetch,
@@ -180,6 +203,9 @@ const UsersTable = memo(() => {
180203
)
181204

182205
const handleStatusFilter = useCallback((value: any) => {
206+
// Sync with advance search form
207+
advanceSearchForm.setValue('status', value || '0')
208+
183209
// If value is '0' or empty, set status to undefined to remove it from the URL
184210
if (value === '0' || value === '') {
185211
setFilters(prev => ({
@@ -196,7 +222,7 @@ const UsersTable = memo(() => {
196222
}
197223

198224
setCurrentPage(0) // Reset current page
199-
}, [])
225+
}, [advanceSearchForm])
200226

201227
const handleFilterChange = useCallback((newFilters: Partial<typeof filters>) => {
202228
setFilters(prev => {
@@ -263,7 +289,7 @@ const UsersTable = memo(() => {
263289
t,
264290
dir,
265291
handleSort,
266-
filters,
292+
filters: filters as { sort: string; status?: UserStatus | null; [key: string]: unknown },
267293
handleStatusFilter,
268294
})
269295

0 commit comments

Comments
 (0)