mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
feat(proxy-routes): align create form with upstream type selection
Let new proxy rules choose direct, tunnel, or Pages origin the same way as the detail reverse-proxy section, instead of only accepting a single upstream URL.
This commit is contained in:
@@ -17,6 +17,14 @@ import {
|
||||
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 {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -26,14 +34,17 @@ import {
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { ProxyRouteItem } from '@/lib/services/openflare';
|
||||
import {
|
||||
NodeService,
|
||||
PagesService,
|
||||
ProxyRouteService,
|
||||
ZoneService,
|
||||
zoneQueryKey,
|
||||
} from '@/lib/services/openflare';
|
||||
|
||||
import { listAllZoneDomains, parseOriginUrl } from './helpers';
|
||||
import { listAllZoneDomains, parseOriginUrl, parseOriginUrls } from './helpers';
|
||||
import { ZoneDomainSelector } from './zone-domain-selector';
|
||||
|
||||
const createProxyRouteSchema = z
|
||||
@@ -42,24 +53,56 @@ const createProxyRouteSchema = z
|
||||
zone_domain_ids: z
|
||||
.array(z.number().int().positive())
|
||||
.min(1, '请至少选择一个域名'),
|
||||
origin_url: z.string().trim().min(1, '请输入上游地址'),
|
||||
upstream_type: z.enum(['direct', 'tunnel', 'pages']),
|
||||
origin_urls_text: z.string().trim(),
|
||||
tunnel_id: z.string().optional(),
|
||||
tunnel_target_addr: z.string().trim().optional(),
|
||||
tunnel_target_protocol: z.enum(['http', 'https']).optional(),
|
||||
pages_project_id: z.string().optional(),
|
||||
enabled: z.boolean(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
try {
|
||||
const parsed = new URL(value.origin_url);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
if (value.upstream_type === 'direct') {
|
||||
if (!value.origin_urls_text.trim()) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['origin_url'],
|
||||
message: '上游地址必须以 http:// 或 https:// 开头',
|
||||
path: ['origin_urls_text'],
|
||||
message: '请至少填写一个上游地址',
|
||||
});
|
||||
} else {
|
||||
const { error } = parseOriginUrls(value.origin_urls_text);
|
||||
if (error) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['origin_urls_text'],
|
||||
message: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (value.upstream_type === 'tunnel') {
|
||||
if (!value.tunnel_id) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['tunnel_id'],
|
||||
message: '请选择内网穿透隧道',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
if (!value.tunnel_target_addr?.trim()) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['tunnel_target_addr'],
|
||||
message: '请填写内网服务地址 (如 127.0.0.1:8080)',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!value.pages_project_id) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['origin_url'],
|
||||
message: '上游地址格式不合法',
|
||||
path: ['pages_project_id'],
|
||||
message: '请选择 Pages 项目',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -69,7 +112,12 @@ type CreateProxyRouteFormValues = z.infer<typeof createProxyRouteSchema>;
|
||||
const defaultValues: CreateProxyRouteFormValues = {
|
||||
site_name: '',
|
||||
zone_domain_ids: [],
|
||||
origin_url: '',
|
||||
upstream_type: 'direct',
|
||||
origin_urls_text: '',
|
||||
tunnel_id: '',
|
||||
tunnel_target_addr: '',
|
||||
tunnel_target_protocol: 'http',
|
||||
pages_project_id: '',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
@@ -101,6 +149,27 @@ export function ProxyRouteCreateSheet({
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const tunnelsQuery = useQuery({
|
||||
queryKey: ['openflare', 'nodes'],
|
||||
queryFn: () => NodeService.listNodes(),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const pagesProjectsQuery = useQuery({
|
||||
queryKey: ['openflare', 'pages-projects'],
|
||||
queryFn: () => PagesService.listProjects(),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const tunnelClients = (tunnelsQuery.data ?? []).filter(
|
||||
(node) => node.node_type === 'tunnel_client',
|
||||
);
|
||||
const pagesProjects = (pagesProjectsQuery.data ?? []).filter(
|
||||
(project) => project.enabled && project.active_deployment_id,
|
||||
);
|
||||
|
||||
const upstreamType = form.watch('upstream_type');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.reset(defaultValues);
|
||||
@@ -108,7 +177,33 @@ export function ProxyRouteCreateSheet({
|
||||
}, [form, open]);
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
const origin = parseOriginUrl(values.origin_url.trim());
|
||||
let originUrl = '';
|
||||
let originScheme: 'http' | 'https' = 'http';
|
||||
let originAddress = '';
|
||||
let originPort = '';
|
||||
let originUri = '';
|
||||
let upstreams: string[] = [];
|
||||
|
||||
if (values.upstream_type === 'direct') {
|
||||
const { urls } = parseOriginUrls(values.origin_urls_text);
|
||||
const primaryOrigin = parseOriginUrl(urls[0]);
|
||||
originUrl = urls[0];
|
||||
originScheme = primaryOrigin.scheme;
|
||||
originAddress = primaryOrigin.address;
|
||||
originPort = primaryOrigin.port;
|
||||
originUri = primaryOrigin.uri;
|
||||
upstreams = urls.slice(1);
|
||||
} else if (values.upstream_type === 'tunnel') {
|
||||
originUrl = `${values.tunnel_target_protocol}://${values.tunnel_target_addr}`;
|
||||
originScheme = values.tunnel_target_protocol as 'http' | 'https';
|
||||
originAddress = values.tunnel_target_addr || '';
|
||||
} else {
|
||||
originUrl = 'http://127.0.0.1';
|
||||
originScheme = 'http';
|
||||
originAddress = '127.0.0.1';
|
||||
originPort = '80';
|
||||
}
|
||||
|
||||
const selectedDomains = (domainsQuery.data ?? []).filter((domain) =>
|
||||
values.zone_domain_ids.includes(domain.id),
|
||||
);
|
||||
@@ -120,13 +215,13 @@ export function ProxyRouteCreateSheet({
|
||||
site_name: values.site_name.trim() || primaryDomain,
|
||||
zone_domain_ids: values.zone_domain_ids,
|
||||
origin_id: null,
|
||||
origin_url: values.origin_url.trim(),
|
||||
origin_scheme: origin.scheme,
|
||||
origin_address: origin.address,
|
||||
origin_port: origin.port,
|
||||
origin_uri: origin.uri,
|
||||
origin_url: originUrl,
|
||||
origin_scheme: originScheme,
|
||||
origin_address: originAddress,
|
||||
origin_port: originPort,
|
||||
origin_uri: originUri,
|
||||
origin_host: '',
|
||||
upstreams: [],
|
||||
upstreams,
|
||||
enabled: values.enabled,
|
||||
enable_https: hasCert,
|
||||
redirect_http: false,
|
||||
@@ -138,7 +233,21 @@ export function ProxyRouteCreateSheet({
|
||||
cache_rules: [],
|
||||
custom_headers: [],
|
||||
basic_auth_enabled: false,
|
||||
upstream_type: 'direct',
|
||||
upstream_type: values.upstream_type,
|
||||
tunnel_node_id:
|
||||
values.upstream_type === 'tunnel' && values.tunnel_id
|
||||
? Number(values.tunnel_id)
|
||||
: null,
|
||||
tunnel_target_addr:
|
||||
values.upstream_type === 'tunnel' ? values.tunnel_target_addr : '',
|
||||
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)
|
||||
: null,
|
||||
});
|
||||
|
||||
form.reset(defaultValues);
|
||||
@@ -200,9 +309,6 @@ export function ProxyRouteCreateSheet({
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
勾选已登记域名,或使用「快捷新增域名」创建后自动勾选。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -210,18 +316,187 @@ export function ProxyRouteCreateSheet({
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='origin_url'
|
||||
name='upstream_type'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>上游地址</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='http://127.0.0.1:8080' {...field} />
|
||||
</FormControl>
|
||||
<FormItem className='space-y-3'>
|
||||
<FormLabel>回源方式</FormLabel>
|
||||
<div className='flex flex-wrap gap-4'>
|
||||
{(
|
||||
[
|
||||
['direct', '直连上游'],
|
||||
['tunnel', '内网穿透 (Tunnel)'],
|
||||
['pages', 'Pages 静态站点'],
|
||||
] as const
|
||||
).map(([value, label]) => (
|
||||
<label
|
||||
key={value}
|
||||
className='flex cursor-pointer items-center gap-2 text-sm'
|
||||
>
|
||||
<input
|
||||
type='radio'
|
||||
value={value}
|
||||
checked={field.value === value}
|
||||
onChange={() => field.onChange(value)}
|
||||
className='size-4 accent-primary'
|
||||
/>
|
||||
<Label className='font-normal'>{label}</Label>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{upstreamType === 'direct' ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='origin_urls_text'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>上游地址</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className='min-h-32 font-mono text-xs'
|
||||
placeholder={
|
||||
'https://origin-a.internal:443\nhttps://origin-b.internal:443'
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
每行一个完整
|
||||
URL。第一行作为主回源,多上游模式请保持相同协议且不要包含
|
||||
path 或 query。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{upstreamType === 'tunnel' ? (
|
||||
<div className='space-y-4 rounded-lg border border-dashed bg-muted/30 p-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='tunnel_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>选择内网穿透隧道</FormLabel>
|
||||
<Select
|
||||
value={field.value || 'none'}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'none' ? '' : value)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='请选择...' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value='none'>请选择...</SelectItem>
|
||||
{tunnelClients.map((tunnel) => (
|
||||
<SelectItem
|
||||
key={tunnel.id}
|
||||
value={String(tunnel.id)}
|
||||
>
|
||||
{tunnel.name} (
|
||||
{tunnel.status === 'online' ? '在线' : '离线'})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
将请求转发到该隧道连接的客户端节点。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='tunnel_target_protocol'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>内网服务协议</FormLabel>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value='http'>HTTP</SelectItem>
|
||||
<SelectItem value='https'>HTTPS</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='tunnel_target_addr'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>内网服务地址</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='127.0.0.1:8080' {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
例如: 127.0.0.1:8080 或 192.168.1.10:80
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{upstreamType === 'pages' ? (
|
||||
<div className='rounded-lg border border-dashed bg-muted/30 p-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='pages_project_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>选择 Pages 项目</FormLabel>
|
||||
<Select
|
||||
value={field.value || 'none'}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'none' ? '' : value)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='请选择...' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value='none'>请选择...</SelectItem>
|
||||
{pagesProjects.map((project) => (
|
||||
<SelectItem
|
||||
key={project.id}
|
||||
value={String(project.id)}
|
||||
>
|
||||
{project.name} ({project.slug})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='enabled'
|
||||
|
||||
@@ -4559,4 +4559,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
Reference in New Issue
Block a user