mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 18:36:37 +08:00
prettier
This commit is contained in:
@@ -53,8 +53,16 @@ interface AuthSectionProps {
|
||||
onSavingChange?: (saving: boolean) => void;
|
||||
}
|
||||
|
||||
export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
|
||||
export function AuthSection({
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
}: AuthSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
);
|
||||
|
||||
const form = useForm<AuthValues>({
|
||||
resolver: zodResolver(authSchema),
|
||||
@@ -77,24 +85,28 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="认证配置"
|
||||
description="配置 Basic Auth 限制未授权访问。PoW 防护请在 WAF 规则组中配置。"
|
||||
title='认证配置'
|
||||
description='配置 Basic Auth 限制未授权访问。PoW 防护请在 WAF 规则组中配置。'
|
||||
formId={proxyRouteFormIds.auth}
|
||||
saving={saving}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={proxyRouteFormIds.auth}
|
||||
className="space-y-5"
|
||||
className='space-y-5'
|
||||
onSubmit={form.handleSubmit(
|
||||
async (values) => {
|
||||
await save(
|
||||
{
|
||||
basic_auth_enabled: values.auth_mode === 'basic',
|
||||
basic_auth_username:
|
||||
values.auth_mode === 'basic' ? values.basic_auth_username.trim() : '',
|
||||
values.auth_mode === 'basic'
|
||||
? values.basic_auth_username.trim()
|
||||
: '',
|
||||
basic_auth_password:
|
||||
values.auth_mode === 'basic' ? values.basic_auth_password.trim() : '',
|
||||
values.auth_mode === 'basic'
|
||||
? values.basic_auth_password.trim()
|
||||
: '',
|
||||
},
|
||||
'认证配置已保存',
|
||||
);
|
||||
@@ -106,7 +118,7 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="auth_mode"
|
||||
name='auth_mode'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>认证模式</FormLabel>
|
||||
@@ -126,11 +138,13 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">无认证</SelectItem>
|
||||
<SelectItem value="basic">Basic Auth</SelectItem>
|
||||
<SelectItem value='none'>无认证</SelectItem>
|
||||
<SelectItem value='basic'>Basic Auth</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>PoW 防护仅支持在 WAF 规则组中启用并绑定站点。</FormDescription>
|
||||
<FormDescription>
|
||||
PoW 防护仅支持在 WAF 规则组中启用并绑定站点。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -140,12 +154,12 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic_auth_username"
|
||||
name='basic_auth_username'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>账号</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="admin" {...field} />
|
||||
<Input placeholder='admin' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -154,12 +168,12 @@ export function AuthSection({ route, onRouteUpdate, onSavingChange }: AuthSectio
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic_auth_password"
|
||||
name='basic_auth_password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="secret123" {...field} />
|
||||
<Input type='text' placeholder='secret123' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -5,27 +5,13 @@ import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {z} from 'zod';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@/components/ui/form';
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
|
||||
import {Switch} from '@/components/ui/switch';
|
||||
import {Textarea} from '@/components/ui/textarea';
|
||||
import type {ProxyRouteItem} from '@/lib/services/openflare';
|
||||
|
||||
import {linesFromTextarea, validateCacheRules} from '../../components/helpers';
|
||||
import {linesFromTextarea, validateCacheRules,} from '../../components/helpers';
|
||||
import {proxyRouteFormIds} from '../helpers';
|
||||
import {useRouteSectionSave} from '../hooks/use-route-section-save';
|
||||
import {SectionShell} from './section-shell';
|
||||
@@ -60,14 +46,23 @@ interface CacheSectionProps {
|
||||
onSavingChange?: (saving: boolean) => void;
|
||||
}
|
||||
|
||||
export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
|
||||
export function CacheSection({
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
}: CacheSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
);
|
||||
|
||||
const form = useForm<CacheValues>({
|
||||
resolver: zodResolver(cacheSchema),
|
||||
defaultValues: {
|
||||
cache_enabled: route.cache_enabled,
|
||||
cache_policy: (route.cache_policy || 'url') as CacheValues['cache_policy'],
|
||||
cache_policy: (route.cache_policy ||
|
||||
'url') as CacheValues['cache_policy'],
|
||||
cache_rules_text: route.cache_rule_list.join('\n'),
|
||||
},
|
||||
});
|
||||
@@ -75,7 +70,8 @@ export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSect
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
cache_enabled: route.cache_enabled,
|
||||
cache_policy: (route.cache_policy || 'url') as CacheValues['cache_policy'],
|
||||
cache_policy: (route.cache_policy ||
|
||||
'url') as CacheValues['cache_policy'],
|
||||
cache_rules_text: route.cache_rule_list.join('\n'),
|
||||
});
|
||||
}, [form, route]);
|
||||
@@ -103,23 +99,27 @@ export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSect
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="缓存"
|
||||
description="保留现有安全绕过逻辑,只对当前站点生效。"
|
||||
title='缓存'
|
||||
description='保留现有安全绕过逻辑,只对当前站点生效。'
|
||||
formId={proxyRouteFormIds.cache}
|
||||
saving={saving}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={proxyRouteFormIds.cache}
|
||||
className="space-y-5"
|
||||
className='space-y-5'
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
const rules = linesFromTextarea(values.cache_rules_text);
|
||||
await save(
|
||||
{
|
||||
cache_enabled: values.cache_enabled,
|
||||
cache_policy: values.cache_enabled ? values.cache_policy : 'url',
|
||||
cache_policy: values.cache_enabled
|
||||
? values.cache_policy
|
||||
: 'url',
|
||||
cache_rules:
|
||||
values.cache_enabled && values.cache_policy !== 'url' ? rules : [],
|
||||
values.cache_enabled && values.cache_policy !== 'url'
|
||||
? rules
|
||||
: [],
|
||||
},
|
||||
'缓存设置已保存',
|
||||
);
|
||||
@@ -127,17 +127,21 @@ export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSect
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cache_enabled"
|
||||
name='cache_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormItem className='flex items-center justify-between rounded-lg border p-3'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel>启用站点缓存</FormLabel>
|
||||
<FormDescription>
|
||||
系统仍会自动绕过非 GET、带 Authorization 或常见登录态 Cookie 的请求。
|
||||
系统仍会自动绕过非 GET、带 Authorization 或常见登录态 Cookie
|
||||
的请求。
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -145,7 +149,7 @@ export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSect
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cache_policy"
|
||||
name='cache_policy'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>缓存策略</FormLabel>
|
||||
@@ -160,10 +164,10 @@ export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSect
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="url">按 URL 缓存</SelectItem>
|
||||
<SelectItem value="suffix">按后缀缓存</SelectItem>
|
||||
<SelectItem value="path_prefix">按路径前缀缓存</SelectItem>
|
||||
<SelectItem value="path_exact">按精确路径缓存</SelectItem>
|
||||
<SelectItem value='url'>按 URL 缓存</SelectItem>
|
||||
<SelectItem value='suffix'>按后缀缓存</SelectItem>
|
||||
<SelectItem value='path_prefix'>按路径前缀缓存</SelectItem>
|
||||
<SelectItem value='path_exact'>按精确路径缓存</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -173,13 +177,13 @@ export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSect
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cache_rules_text"
|
||||
name='cache_rules_text'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>缓存规则</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="min-h-32"
|
||||
className='min-h-32'
|
||||
disabled={!watchedEnabled || watchedPolicy === 'url'}
|
||||
placeholder={rulesPlaceholder}
|
||||
{...field}
|
||||
@@ -194,4 +198,4 @@ export function CacheSection({ route, onRouteUpdate, onSavingChange }: CacheSect
|
||||
</Form>
|
||||
</SectionShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, Form
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Switch} from '@/components/ui/switch';
|
||||
import type {ProxyRouteItem} from '@/lib/services/openflare';
|
||||
import {ZoneService, zoneQueryKey} from '@/lib/services/openflare';
|
||||
import {zoneQueryKey, ZoneService} from '@/lib/services/openflare';
|
||||
|
||||
import {listAllZoneDomains} from '../../components/helpers';
|
||||
import {ZoneDomainSelector} from '../../components/zone-domain-selector';
|
||||
@@ -18,17 +18,18 @@ import {proxyRouteFormIds} from '../helpers';
|
||||
import {useRouteSectionSave} from '../hooks/use-route-section-save';
|
||||
import {SectionShell} from './section-shell';
|
||||
|
||||
const domainSettingsSchema = z
|
||||
.object({
|
||||
site_name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, '请输入站点标识')
|
||||
.max(255, '站点标识不能超过 255 个字符'),
|
||||
zone_domain_ids: z.array(z.number().int().positive()).min(1, '请至少选择一个域名'),
|
||||
enabled: z.boolean(),
|
||||
redirect_http: z.boolean(),
|
||||
});
|
||||
const domainSettingsSchema = z.object({
|
||||
site_name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, '请输入站点标识')
|
||||
.max(255, '站点标识不能超过 255 个字符'),
|
||||
zone_domain_ids: z
|
||||
.array(z.number().int().positive())
|
||||
.min(1, '请至少选择一个域名'),
|
||||
enabled: z.boolean(),
|
||||
redirect_http: z.boolean(),
|
||||
});
|
||||
|
||||
type DomainSettingsValues = z.infer<typeof domainSettingsSchema>;
|
||||
|
||||
@@ -38,8 +39,16 @@ interface DomainSectionProps {
|
||||
onSavingChange?: (saving: boolean) => void;
|
||||
}
|
||||
|
||||
export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
|
||||
export function DomainSection({
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
}: DomainSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
);
|
||||
|
||||
const zonesQuery = useQuery({
|
||||
queryKey: zoneQueryKey,
|
||||
@@ -98,23 +107,26 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
.filter((item): item is NonNullable<typeof item> => item != null);
|
||||
}, [domainsQuery.data, route.id, route.zone_domains, selectedIDs]);
|
||||
|
||||
const hasCertificate = selectedDomains.some((domain) => domain.cert_id != null);
|
||||
const hasCertificate = selectedDomains.some(
|
||||
(domain) => domain.cert_id != null,
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="域名设置"
|
||||
description="绑定已在 Zone 中注册的 FQDN。证书请在 Zone 域名管理中维护。"
|
||||
title='域名设置'
|
||||
description='绑定已在 Zone 中注册的 FQDN。证书请在 Zone 域名管理中维护。'
|
||||
formId={proxyRouteFormIds.domains}
|
||||
saving={saving}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={proxyRouteFormIds.domains}
|
||||
className="space-y-5"
|
||||
className='space-y-5'
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
if (values.redirect_http && !hasCertificate) {
|
||||
form.setError('redirect_http', {
|
||||
message: '启用 HTTP 跳转前,请先为所选域名绑定证书(在 Zone 中配置)',
|
||||
message:
|
||||
'启用 HTTP 跳转前,请先为所选域名绑定证书(在 Zone 中配置)',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -133,15 +145,20 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
name='enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormItem className='flex items-center justify-between rounded-lg border p-3'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel>启用站点</FormLabel>
|
||||
<FormDescription>关闭后会保留配置,但不会参与发布。</FormDescription>
|
||||
<FormDescription>
|
||||
关闭后会保留配置,但不会参与发布。
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -149,14 +166,16 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="site_name"
|
||||
name='site_name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>站点标识</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="marketing-site" {...field} />
|
||||
<Input placeholder='marketing-site' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>建议使用稳定、可读的业务标识,不必与域名完全一致。</FormDescription>
|
||||
<FormDescription>
|
||||
建议使用稳定、可读的业务标识,不必与域名完全一致。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -164,7 +183,7 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="zone_domain_ids"
|
||||
name='zone_domain_ids'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>绑定域名</FormLabel>
|
||||
@@ -182,8 +201,8 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
从已登记域名中勾选绑定;可用「快捷新增域名」在 Zone 下创建 FQDN
|
||||
并自动勾选。
|
||||
从已登记域名中勾选绑定;可用「快捷新增域名」在 Zone 下创建
|
||||
FQDN 并自动勾选。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -192,10 +211,10 @@ export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSe
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="redirect_http"
|
||||
name='redirect_http'
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormItem className='flex items-center justify-between rounded-lg border p-3'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel>HTTP 自动跳转到 HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
{hasCertificate
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {z} from 'zod';
|
||||
import { useEffect } from 'react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@/components/ui/form';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import type {ProxyRouteItem} from '@/lib/services/openflare';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { ProxyRouteItem } from '@/lib/services/openflare';
|
||||
|
||||
import {normalizeLimitRate, validateLimitRate} from '../../components/helpers';
|
||||
import {proxyRouteFormIds} from '../helpers';
|
||||
import {useRouteSectionSave} from '../hooks/use-route-section-save';
|
||||
import {SectionShell} from './section-shell';
|
||||
import {
|
||||
normalizeLimitRate,
|
||||
validateLimitRate,
|
||||
} from '../../components/helpers';
|
||||
import { proxyRouteFormIds } from '../helpers';
|
||||
import { useRouteSectionSave } from '../hooks/use-route-section-save';
|
||||
import { SectionShell } from './section-shell';
|
||||
|
||||
const rateLimitSchema = z
|
||||
.object({
|
||||
@@ -21,7 +32,10 @@ const rateLimitSchema = z
|
||||
limit_rate: z.string(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
for (const field of ['limit_conn_per_server', 'limit_conn_per_ip'] as const) {
|
||||
for (const field of [
|
||||
'limit_conn_per_server',
|
||||
'limit_conn_per_ip',
|
||||
] as const) {
|
||||
const rawValue = value[field].trim();
|
||||
if (!rawValue) {
|
||||
continue;
|
||||
@@ -53,8 +67,16 @@ interface LimitsSectionProps {
|
||||
onSavingChange?: (saving: boolean) => void;
|
||||
}
|
||||
|
||||
export function LimitsSection({ route, onRouteUpdate, onSavingChange }: LimitsSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
|
||||
export function LimitsSection({
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
}: LimitsSectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
);
|
||||
|
||||
const form = useForm<RateLimitValues>({
|
||||
resolver: zodResolver(rateLimitSchema),
|
||||
@@ -62,7 +84,9 @@ export function LimitsSection({ route, onRouteUpdate, onSavingChange }: LimitsSe
|
||||
limit_conn_per_server: route.limit_conn_per_server
|
||||
? String(route.limit_conn_per_server)
|
||||
: '',
|
||||
limit_conn_per_ip: route.limit_conn_per_ip ? String(route.limit_conn_per_ip) : '',
|
||||
limit_conn_per_ip: route.limit_conn_per_ip
|
||||
? String(route.limit_conn_per_ip)
|
||||
: '',
|
||||
limit_rate: route.limit_rate || '',
|
||||
},
|
||||
});
|
||||
@@ -72,27 +96,33 @@ export function LimitsSection({ route, onRouteUpdate, onSavingChange }: LimitsSe
|
||||
limit_conn_per_server: route.limit_conn_per_server
|
||||
? String(route.limit_conn_per_server)
|
||||
: '',
|
||||
limit_conn_per_ip: route.limit_conn_per_ip ? String(route.limit_conn_per_ip) : '',
|
||||
limit_conn_per_ip: route.limit_conn_per_ip
|
||||
? String(route.limit_conn_per_ip)
|
||||
: '',
|
||||
limit_rate: route.limit_rate || '',
|
||||
});
|
||||
}, [form, route]);
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="流量限制"
|
||||
description="站点限流,空值或 0 表示关闭。"
|
||||
title='流量限制'
|
||||
description='站点限流,空值或 0 表示关闭。'
|
||||
formId={proxyRouteFormIds.limits}
|
||||
saving={saving}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={proxyRouteFormIds.limits}
|
||||
className="grid gap-5 md:grid-cols-2"
|
||||
className='grid gap-5 md:grid-cols-2'
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
await save(
|
||||
{
|
||||
limit_conn_per_server: Number(values.limit_conn_per_server.trim() || '0'),
|
||||
limit_conn_per_ip: Number(values.limit_conn_per_ip.trim() || '0'),
|
||||
limit_conn_per_server: Number(
|
||||
values.limit_conn_per_server.trim() || '0',
|
||||
),
|
||||
limit_conn_per_ip: Number(
|
||||
values.limit_conn_per_ip.trim() || '0',
|
||||
),
|
||||
limit_rate: normalizeLimitRate(values.limit_rate),
|
||||
},
|
||||
'流量限制已保存',
|
||||
@@ -101,12 +131,12 @@ export function LimitsSection({ route, onRouteUpdate, onSavingChange }: LimitsSe
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="limit_conn_per_server"
|
||||
name='limit_conn_per_server'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>并发限制</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="120" {...field} />
|
||||
<Input placeholder='120' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>限制当前站点最大并发连接数。</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -116,12 +146,12 @@ export function LimitsSection({ route, onRouteUpdate, onSavingChange }: LimitsSe
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="limit_conn_per_ip"
|
||||
name='limit_conn_per_ip'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>单 IP 限制</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="12" {...field} />
|
||||
<Input placeholder='12' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>限制单个 IP 的最大并发数。</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -131,14 +161,16 @@ export function LimitsSection({ route, onRouteUpdate, onSavingChange }: LimitsSe
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="limit_rate"
|
||||
name='limit_rate'
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<FormItem className='md:col-span-2'>
|
||||
<FormLabel>限速</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="512k/1m" {...field} />
|
||||
<Input placeholder='512k/1m' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>限制单请求带宽,例如 512k 或 1m。</FormDescription>
|
||||
<FormDescription>
|
||||
限制单请求带宽,例如 512k 或 1m。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {z} from 'zod';
|
||||
import { useEffect } from 'react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@/components/ui/form';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
|
||||
import {Textarea} from '@/components/ui/textarea';
|
||||
import type {ProxyRouteItem} from '@/lib/services/openflare';
|
||||
import {NodeService, PagesService} from '@/lib/services/openflare';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { ProxyRouteItem } from '@/lib/services/openflare';
|
||||
import { NodeService, PagesService } from '@/lib/services/openflare';
|
||||
|
||||
import {
|
||||
customHeadersToText,
|
||||
@@ -21,9 +35,9 @@ import {
|
||||
parseOriginUrls,
|
||||
validateOriginHost,
|
||||
} from '../../components/helpers';
|
||||
import {proxyRouteFormIds} from '../helpers';
|
||||
import {useRouteSectionSave} from '../hooks/use-route-section-save';
|
||||
import {SectionShell} from './section-shell';
|
||||
import { proxyRouteFormIds } from '../helpers';
|
||||
import { useRouteSectionSave } from '../hooks/use-route-section-save';
|
||||
import { SectionShell } from './section-shell';
|
||||
|
||||
const reverseProxySchema = z
|
||||
.object({
|
||||
@@ -86,7 +100,9 @@ const reverseProxySchema = z
|
||||
});
|
||||
}
|
||||
|
||||
const { error: headerError } = parseCustomHeadersText(value.custom_headers_text);
|
||||
const { error: headerError } = parseCustomHeadersText(
|
||||
value.custom_headers_text,
|
||||
);
|
||||
if (headerError) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@@ -104,8 +120,16 @@ interface ProxySectionProps {
|
||||
onSavingChange?: (saving: boolean) => void;
|
||||
}
|
||||
|
||||
export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
|
||||
export function ProxySection({
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
}: ProxySectionProps) {
|
||||
const { saving, save } = useRouteSectionSave(
|
||||
route,
|
||||
onRouteUpdate,
|
||||
onSavingChange,
|
||||
);
|
||||
|
||||
const tunnelsQuery = useQuery({
|
||||
queryKey: ['openflare', 'nodes'],
|
||||
@@ -134,7 +158,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
tunnel_target_addr: route.tunnel_target_addr || '',
|
||||
tunnel_target_protocol:
|
||||
(route.tunnel_target_protocol as 'http' | 'https') || 'http',
|
||||
pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '',
|
||||
pages_project_id: route.pages_project_id
|
||||
? String(route.pages_project_id)
|
||||
: '',
|
||||
custom_headers_text: customHeadersToText(route.custom_header_list),
|
||||
},
|
||||
});
|
||||
@@ -148,7 +174,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
tunnel_target_addr: route.tunnel_target_addr || '',
|
||||
tunnel_target_protocol:
|
||||
(route.tunnel_target_protocol as 'http' | 'https') || 'http',
|
||||
pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '',
|
||||
pages_project_id: route.pages_project_id
|
||||
? String(route.pages_project_id)
|
||||
: '',
|
||||
custom_headers_text: customHeadersToText(route.custom_header_list),
|
||||
});
|
||||
}, [form, route]);
|
||||
@@ -157,15 +185,15 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="反向代理"
|
||||
description="配置请求回源上游的策略与地址。"
|
||||
title='反向代理'
|
||||
description='配置请求回源上游的策略与地址。'
|
||||
formId={proxyRouteFormIds.proxy}
|
||||
saving={saving}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={proxyRouteFormIds.proxy}
|
||||
className="space-y-5"
|
||||
className='space-y-5'
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
let originUrl = '';
|
||||
let originScheme: 'http' | 'https' = 'http';
|
||||
@@ -194,7 +222,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
originPort = '80';
|
||||
}
|
||||
|
||||
const { headers } = parseCustomHeadersText(values.custom_headers_text);
|
||||
const { headers } = parseCustomHeadersText(
|
||||
values.custom_headers_text,
|
||||
);
|
||||
|
||||
await save(
|
||||
{
|
||||
@@ -213,9 +243,13 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
? Number(values.tunnel_id)
|
||||
: null,
|
||||
tunnel_target_addr:
|
||||
values.upstream_type === 'tunnel' ? values.tunnel_target_addr : '',
|
||||
values.upstream_type === 'tunnel'
|
||||
? values.tunnel_target_addr
|
||||
: '',
|
||||
tunnel_target_protocol:
|
||||
values.upstream_type === 'tunnel' ? values.tunnel_target_protocol : '',
|
||||
values.upstream_type === 'tunnel'
|
||||
? values.tunnel_target_protocol
|
||||
: '',
|
||||
pages_project_id:
|
||||
values.upstream_type === 'pages' && values.pages_project_id
|
||||
? Number(values.pages_project_id)
|
||||
@@ -227,11 +261,11 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="upstream_type"
|
||||
name='upstream_type'
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>回源方式</FormLabel>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className='flex flex-wrap gap-4'>
|
||||
{(
|
||||
[
|
||||
['direct', '直连上游'],
|
||||
@@ -239,15 +273,18 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
['pages', 'Pages 静态站点'],
|
||||
] as const
|
||||
).map(([value, label]) => (
|
||||
<label key={value} className="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<label
|
||||
key={value}
|
||||
className='flex cursor-pointer items-center gap-2 text-sm'
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
type='radio'
|
||||
value={value}
|
||||
checked={field.value === value}
|
||||
onChange={() => field.onChange(value)}
|
||||
className="size-4 accent-primary"
|
||||
className='size-4 accent-primary'
|
||||
/>
|
||||
<Label className="font-normal">{label}</Label>
|
||||
<Label className='font-normal'>{label}</Label>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -259,13 +296,13 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
{upstreamType === 'direct' ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="origin_urls_text"
|
||||
name='origin_urls_text'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>上游地址</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="min-h-40 font-mono text-xs"
|
||||
className='min-h-40 font-mono text-xs'
|
||||
placeholder={
|
||||
'https://origin-a.internal:443\nhttps://origin-b.internal:443'
|
||||
}
|
||||
@@ -273,7 +310,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
每行一个完整 URL。第一行作为主回源,多上游模式请保持相同协议且不要包含 path 或 query。
|
||||
每行一个完整
|
||||
URL。第一行作为主回源,多上游模式请保持相同协议且不要包含
|
||||
path 或 query。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -282,29 +321,37 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
) : null}
|
||||
|
||||
{upstreamType === 'tunnel' ? (
|
||||
<div className="space-y-4 rounded-lg border border-dashed bg-muted/30 p-4">
|
||||
<div className='space-y-4 rounded-lg border border-dashed bg-muted/30 p-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tunnel_id"
|
||||
name='tunnel_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>选择内网穿透隧道</FormLabel>
|
||||
<Select value={field.value || 'none'} onValueChange={(value) => field.onChange(value === 'none' ? '' : value)}>
|
||||
<Select
|
||||
value={field.value || 'none'}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'none' ? '' : value)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择..." />
|
||||
<SelectValue placeholder='请选择...' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">请选择...</SelectItem>
|
||||
<SelectItem value='none'>请选择...</SelectItem>
|
||||
{tunnelClients.map((tunnel) => (
|
||||
<SelectItem key={tunnel.id} value={String(tunnel.id)}>
|
||||
{tunnel.name} ({tunnel.status === 'online' ? '在线' : '离线'})
|
||||
{tunnel.name} (
|
||||
{tunnel.status === 'online' ? '在线' : '离线'})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>将请求转发到该隧道连接的客户端节点。</FormDescription>
|
||||
<FormDescription>
|
||||
将请求转发到该隧道连接的客户端节点。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -312,7 +359,7 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tunnel_target_protocol"
|
||||
name='tunnel_target_protocol'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>内网服务协议</FormLabel>
|
||||
@@ -323,8 +370,8 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">HTTP</SelectItem>
|
||||
<SelectItem value="https">HTTPS</SelectItem>
|
||||
<SelectItem value='http'>HTTP</SelectItem>
|
||||
<SelectItem value='https'>HTTPS</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -334,14 +381,16 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tunnel_target_addr"
|
||||
name='tunnel_target_addr'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>内网服务地址</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="127.0.0.1:8080" {...field} />
|
||||
<Input placeholder='127.0.0.1:8080' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>例如: 127.0.0.1:8080 或 192.168.1.10:80</FormDescription>
|
||||
<FormDescription>
|
||||
例如: 127.0.0.1:8080 或 192.168.1.10:80
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -350,29 +399,39 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
) : null}
|
||||
|
||||
{upstreamType === 'pages' ? (
|
||||
<div className="rounded-lg border border-dashed bg-muted/30 p-4">
|
||||
<div className='rounded-lg border border-dashed bg-muted/30 p-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pages_project_id"
|
||||
name='pages_project_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>选择 Pages 项目</FormLabel>
|
||||
<Select value={field.value || 'none'} onValueChange={(value) => field.onChange(value === 'none' ? '' : value)}>
|
||||
<Select
|
||||
value={field.value || 'none'}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'none' ? '' : value)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择..." />
|
||||
<SelectValue placeholder='请选择...' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">请选择...</SelectItem>
|
||||
<SelectItem value='none'>请选择...</SelectItem>
|
||||
{pagesProjects.map((project) => (
|
||||
<SelectItem key={project.id} value={String(project.id)}>
|
||||
<SelectItem
|
||||
key={project.id}
|
||||
value={String(project.id)}
|
||||
>
|
||||
{project.name} ({project.slug})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>仅显示已启用且已有激活部署的 Pages 项目。</FormDescription>
|
||||
<FormDescription>
|
||||
仅显示已启用且已有激活部署的 Pages 项目。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -382,14 +441,16 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="origin_host"
|
||||
name='origin_host'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Origin Host Header</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="origin.example.internal" {...field} />
|
||||
<Input placeholder='origin.example.internal' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>留空时默认透传访问域名 $host。</FormDescription>
|
||||
<FormDescription>
|
||||
留空时默认透传访问域名 $host。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -397,13 +458,13 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="custom_headers_text"
|
||||
name='custom_headers_text'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>自定义请求头</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="min-h-32 font-mono text-xs"
|
||||
className='min-h-32 font-mono text-xs'
|
||||
placeholder={'X-Trace-Id: $request_id\nX-Site: marketing'}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {useCallback, useState} from 'react';
|
||||
import {ArrowLeft, Loader2, Route, Upload} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ArrowLeft, Loader2, Route, Upload } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -14,12 +14,15 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import type {ConfigDiffResult, ProxyRouteItem} from '@/lib/services/openflare';
|
||||
import {ConfigVersionService} from '@/lib/services/openflare';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type {
|
||||
ConfigDiffResult,
|
||||
ProxyRouteItem,
|
||||
} from '@/lib/services/openflare';
|
||||
import { ConfigVersionService } from '@/lib/services/openflare';
|
||||
|
||||
import {getErrorMessage} from '../../components/helpers';
|
||||
import { getErrorMessage } from '../../components/helpers';
|
||||
|
||||
function hasConfigDiff(diff: ConfigDiffResult) {
|
||||
return (
|
||||
@@ -90,39 +93,41 @@ export function RouteHeader({ route }: RouteHeaderProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 px-0 text-xs"
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-8 gap-1.5 px-0 text-xs'
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft className="size-3.5" />
|
||||
<ArrowLeft className='size-3.5' />
|
||||
返回
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Route className="size-5 text-primary" />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{route.site_name}</h1>
|
||||
<div className='flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Route className='size-5 text-primary' />
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||
{route.site_name}
|
||||
</h1>
|
||||
<Badge variant={route.enabled ? 'default' : 'secondary'}>
|
||||
{route.enabled ? '已启用' : '已停用'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
size='sm'
|
||||
className='h-8 gap-1.5 text-xs'
|
||||
disabled={publishing || diffLoading}
|
||||
onClick={() => void handlePublishClick()}
|
||||
>
|
||||
{publishing || diffLoading ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
<Loader2 className='size-3.5 animate-spin' />
|
||||
) : (
|
||||
<Upload className="size-3.5" />
|
||||
<Upload className='size-3.5' />
|
||||
)}
|
||||
发布配置
|
||||
</Button>
|
||||
@@ -130,7 +135,10 @@ export function RouteHeader({ route }: RouteHeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={publishConfirmOpen} onOpenChange={setPublishConfirmOpen}>
|
||||
<AlertDialog
|
||||
open={publishConfirmOpen}
|
||||
onOpenChange={setPublishConfirmOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认发布配置</AlertDialogTitle>
|
||||
@@ -141,11 +149,15 @@ export function RouteHeader({ route }: RouteHeaderProps) {
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={publishing}>取消</AlertDialogCancel>
|
||||
<Button onClick={() => void handlePublish()} disabled={publishing}>
|
||||
{publishing ? <Loader2 className="size-4 animate-spin" /> : '确认发布'}
|
||||
{publishing ? (
|
||||
<Loader2 className='size-4 animate-spin' />
|
||||
) : (
|
||||
'确认发布'
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
interface SectionShellProps {
|
||||
title: string;
|
||||
@@ -22,16 +28,22 @@ export function SectionShell({
|
||||
}: SectionShellProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-4 space-y-0">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
|
||||
<CardHeader className='flex flex-row items-start justify-between gap-4 space-y-0'>
|
||||
<div className='space-y-1'>
|
||||
<CardTitle className='text-sm font-semibold'>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</div>
|
||||
<Button type="submit" form={formId} size="sm" className="h-8 shrink-0 text-xs" disabled={saving}>
|
||||
<Button
|
||||
type='submit'
|
||||
form={formId}
|
||||
size='sm'
|
||||
className='h-8 shrink-0 text-xs'
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
@@ -17,14 +17,14 @@ import {
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import {CSS} from '@dnd-kit/utilities';
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import {ArrowDown, ArrowUp, GripVertical} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { ArrowDown, ArrowUp, GripVertical } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Checkbox} from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -33,15 +33,15 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import {EmptyStateWithBorder} from '@/components/layout/empty';
|
||||
import {ErrorInline} from '@/components/layout/error';
|
||||
import {LoadingStateWithBorder} from '@/components/layout/loading';
|
||||
import type {ProxyRouteItem} from '@/lib/services/openflare';
|
||||
import {WafService} from '@/lib/services/openflare';
|
||||
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
||||
import { ErrorInline } from '@/components/layout/error';
|
||||
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
||||
import type { ProxyRouteItem } from '@/lib/services/openflare';
|
||||
import { WafService } from '@/lib/services/openflare';
|
||||
|
||||
import {getErrorMessage} from '../../components/helpers';
|
||||
import {proxyRouteFormIds} from '../helpers';
|
||||
import {SectionShell} from './section-shell';
|
||||
import { getErrorMessage } from '../../components/helpers';
|
||||
import { proxyRouteFormIds } from '../helpers';
|
||||
import { SectionShell } from './section-shell';
|
||||
|
||||
interface WafSectionProps {
|
||||
route: ProxyRouteItem;
|
||||
@@ -56,50 +56,57 @@ interface SortableRuleProps {
|
||||
onMove: (from: number, to: number) => void;
|
||||
}
|
||||
|
||||
export function reorderRuleIDs(ids: number[], activeID: number, overID: number) {
|
||||
export function reorderRuleIDs(
|
||||
ids: number[],
|
||||
activeID: number,
|
||||
overID: number,
|
||||
) {
|
||||
const from = ids.indexOf(activeID);
|
||||
const to = ids.indexOf(overID);
|
||||
return from < 0 || to < 0 || from === to ? ids : arrayMove(ids, from, to);
|
||||
}
|
||||
|
||||
function SortableRule({id, name, index, total, onMove}: SortableRuleProps) {
|
||||
const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id});
|
||||
function SortableRule({ id, name, index, total, onMove }: SortableRuleProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id });
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{transform: CSS.Transform.toString(transform), transition}}
|
||||
className="flex items-center gap-2 rounded-lg border p-3"
|
||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||
className='flex items-center gap-2 rounded-lg border p-3'
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label={`拖动${name}`}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<GripVertical data-icon="inline-start" />
|
||||
<GripVertical data-icon='inline-start' />
|
||||
</Button>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium">{name}</span>
|
||||
<span className='min-w-0 flex-1 truncate text-sm font-medium'>
|
||||
{name}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label={`上移${name}`}
|
||||
disabled={index === 0}
|
||||
onClick={() => onMove(index, index - 1)}
|
||||
>
|
||||
<ArrowUp data-icon="inline-start" />
|
||||
<ArrowUp data-icon='inline-start' />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
aria-label={`下移${name}`}
|
||||
disabled={index === total - 1}
|
||||
onClick={() => onMove(index, index + 1)}
|
||||
>
|
||||
<ArrowDown data-icon="inline-start" />
|
||||
<ArrowDown data-icon='inline-start' />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -110,7 +117,9 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
const [selectedIDs, setSelectedIDs] = useState<number[]>([]);
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {coordinateGetter: sortableKeyboardCoordinates}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const wafQuery = useQuery({
|
||||
@@ -119,7 +128,8 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
});
|
||||
|
||||
const wafMutation = useMutation({
|
||||
mutationFn: (ids: number[]) => WafService.updateSiteRuleGroups(route.id, ids),
|
||||
mutationFn: (ids: number[]) =>
|
||||
WafService.updateSiteRuleGroups(route.id, ids),
|
||||
onMutate: () => {
|
||||
onSavingChange?.(true);
|
||||
},
|
||||
@@ -133,8 +143,12 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['openflare', 'waf', 'site-rule-groups', route.id],
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: ['openflare', 'waf', 'rule-groups'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['openflare', 'config-versions', 'diff'] }),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['openflare', 'waf', 'rule-groups'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['openflare', 'config-versions', 'diff'],
|
||||
}),
|
||||
]);
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -150,7 +164,10 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
|
||||
const selectedSet = useMemo(() => new Set(selectedIDs), [selectedIDs]);
|
||||
const ruleMap = useMemo(
|
||||
() => new Map((wafQuery.data?.rule_groups ?? []).map((rule) => [rule.id, rule])),
|
||||
() =>
|
||||
new Map(
|
||||
(wafQuery.data?.rule_groups ?? []).map((rule) => [rule.id, rule]),
|
||||
),
|
||||
[wafQuery.data?.rule_groups],
|
||||
);
|
||||
|
||||
@@ -159,7 +176,7 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
setSelectedIDs((current) => arrayMove(current, from, to));
|
||||
};
|
||||
|
||||
const handleDragEnd = ({active, over}: DragEndEvent) => {
|
||||
const handleDragEnd = ({ active, over }: DragEndEvent) => {
|
||||
if (!over || active.id === over.id) return;
|
||||
setSelectedIDs((current) =>
|
||||
reorderRuleIDs(current, Number(active.id), Number(over.id)),
|
||||
@@ -168,13 +185,13 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionShell
|
||||
title="WAF"
|
||||
description="全局规则组始终生效;这里可以为当前网站叠加自定义规则组。"
|
||||
title='WAF'
|
||||
description='全局规则组始终生效;这里可以为当前网站叠加自定义规则组。'
|
||||
formId={proxyRouteFormIds.waf}
|
||||
saving={wafMutation.isPending}
|
||||
>
|
||||
{wafQuery.isLoading ? (
|
||||
<LoadingStateWithBorder description="加载 WAF 规则组..." />
|
||||
<LoadingStateWithBorder description='加载 WAF 规则组...' />
|
||||
) : wafQuery.isError ? (
|
||||
<ErrorInline
|
||||
message={getErrorMessage(wafQuery.error)}
|
||||
@@ -183,39 +200,47 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
) : (
|
||||
<form
|
||||
id={proxyRouteFormIds.waf}
|
||||
className="flex flex-col gap-5"
|
||||
className='flex flex-col gap-5'
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
wafMutation.mutate(selectedIDs);
|
||||
}}
|
||||
>
|
||||
{wafQuery.data?.global_rule_group ? (
|
||||
<div className="rounded-lg border bg-muted/30 p-4" data-testid="global-waf-rule">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div
|
||||
className='rounded-lg border bg-muted/30 p-4'
|
||||
data-testid='global-waf-rule'
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div>
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<p className='text-[10px] font-semibold uppercase tracking-wider text-muted-foreground'>
|
||||
Global Rule Group
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-semibold">
|
||||
<p className='mt-1 text-sm font-semibold'>
|
||||
{wafQuery.data.global_rule_group.name}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline">始终生效</Badge>
|
||||
<Badge variant='outline'>始终生效</Badge>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">选择自定义规则</FieldLegend>
|
||||
<FieldDescription>选中的规则会按下方执行顺序依次运行。</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group" className="grid gap-3 md:grid-cols-2">
|
||||
<FieldLegend variant='label'>选择自定义规则</FieldLegend>
|
||||
<FieldDescription>
|
||||
选中的规则会按下方执行顺序依次运行。
|
||||
</FieldDescription>
|
||||
<FieldGroup
|
||||
data-slot='checkbox-group'
|
||||
className='grid gap-3 md:grid-cols-2'
|
||||
>
|
||||
{(wafQuery.data?.rule_groups ?? []).map((group) => {
|
||||
const checkboxID = `waf-rule-${group.id}`;
|
||||
return (
|
||||
<Field
|
||||
key={group.id}
|
||||
orientation="horizontal"
|
||||
className="rounded-lg border p-4"
|
||||
orientation='horizontal'
|
||||
className='rounded-lg border p-4'
|
||||
>
|
||||
<Checkbox
|
||||
id={checkboxID}
|
||||
@@ -231,10 +256,12 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor={checkboxID}
|
||||
className="min-w-0 cursor-pointer flex-col items-start gap-1"
|
||||
className='min-w-0 cursor-pointer flex-col items-start gap-1'
|
||||
>
|
||||
<span className="truncate text-sm font-semibold">{group.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className='truncate text-sm font-semibold'>
|
||||
{group.name}
|
||||
</span>
|
||||
<span className='text-xs text-muted-foreground'>
|
||||
{group.enabled ? '启用中' : '已停用'} ·{' '}
|
||||
{group.graph.nodes.length} 个节点
|
||||
</span>
|
||||
@@ -246,9 +273,9 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
</FieldSet>
|
||||
|
||||
{selectedIDs.length > 0 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">执行顺序</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<div className='flex flex-col gap-2'>
|
||||
<p className='text-sm font-medium'>执行顺序</p>
|
||||
<p className='text-xs text-muted-foreground'>
|
||||
自定义规则按此顺序执行,可拖动或使用上下移动按钮调整。
|
||||
</p>
|
||||
<DndContext
|
||||
@@ -256,8 +283,11 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={selectedIDs} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SortableContext
|
||||
items={selectedIDs}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className='flex flex-col gap-2'>
|
||||
{selectedIDs.map((id, index) => {
|
||||
const rule = ruleMap.get(id);
|
||||
return rule ? (
|
||||
@@ -278,7 +308,7 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
|
||||
) : null}
|
||||
|
||||
{(wafQuery.data?.rule_groups ?? []).length === 0 ? (
|
||||
<EmptyStateWithBorder description="暂无自定义 WAF 规则组" />
|
||||
<EmptyStateWithBorder description='暂无自定义 WAF 规则组' />
|
||||
) : null}
|
||||
</form>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user