Skip to content

Commit 32a5984

Browse files
committed
feat(ui): add refresh button to groups, hosts, cores, and user templates components for improved custom data fetching
1 parent c0d58f6 commit 32a5984

File tree

5 files changed

+88
-12
lines changed

5 files changed

+88
-12
lines changed

dashboard/src/components/groups/groups-list.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import useDirDetection from '@/hooks/use-dir-detection'
1313
import { Skeleton } from '@/components/ui/skeleton'
1414
import { Card } from '@/components/ui/card'
1515
import { Input } from '@/components/ui/input'
16-
import { Search, X } from 'lucide-react'
16+
import { Button } from '@/components/ui/button'
17+
import { RefreshCw, Search, X } from 'lucide-react'
1718
import { cn } from '@/lib/utils'
1819

1920
const initialDefaultValues: Partial<GroupFormValues> = {
@@ -33,7 +34,7 @@ export default function GroupsList({ isDialogOpen, onOpenChange }: GroupsListPro
3334
const { t } = useTranslation()
3435
const modifyGroupMutation = useModifyGroup()
3536
const dir = useDirDetection()
36-
const { data: groupsData, isLoading } = useGetAllGroups({})
37+
const { data: groupsData, isLoading, isFetching, refetch } = useGetAllGroups({})
3738

3839
const form = useForm<GroupFormValues>({
3940
resolver: zodResolver(groupFormSchema),
@@ -88,6 +89,10 @@ export default function GroupsList({ isDialogOpen, onOpenChange }: GroupsListPro
8889
return groupsData.groups.filter((group: GroupResponse) => group.name?.toLowerCase().includes(query))
8990
}, [groupsData?.groups, searchQuery])
9091

92+
const handleRefresh = async () => {
93+
await refetch()
94+
}
95+
9196
return (
9297
<div className="w-full flex-1 space-y-4">
9398
{/* Search Input */}
@@ -101,6 +106,16 @@ export default function GroupsList({ isDialogOpen, onOpenChange }: GroupsListPro
101106
</button>
102107
)}
103108
</div>
109+
<Button
110+
size="icon-md"
111+
variant="ghost"
112+
onClick={handleRefresh}
113+
className={cn('border', isFetching && 'opacity-70')}
114+
aria-label={t('autoRefresh.refreshNow')}
115+
title={t('autoRefresh.refreshNow')}
116+
>
117+
<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')} />
118+
</Button>
104119
</div>
105120
<ScrollArea className="h-[calc(100vh-8rem)]">
106121
<div dir={dir} className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">

dashboard/src/components/hosts/hosts-list.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import * as z from 'zod'
1111
import HostModal from '../dialogs/host-modal'
1212
import SortableHost from './sortable-host'
1313
import { Input } from '@/components/ui/input'
14-
import { Search, X } from 'lucide-react'
14+
import { Button } from '@/components/ui/button'
15+
import { RefreshCw, Search, X } from 'lucide-react'
1516
import useDirDetection from '@/hooks/use-dir-detection'
1617
import { cn } from '@/lib/utils'
1718

@@ -439,12 +440,15 @@ export interface HostsListProps {
439440
onSubmit: (data: HostFormValues) => Promise<{ status: number }>
440441
editingHost: BaseHost | null
441442
setEditingHost: (host: BaseHost | null) => void
443+
onRefresh?: () => Promise<unknown>
444+
isRefreshing?: boolean
442445
}
443446

444-
export default function HostsList({ data, onAddHost, isDialogOpen, onSubmit, editingHost, setEditingHost }: HostsListProps) {
447+
export default function HostsList({ data, onAddHost, isDialogOpen, onSubmit, editingHost, setEditingHost, onRefresh, isRefreshing: isRefreshingProp }: HostsListProps) {
445448
const [hosts, setHosts] = useState<BaseHost[] | undefined>()
446449
const [isUpdatingPriorities, setIsUpdatingPriorities] = useState(false)
447450
const [searchQuery, setSearchQuery] = useState('')
451+
const [isManualRefreshing, setIsManualRefreshing] = useState(false)
448452
const { t } = useTranslation()
449453
const dir = useDirDetection()
450454

@@ -460,13 +464,28 @@ export default function HostsList({ data, onAddHost, isDialogOpen, onSubmit, edi
460464

461465
const refreshHostsData = () => {
462466
// Just invalidate the main query key used in the dashboard
463-
queryClient.invalidateQueries({
467+
return queryClient.invalidateQueries({
464468
queryKey: ['getGetHostsQueryKey'],
465469
exact: true, // Only invalidate this exact query
466470
refetchType: 'active', // Only refetch if the query is currently being rendered
467471
})
468472
}
469473

474+
const handleRefreshClick = async () => {
475+
if (onRefresh) {
476+
await onRefresh()
477+
return
478+
}
479+
setIsManualRefreshing(true)
480+
try {
481+
await refreshHostsData()
482+
} finally {
483+
setIsManualRefreshing(false)
484+
}
485+
}
486+
487+
const isRefreshing = isRefreshingProp ?? isManualRefreshing
488+
470489
const handleEdit = (host: BaseHost) => {
471490
const formData: HostFormValues = {
472491
remark: host.remark || '',
@@ -814,7 +833,7 @@ export default function HostsList({ data, onAddHost, isDialogOpen, onSubmit, edi
814833
return (
815834
<div>
816835
{/* Search Input */}
817-
<div className="mb-4">
836+
<div className="mb-4 flex items-center gap-2 md:gap-3">
818837
<div className="relative w-full md:w-[calc(100%/3-10px)]" dir={dir}>
819838
<Search className={cn('absolute', dir === 'rtl' ? 'right-2' : 'left-2', 'top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground')} />
820839
<Input placeholder={t('search')} value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className={cn('pl-8 pr-10', dir === 'rtl' && 'pl-10 pr-8')} />
@@ -824,6 +843,16 @@ export default function HostsList({ data, onAddHost, isDialogOpen, onSubmit, edi
824843
</button>
825844
)}
826845
</div>
846+
<Button
847+
size="icon-md"
848+
variant="ghost"
849+
onClick={handleRefreshClick}
850+
className={cn('border', isRefreshing && 'opacity-70')}
851+
aria-label={t('autoRefresh.refreshNow')}
852+
title={t('autoRefresh.refreshNow')}
853+
>
854+
<RefreshCw className={cn('h-4 w-4', isRefreshing && 'animate-spin')} />
855+
</Button>
827856
</div>
828857
<div>
829858
<DndContext sensors={isUpdatingPriorities ? [] : sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>

dashboard/src/components/settings/cores.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import useDirDetection from '@/hooks/use-dir-detection'
1313
import { Skeleton } from '@/components/ui/skeleton'
1414
import { Card } from '@/components/ui/card'
1515
import { Input } from '@/components/ui/input'
16-
import { Search, X } from 'lucide-react'
16+
import { Button } from '@/components/ui/button'
17+
import { RefreshCw, Search, X } from 'lucide-react'
1718
import { cn } from '@/lib/utils'
1819

1920
const initialDefaultValues: Partial<CoreConfigFormValues> = {
@@ -38,7 +39,7 @@ export default function Cores({ isDialogOpen, onOpenChange, cores, onEditCore, o
3839
const modifyCoreMutation = useModifyCoreConfig()
3940
const dir = useDirDetection()
4041

41-
const { data: coresData, isLoading, refetch } = useGetAllCores({})
42+
const { data: coresData, isLoading, isFetching, refetch } = useGetAllCores({})
4243

4344
useEffect(() => {
4445
const handleOpenDialog = () => onOpenChange?.(true)
@@ -117,9 +118,13 @@ export default function Cores({ isDialogOpen, onOpenChange, cores, onEditCore, o
117118
return coresList.filter((core: CoreResponse) => core.name?.toLowerCase().includes(query))
118119
}, [coresList, searchQuery])
119120

121+
const handleRefreshClick = async () => {
122+
await refetch()
123+
}
124+
120125
return (
121126
<div className={cn('flex w-full flex-col gap-4 pt-4', dir === 'rtl' && 'rtl')}>
122-
<div>
127+
<div className="flex items-center gap-2 md:gap-3">
123128
{/* Search Input */}
124129
<div className="relative w-full md:w-[calc(100%/3-10px)]" dir={dir}>
125130
<Search className={cn('absolute', dir === 'rtl' ? 'right-2' : 'left-2', 'top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground')} />
@@ -130,6 +135,16 @@ export default function Cores({ isDialogOpen, onOpenChange, cores, onEditCore, o
130135
</button>
131136
)}
132137
</div>
138+
<Button
139+
size="icon-md"
140+
variant="ghost"
141+
onClick={handleRefreshClick}
142+
className={cn('border', isFetching && 'opacity-70')}
143+
aria-label={t('autoRefresh.refreshNow')}
144+
title={t('autoRefresh.refreshNow')}
145+
>
146+
<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')} />
147+
</Button>
133148
</div>
134149
<ScrollArea dir={dir} className="h-[calc(100vh-8rem)]">
135150
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">

dashboard/src/pages/_dashboard.hosts.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { toast } from 'sonner'
1212
export default function HostsPage() {
1313
const [isDialogOpen, setIsDialogOpen] = useState(false)
1414
const [editingHost, setEditingHost] = useState<BaseHost | null>(null)
15-
const { data, isLoading } = useQuery({
15+
const { data, isLoading, refetch, isFetching } = useQuery({
1616
queryKey: ['getGetHostsQueryKey'],
1717
queryFn: () => getHosts(),
1818
})
@@ -247,6 +247,8 @@ export default function HostsPage() {
247247
onSubmit={handleSubmit}
248248
editingHost={editingHost}
249249
setEditingHost={setEditingHost}
250+
onRefresh={refetch}
251+
isRefreshing={isFetching}
250252
/>
251253
)}
252254
</div>

dashboard/src/pages/_dashboard.templates.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import UserTemplate from '../components/templates/user-template'
22
import { useGetUserTemplates, useModifyUserTemplate, UserTemplateResponse, ShadowsocksMethods, XTLSFlows } from '@/service/api'
33
import PageHeader from '@/components/layout/page-header'
4-
import { Plus } from 'lucide-react'
4+
import { Plus, RefreshCw } from 'lucide-react'
55
import { Separator } from '@/components/ui/separator.tsx'
66
import { Skeleton } from '@/components/ui/skeleton'
77
import { Card } from '@/components/ui/card'
@@ -14,6 +14,7 @@ import { queryClient } from '@/utils/query-client.ts'
1414
import { toast } from 'sonner'
1515
import { useTranslation } from 'react-i18next'
1616
import { Input } from '@/components/ui/input'
17+
import { Button } from '@/components/ui/button'
1718
import { Search, X } from 'lucide-react'
1819
import useDirDetection from '@/hooks/use-dir-detection'
1920
import { cn } from '@/lib/utils'
@@ -36,7 +37,7 @@ export default function UserTemplates() {
3637
const [isDialogOpen, setIsDialogOpen] = useState(false)
3738
const [editingUserTemplate, setEditingUserTemplate] = useState<UserTemplateResponse | null>(null)
3839
const [searchQuery, setSearchQuery] = useState('')
39-
const { data: userTemplates, isLoading } = useGetUserTemplates()
40+
const { data: userTemplates, isLoading, isFetching, refetch } = useGetUserTemplates()
4041
const form = useForm<UserTemplatesFromValue>({
4142
resolver: zodResolver(userTemplateFormSchema),
4243
})
@@ -113,6 +114,10 @@ export default function UserTemplates() {
113114
)
114115
}, [userTemplates, searchQuery])
115116

117+
const handleRefreshClick = async () => {
118+
await refetch()
119+
}
120+
116121
return (
117122
<div className="flex w-full flex-col items-start gap-2">
118123
<div className="w-full transform-gpu animate-fade-in" style={{ animationDuration: '400ms' }}>
@@ -140,6 +145,16 @@ export default function UserTemplates() {
140145
</button>
141146
)}
142147
</div>
148+
<Button
149+
size="icon-md"
150+
variant="ghost"
151+
onClick={handleRefreshClick}
152+
className={cn('border', isFetching && 'opacity-70')}
153+
aria-label={t('autoRefresh.refreshNow')}
154+
title={t('autoRefresh.refreshNow')}
155+
>
156+
<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')} />
157+
</Button>
143158
</div>
144159

145160
<div

0 commit comments

Comments
 (0)