Skip to content

Commit d2c3395

Browse files
committed
feat(users): add auto refresh
1 parent 0c86ab1 commit d2c3395

File tree

6 files changed

+178
-7
lines changed

6 files changed

+178
-7
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,19 @@
12001200
"usersTable.noUserMatched": "It seems there is no user matched with what you are looking for",
12011201
"usersTable.status": "Status",
12021202
"usersTable.total": "Total",
1203+
"autoRefresh": {
1204+
"label": "Auto refresh",
1205+
"currentSelection": "Current: {{value}}",
1206+
"off": "Manual refresh only",
1207+
"offShort": "Off",
1208+
"shortSeconds": "{{count}}s",
1209+
"shortMinutes": "{{count}}m",
1210+
"15Seconds": "Every 15 seconds",
1211+
"30Seconds": "Every 30 seconds",
1212+
"1Minute": "Every minute",
1213+
"5Minutes": "Every 5 minutes",
1214+
"refreshNow": "Refresh now"
1215+
},
12031216
"statistics.system": "System",
12041217
"statistics.totalTraffic": "Total Traffic",
12051218
"statistics.networkSpeed": "Network Speed",

dashboard/public/statics/locales/fa.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,19 @@
10411041
"usersTable.noUserMatched": "به‌نظر میرسه کاربری که جستجو کردید، وجود ندارد",
10421042
"usersTable.status": "وضعیت",
10431043
"usersTable.total": "مجموع",
1044+
"autoRefresh": {
1045+
"label": "به‌روزرسانی خودکار",
1046+
"currentSelection": "حالت فعلی: {{value}}",
1047+
"off": "فقط به‌روزرسانی دستی",
1048+
"offShort": "خاموش",
1049+
"shortSeconds": "{{count}} ث",
1050+
"shortMinutes": "{{count}} د",
1051+
"15Seconds": "هر ۱۵ ثانیه",
1052+
"30Seconds": "هر ۳۰ ثانیه",
1053+
"1Minute": "هر ۱ دقیقه",
1054+
"5Minutes": "هر ۵ دقیقه",
1055+
"refreshNow": "به‌روزرسانی دستی"
1056+
},
10441057
"statistics.system": "سیستم",
10451058
"statistics.totalTraffic": "کل ترافیک",
10461059
"statistics.networkSpeed": "سرعت شبکه",

dashboard/public/statics/locales/ru.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,19 @@
832832
"usersTable.noUserMatched": "Похоже, нет пользователя, соответствующего вашему запросу",
833833
"usersTable.status": "Статус",
834834
"usersTable.total": "Всего",
835+
"autoRefresh": {
836+
"label": "Автообновление",
837+
"currentSelection": "Текущий режим: {{value}}",
838+
"off": "Только ручное обновление",
839+
"offShort": "Выкл.",
840+
"shortSeconds": "{{count}} с",
841+
"shortMinutes": "{{count}} мин",
842+
"15Seconds": "Каждые 15 секунд",
843+
"30Seconds": "Каждые 30 секунд",
844+
"1Minute": "Каждую минуту",
845+
"5Minutes": "Каждые 5 минут",
846+
"refreshNow": "Обновить сейчас"
847+
},
835848
"statistics.system": "Система",
836849
"statistics.totalTraffic": "Общий трафик",
837850
"statistics.networkSpeed": "Скорость сети",

dashboard/public/statics/locales/zh.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,19 @@
11481148
"usersTable.status": "状态",
11491149
"usersTable.sortByExpire": "按过期时间排序",
11501150
"usersTable.total": "总共",
1151+
"autoRefresh": {
1152+
"label": "自动刷新",
1153+
"currentSelection": "当前:{{value}}",
1154+
"off": "仅手动刷新",
1155+
"offShort": "",
1156+
"shortSeconds": "{{count}} 秒",
1157+
"shortMinutes": "{{count}} 分",
1158+
"15Seconds": "每 15 秒",
1159+
"30Seconds": "每 30 秒",
1160+
"1Minute": "每 1 分钟",
1161+
"5Minutes": "每 5 分钟",
1162+
"refreshNow": "立即刷新"
1163+
},
11511164
"statistics.system": "系统",
11521165
"statistics.totalTraffic": "总流量",
11531166
"statistics.networkSpeed": "网络速度",

dashboard/src/components/users/filters.tsx

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ 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 } from 'lucide-react'
12-
import { useState, useRef, useEffect } from 'react'
11+
import { RefreshCw, SearchIcon, Filter, X, ArrowUpDown, User, Calendar, ChartPie, ChevronDown, Check } from 'lucide-react'
12+
import { useState, useRef, useEffect, useCallback } from 'react'
1313
import { useTranslation } from 'react-i18next'
1414
import { useGetUsers, UserStatus } from '@/service/api'
1515
import { RefetchOptions } from '@tanstack/react-query'
1616
import { LoaderCircle } from 'lucide-react'
1717
import { UseFormReturn } from 'react-hook-form'
18+
import { getUsersAutoRefreshIntervalSeconds, setUsersAutoRefreshIntervalSeconds } from '@/utils/userPreferenceStorage'
1819

1920
// Sort configuration to eliminate duplication
2021
const sortSections = [
@@ -47,6 +48,14 @@ const sortSections = [
4748
},
4849
] as const
4950

51+
const autoRefreshOptions = [
52+
{ value: 0, labelKey: 'autoRefresh.off' },
53+
{ value: 15, labelKey: 'autoRefresh.15Seconds' },
54+
{ value: 30, labelKey: 'autoRefresh.30Seconds' },
55+
{ value: 60, labelKey: 'autoRefresh.1Minute' },
56+
{ value: 300, labelKey: 'autoRefresh.5Minutes' },
57+
] as const
58+
5059
interface FiltersProps {
5160
filters: {
5261
search?: string
@@ -69,8 +78,47 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
6978
const dir = useDirDetection()
7079
const [search, setSearch] = useState(filters.search || '')
7180
const [isRefreshing, setIsRefreshing] = useState(false)
72-
const userQuery = useGetUsers(filters)
73-
const handleRefetch = refetch || userQuery.refetch
81+
const [autoRefreshInterval, setAutoRefreshInterval] = useState<number>(() => getUsersAutoRefreshIntervalSeconds())
82+
const { refetch: queryRefetch } = useGetUsers(filters, {
83+
query: {
84+
refetchOnWindowFocus: false,
85+
},
86+
})
87+
const refetchUsers = useCallback(() => {
88+
const refetchFn = refetch ?? queryRefetch
89+
return refetchFn()
90+
}, [refetch, queryRefetch])
91+
useEffect(() => {
92+
const persistedValue = getUsersAutoRefreshIntervalSeconds()
93+
setAutoRefreshInterval(prev => (prev === persistedValue ? prev : persistedValue))
94+
}, [])
95+
useEffect(() => {
96+
if (!autoRefreshInterval) return
97+
const intervalId = setInterval(() => {
98+
refetchUsers()
99+
}, autoRefreshInterval * 1000)
100+
return () => clearInterval(intervalId)
101+
}, [autoRefreshInterval, refetchUsers])
102+
useEffect(() => {
103+
if (typeof document === 'undefined') return
104+
const handleVisibilityChange = () => {
105+
if (document.visibilityState === 'visible' && autoRefreshInterval > 0) {
106+
refetchUsers()
107+
}
108+
}
109+
document.addEventListener('visibilitychange', handleVisibilityChange)
110+
return () => {
111+
document.removeEventListener('visibilitychange', handleVisibilityChange)
112+
}
113+
}, [autoRefreshInterval, refetchUsers])
114+
const currentAutoRefreshOption = autoRefreshOptions.find(option => option.value === autoRefreshInterval) ?? autoRefreshOptions[0]
115+
const autoRefreshShortLabel =
116+
autoRefreshInterval === 0
117+
? t('autoRefresh.offShort')
118+
: autoRefreshInterval < 60
119+
? t('autoRefresh.shortSeconds', { count: autoRefreshInterval })
120+
: t('autoRefresh.shortMinutes', { count: Math.round(autoRefreshInterval / 60) })
121+
const currentAutoRefreshDescription = t(currentAutoRefreshOption.labelKey)
74122
const onFilterChangeRef = useRef(onFilterChange)
75123

76124
// Keep the ref in sync with the prop
@@ -114,13 +162,18 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
114162
const handleRefreshClick = async () => {
115163
setIsRefreshing(true)
116164
try {
117-
await handleRefetch()
165+
await refetchUsers()
118166
} finally {
119167
// Instant response - no delay
120168
setIsRefreshing(false)
121169
}
122170
}
123171

172+
const handleAutoRefreshChange = (seconds: number) => {
173+
setUsersAutoRefreshIntervalSeconds(seconds)
174+
setAutoRefreshInterval(seconds)
175+
}
176+
124177
const handleOpenAdvanceSearch = () => {
125178
advanceSearchOnOpen(true)
126179
}
@@ -224,10 +277,62 @@ export const Filters = ({ filters, onFilterChange, refetch, advanceSearchOnOpen,
224277
</div>
225278
)}
226279
{/* Refresh Button */}
227-
<div className="flex h-full items-center gap-2">
228-
<Button size="icon-md" onClick={handleRefreshClick} variant="ghost" className="flex items-center gap-2 border" disabled={isRefreshing}>
280+
<div className="flex h-full items-center gap-0">
281+
<Button
282+
size="icon-md"
283+
onClick={handleRefreshClick}
284+
variant="ghost"
285+
className={cn('relative flex items-center gap-2 border', dir === 'rtl' ? 'rounded-l-none border-l-0' : 'rounded-r-none')}
286+
aria-label={t('autoRefresh.refreshNow')}
287+
title={t('autoRefresh.refreshNow')}
288+
disabled={isRefreshing}
289+
>
229290
<RefreshCw className={cn('h-4 w-4', isRefreshing && 'animate-spin')} />
291+
{autoRefreshInterval > 0 && <div className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-primary" />}
230292
</Button>
293+
<DropdownMenu>
294+
<DropdownMenuTrigger asChild>
295+
<Button
296+
size="icon-md"
297+
variant="ghost"
298+
className={cn('relative flex items-center gap-2 border', dir === 'rtl' ? 'rounded-r-none' : 'rounded-l-none border-l-0')}
299+
aria-label={t('autoRefresh.label')}
300+
title={`${t('autoRefresh.label')} (${autoRefreshShortLabel})`}
301+
>
302+
<ChevronDown className="h-3 w-3" />
303+
</Button>
304+
</DropdownMenuTrigger>
305+
<DropdownMenuContent align="end" className="w-52 md:w-56">
306+
<DropdownMenuLabel className="flex flex-col gap-0.5 px-2 py-1.5 text-xs text-muted-foreground md:px-3 md:py-2">
307+
<span>{t('autoRefresh.label')}</span>
308+
<span className="text-[11px] md:text-xs">{t('autoRefresh.currentSelection', { value: currentAutoRefreshDescription })}</span>
309+
</DropdownMenuLabel>
310+
<DropdownMenuSeparator />
311+
<DropdownMenuItem
312+
onSelect={() => void handleRefreshClick()}
313+
disabled={isRefreshing}
314+
className="flex items-center gap-2 px-2 py-1.5 text-xs md:px-3 md:py-2"
315+
>
316+
<RefreshCw className={cn('h-3.5 w-3.5 flex-shrink-0', isRefreshing && 'animate-spin')} />
317+
<span className="truncate">{t('autoRefresh.refreshNow')}</span>
318+
{isRefreshing && <LoaderCircle className="ml-auto h-3.5 w-3.5 animate-spin" />}
319+
</DropdownMenuItem>
320+
<DropdownMenuSeparator />
321+
{autoRefreshOptions.map(option => {
322+
const isActive = option.value === autoRefreshInterval
323+
return (
324+
<DropdownMenuItem
325+
key={option.value}
326+
onSelect={() => handleAutoRefreshChange(option.value)}
327+
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')}
328+
>
329+
<span>{t(option.labelKey)}</span>
330+
{isActive && <Check className="ml-auto h-3 w-3 flex-shrink-0" />}
331+
</DropdownMenuItem>
332+
)
333+
})}
334+
</DropdownMenuContent>
335+
</DropdownMenu>
231336
</div>
232337
</div>
233338
)

dashboard/src/utils/userPreferenceStorage.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ const NUM_USERS_PER_PAGE_LOCAL_STORAGE_KEY = 'pasarguard-num-users-per-page'
22
const NUM_ADMINS_PER_PAGE_LOCAL_STORAGE_KEY = 'pasarguard-num-admins-per-page'
33
const NUM_ITEMS_PER_PAGE_DEFAULT = 10
44

5+
const USERS_AUTO_REFRESH_INTERVAL_KEY = 'pasarguard-users-auto-refresh-interval'
6+
const DEFAULT_USERS_AUTO_REFRESH_INTERVAL_SECONDS = 0
7+
58
// Generic function for any table type
69
export const getItemsPerPageLimitSize = (tableType: 'users' | 'admins' = 'users') => {
710
const storageKey = tableType === 'users' ? NUM_USERS_PER_PAGE_LOCAL_STORAGE_KEY : NUM_ADMINS_PER_PAGE_LOCAL_STORAGE_KEY
@@ -20,3 +23,14 @@ export const setUsersPerPageLimitSize = (value: string) => setItemsPerPageLimitS
2023

2124
export const getAdminsPerPageLimitSize = () => getItemsPerPageLimitSize('admins')
2225
export const setAdminsPerPageLimitSize = (value: string) => setItemsPerPageLimitSize(value, 'admins')
26+
27+
export const getUsersAutoRefreshIntervalSeconds = () => {
28+
const storedValue = typeof localStorage !== 'undefined' && localStorage.getItem(USERS_AUTO_REFRESH_INTERVAL_KEY)
29+
const parsed = storedValue ? parseInt(storedValue, 10) : DEFAULT_USERS_AUTO_REFRESH_INTERVAL_SECONDS
30+
return Number.isNaN(parsed) ? DEFAULT_USERS_AUTO_REFRESH_INTERVAL_SECONDS : parsed
31+
}
32+
33+
export const setUsersAutoRefreshIntervalSeconds = (seconds: number) => {
34+
if (typeof localStorage === 'undefined') return
35+
localStorage.setItem(USERS_AUTO_REFRESH_INTERVAL_KEY, seconds.toString())
36+
}

0 commit comments

Comments
 (0)