Skip to content

Commit 0673128

Browse files
committed
feat: Enhance localization files with new host filtering options and descriptions
1 parent 6fad071 commit 0673128

File tree

8 files changed

+585
-52
lines changed

8 files changed

+585
-52
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1991,6 +1991,14 @@
19911991
},
19921992
"clearAllFilters": "Clear All Filters",
19931993
"activeFilters": "active",
1994+
"all": "All",
1995+
"hosts.filters.userStatus": "User Status Rules",
1996+
"hosts.filters.userStatusDescription": "Matches hosts by configured user statuses (active, on-hold, limited, etc).",
1997+
"hosts.filters.clearAllInbounds": "Clear all inbounds",
1998+
"hosts.filters.hostState": "Host State",
1999+
"hosts.filters.hostStateDescription": "Filters whether the host itself is enabled or disabled.",
2000+
"hosts.filters.enabledHosts": "Enabled Hosts",
2001+
"hosts.filters.disabledHosts": "Disabled Hosts",
19942002
"donation": {
19952003
"title": "Support PasarGuard",
19962004
"message": "Your support helps us improve PasarGuard and build better features for everyone!",

dashboard/public/statics/locales/fa.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,14 @@
19641964
},
19651965
"clearAllFilters": "پاک کردن همه فیلترها",
19661966
"activeFilters": "فعال",
1967+
"all": "همه",
1968+
"hosts.filters.userStatus": "قوانین وضعیت کاربر",
1969+
"hosts.filters.userStatusDescription": "هاست‌ها را بر اساس وضعیت‌های کاربرِ پیکربندی‌شده فیلتر می‌کند (فعال، در انتظار، محدود و ...).",
1970+
"hosts.filters.clearAllInbounds": "پاک کردن همه اینباندها",
1971+
"hosts.filters.hostState": "وضعیت هاست",
1972+
"hosts.filters.hostStateDescription": "مشخص می‌کند خود هاست فعال است یا غیرفعال.",
1973+
"hosts.filters.enabledHosts": "هاست‌های فعال",
1974+
"hosts.filters.disabledHosts": "هاست‌های غیرفعال",
19671975
"donation": {
19681976
"title": "حمایت از پاسارگارد",
19691977
"message": "حمایت شما به ما کمک می‌کند تا پاسارگارد را بهبود بخشیم و ویژگی‌های بهتری برای همه بسازیم!",

dashboard/public/statics/locales/ru.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,6 +1932,15 @@
19321932
},
19331933
"clearAllFilters": "Очистить все фильтры",
19341934
"activeFilters": "активных",
1935+
"loading": "Загрузка...",
1936+
"all": "Все",
1937+
"hosts.filters.userStatus": "Правила статуса пользователя",
1938+
"hosts.filters.userStatusDescription": "Фильтрует хосты по заданным статусам пользователей (активен, на паузе, ограничен и т.д.).",
1939+
"hosts.filters.clearAllInbounds": "Очистить все входящие",
1940+
"hosts.filters.hostState": "Состояние хоста",
1941+
"hosts.filters.hostStateDescription": "Фильтрует, включен или отключен сам хост.",
1942+
"hosts.filters.enabledHosts": "Включенные хосты",
1943+
"hosts.filters.disabledHosts": "Отключенные хосты",
19351944
"donation": {
19361945
"title": "Поддержите PasarGuard",
19371946
"message": "Ваша поддержка помогает нам улучшать PasarGuard и создавать лучшие функции для всех!",

dashboard/public/statics/locales/zh.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,6 +1986,14 @@
19861986
},
19871987
"clearAllFilters": "清除所有筛选器",
19881988
"activeFilters": "活跃",
1989+
"all": "全部",
1990+
"hosts.filters.userStatus": "用户状态规则",
1991+
"hosts.filters.userStatusDescription": "按已配置的用户状态筛选主机(活跃、暂停、受限等)。",
1992+
"hosts.filters.clearAllInbounds": "清除所有入站",
1993+
"hosts.filters.hostState": "主机状态",
1994+
"hosts.filters.hostStateDescription": "筛选主机本身是启用还是禁用。",
1995+
"hosts.filters.enabledHosts": "已启用主机",
1996+
"hosts.filters.disabledHosts": "已禁用主机",
19891997
"donation": {
19901998
"title": "支持 PasarGuard",
19911999
"message": "您的支持帮助我们改进 PasarGuard 并为所有人构建更好的功能!",
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { Badge } from '@/components/ui/badge'
2+
import { Button } from '@/components/ui/button'
3+
import { Checkbox } from '@/components/ui/checkbox'
4+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
5+
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
6+
import { LoaderButton } from '@/components/ui/loader-button'
7+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
8+
import type { HostAdvanceSearchFormValues } from '@/components/forms/host-advance-search-form'
9+
import useDirDetection from '@/hooks/use-dir-detection'
10+
import { ProxyHostSecurity, UserStatus } from '@/service/api'
11+
import { X } from 'lucide-react'
12+
import type { UseFormReturn } from 'react-hook-form'
13+
import { useTranslation } from 'react-i18next'
14+
15+
interface HostAdvanceSearchModalProps {
16+
isDialogOpen: boolean
17+
onOpenChange: (open: boolean) => void
18+
form: UseFormReturn<HostAdvanceSearchFormValues>
19+
onSubmit: (values: HostAdvanceSearchFormValues) => void
20+
inbounds: string[]
21+
isLoadingInbounds?: boolean
22+
}
23+
24+
const statusOptions = [
25+
{ value: UserStatus.active, label: 'hostsDialog.status.active' },
26+
{ value: UserStatus.disabled, label: 'hostsDialog.status.disabled' },
27+
{ value: UserStatus.limited, label: 'hostsDialog.status.limited' },
28+
{ value: UserStatus.expired, label: 'hostsDialog.status.expired' },
29+
{ value: UserStatus.on_hold, label: 'hostsDialog.status.onHold' },
30+
] as const
31+
32+
const securityOptions = [
33+
{ value: ProxyHostSecurity.inbound_default, label: 'hostsDialog.inboundDefault' },
34+
{ value: ProxyHostSecurity.tls, label: 'tls' },
35+
{ value: ProxyHostSecurity.none, label: 'none' },
36+
] as const
37+
38+
export default function HostAdvanceSearchModal({ isDialogOpen, onOpenChange, form, onSubmit, inbounds, isLoadingInbounds }: HostAdvanceSearchModalProps) {
39+
const { t } = useTranslation()
40+
const dir = useDirDetection()
41+
42+
return (
43+
<Dialog open={isDialogOpen} onOpenChange={onOpenChange}>
44+
<DialogContent className="flex h-auto max-w-[650px] flex-col justify-start" onOpenAutoFocus={e => e.preventDefault()}>
45+
<DialogHeader>
46+
<DialogTitle className={dir === 'rtl' ? 'text-right' : 'text-left'} dir={dir}>
47+
{t('advanceSearch.title')}
48+
</DialogTitle>
49+
</DialogHeader>
50+
51+
<Form {...form}>
52+
<form onSubmit={form.handleSubmit(onSubmit)} className="flex h-full flex-col justify-between space-y-4">
53+
<div className="-mr-4 max-h-[80dvh] overflow-y-auto px-2 pr-4 sm:max-h-[75dvh]">
54+
<div className="flex w-full flex-1 flex-col items-start gap-4 pb-4">
55+
<FormField
56+
control={form.control}
57+
name="status"
58+
render={({ field }) => (
59+
<FormItem className="w-full">
60+
<FormLabel>{t('hosts.filters.userStatus', { defaultValue: 'User Status Rules' })}</FormLabel>
61+
<FormDescription>
62+
{t('hosts.filters.userStatusDescription', {
63+
defaultValue: 'Matches hosts by configured user statuses (active, on-hold, limited, etc).',
64+
})}
65+
</FormDescription>
66+
<FormControl>
67+
<>
68+
{field.value && field.value.length > 0 && (
69+
<div className="mb-2 flex flex-wrap gap-2">
70+
{field.value.map(status => {
71+
const option = statusOptions.find(item => item.value === status)
72+
if (!option) return null
73+
return (
74+
<Badge key={status} variant="secondary" className="flex items-center gap-1">
75+
{t(option.label)}
76+
<X
77+
className="h-3 w-3 cursor-pointer"
78+
onClick={() => {
79+
field.onChange(field.value?.filter(item => item !== status))
80+
}}
81+
/>
82+
</Badge>
83+
)
84+
})}
85+
</div>
86+
)}
87+
88+
<Select
89+
value=""
90+
onValueChange={(value: UserStatus) => {
91+
if (!value) return
92+
const current = field.value || []
93+
if (!current.includes(value)) {
94+
field.onChange([...current, value])
95+
}
96+
}}
97+
>
98+
<SelectTrigger dir={dir} className="w-full gap-2 py-2">
99+
<SelectValue placeholder={t('hostsDialog.selectStatus')} />
100+
</SelectTrigger>
101+
<SelectContent dir={dir} className="bg-background">
102+
{statusOptions.map(option => (
103+
<SelectItem key={option.value} value={option.value} className="flex cursor-pointer items-center gap-2 px-4 py-2 focus:bg-accent" disabled={field.value?.includes(option.value)}>
104+
<div className="flex w-full items-center gap-3">
105+
<Checkbox checked={field.value?.includes(option.value)} className="h-4 w-4" />
106+
<span className="text-sm font-normal">{t(option.label)}</span>
107+
</div>
108+
</SelectItem>
109+
))}
110+
</SelectContent>
111+
</Select>
112+
113+
{field.value && field.value.length > 0 && (
114+
<Button type="button" variant="outline" size="sm" onClick={() => field.onChange([])} className="mt-2 w-full">
115+
{t('hostsDialog.clearAllStatuses')}
116+
</Button>
117+
)}
118+
</>
119+
</FormControl>
120+
<FormMessage />
121+
</FormItem>
122+
)}
123+
/>
124+
125+
<FormField
126+
control={form.control}
127+
name="inbound_tags"
128+
render={({ field }) => (
129+
<FormItem className="w-full">
130+
<FormLabel>{t('inbound')}</FormLabel>
131+
<FormControl>
132+
<>
133+
{field.value && field.value.length > 0 && (
134+
<div className="mb-2 flex flex-wrap gap-2">
135+
{field.value.map(tag => (
136+
<Badge key={tag} variant="secondary" className="flex items-center gap-1">
137+
{tag}
138+
<X
139+
className="h-3 w-3 cursor-pointer"
140+
onClick={() => {
141+
field.onChange(field.value?.filter(item => item !== tag))
142+
}}
143+
/>
144+
</Badge>
145+
))}
146+
</div>
147+
)}
148+
149+
<Select
150+
value=""
151+
onValueChange={(value: string) => {
152+
if (!value) return
153+
const current = field.value || []
154+
if (!current.includes(value)) {
155+
field.onChange([...current, value])
156+
}
157+
}}
158+
>
159+
<SelectTrigger dir={dir}>
160+
<SelectValue placeholder={t('hostsDialog.selectInbound')} />
161+
</SelectTrigger>
162+
<SelectContent dir={dir} className="bg-background">
163+
{isLoadingInbounds ? (
164+
<SelectItem value="__loading_inbounds__" disabled>
165+
{t('loading', { defaultValue: 'Loading...' })}
166+
</SelectItem>
167+
) : inbounds.length > 0 ? (
168+
inbounds.map(tag => (
169+
<SelectItem key={tag} value={tag} disabled={field.value?.includes(tag)}>
170+
<div className="flex w-full items-center gap-3">
171+
<Checkbox checked={field.value?.includes(tag)} className="h-4 w-4" />
172+
<span className="text-sm font-normal">{tag}</span>
173+
</div>
174+
</SelectItem>
175+
))
176+
) : (
177+
<SelectItem value="__no_inbounds__" disabled>
178+
{t('noInboundsFound', { defaultValue: 'No inbounds found' })}
179+
</SelectItem>
180+
)}
181+
</SelectContent>
182+
</Select>
183+
184+
{field.value && field.value.length > 0 && (
185+
<Button type="button" variant="outline" size="sm" onClick={() => field.onChange([])} className="mt-2 w-full">
186+
{t('hosts.filters.clearAllInbounds', { defaultValue: 'Clear all inbounds' })}
187+
</Button>
188+
)}
189+
</>
190+
</FormControl>
191+
<FormMessage />
192+
</FormItem>
193+
)}
194+
/>
195+
196+
<FormField
197+
control={form.control}
198+
name="security"
199+
render={({ field }) => (
200+
<FormItem className="w-full">
201+
<FormLabel>{t('hostsDialog.security', { defaultValue: 'Security' })}</FormLabel>
202+
<FormControl>
203+
<Select
204+
value={field.value || '__all__'}
205+
onValueChange={value => {
206+
field.onChange(value === '__all__' ? undefined : (value as ProxyHostSecurity))
207+
}}
208+
>
209+
<SelectTrigger dir={dir}>
210+
<SelectValue placeholder={t('hostsDialog.security', { defaultValue: 'Security' })} />
211+
</SelectTrigger>
212+
<SelectContent dir={dir} className="bg-background">
213+
<SelectItem value="__all__">{t('all', { defaultValue: 'All' })}</SelectItem>
214+
{securityOptions.map(option => (
215+
<SelectItem key={option.value} value={option.value}>
216+
{t(option.label, { defaultValue: option.label.toUpperCase() })}
217+
</SelectItem>
218+
))}
219+
</SelectContent>
220+
</Select>
221+
</FormControl>
222+
<FormMessage />
223+
</FormItem>
224+
)}
225+
/>
226+
227+
<FormField
228+
control={form.control}
229+
name="is_disabled"
230+
render={({ field }) => (
231+
<FormItem className="w-full">
232+
<FormLabel>{t('hosts.filters.hostState', { defaultValue: 'Host State' })}</FormLabel>
233+
<FormDescription>
234+
{t('hosts.filters.hostStateDescription', {
235+
defaultValue: 'Filters whether the host itself is enabled or disabled.',
236+
})}
237+
</FormDescription>
238+
<FormControl>
239+
<Select
240+
value={field.value === undefined || field.value === null ? '__all__' : field.value ? 'disabled' : 'enabled'}
241+
onValueChange={value => {
242+
if (value === '__all__') {
243+
field.onChange(undefined)
244+
} else {
245+
field.onChange(value === 'disabled')
246+
}
247+
}}
248+
>
249+
<SelectTrigger dir={dir}>
250+
<SelectValue placeholder={t('hosts.filters.hostState', { defaultValue: 'Host State' })} />
251+
</SelectTrigger>
252+
<SelectContent dir={dir} className="bg-background">
253+
<SelectItem value="__all__">{t('all', { defaultValue: 'All' })}</SelectItem>
254+
<SelectItem value="enabled">{t('hosts.filters.enabledHosts', { defaultValue: 'Enabled Hosts' })}</SelectItem>
255+
<SelectItem value="disabled">{t('hosts.filters.disabledHosts', { defaultValue: 'Disabled Hosts' })}</SelectItem>
256+
</SelectContent>
257+
</Select>
258+
</FormControl>
259+
<FormMessage />
260+
</FormItem>
261+
)}
262+
/>
263+
</div>
264+
</div>
265+
266+
<div className="flex justify-end gap-2">
267+
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
268+
{t('cancel')}
269+
</Button>
270+
<LoaderButton type="submit">{t('apply')}</LoaderButton>
271+
</div>
272+
</form>
273+
</Form>
274+
</DialogContent>
275+
</Dialog>
276+
)
277+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ProxyHostSecurity, UserStatus } from '@/service/api'
2+
import { z } from 'zod'
3+
4+
export const hostAdvanceSearchFormSchema = z.object({
5+
status: z.array(z.nativeEnum(UserStatus)).optional(),
6+
inbound_tags: z.array(z.string()).optional(),
7+
security: z.nativeEnum(ProxyHostSecurity).optional().nullable(),
8+
is_disabled: z.boolean().optional().nullable(),
9+
})
10+
11+
export type HostAdvanceSearchFormValues = z.infer<typeof hostAdvanceSearchFormSchema>

0 commit comments

Comments
 (0)