'use client'; import { useEffect, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Field, FieldDescription, FieldGroup, FieldLabel, FieldTitle, } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; import { Spinner } from '@/components/ui/spinner'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { type PagesRemoteNetworkPolicy, type PagesSource, PagesService, } from '@/lib/services/openflare'; import { deploymentsQueryKey, projectQueryKey, projectsQueryKey, sourceQueryKey, } from '../../components/pages-utils'; type SourceMode = 'manual' | 'remote_url'; type Confirmation = 'trusted_internal' | 'manual' | null; interface PagesSourceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectId: number; source: PagesSource; initialMode?: SourceMode; } export function PagesSourceDialog({ open, onOpenChange, projectId, source, initialMode, }: PagesSourceDialogProps) { const queryClient = useQueryClient(); const [mode, setMode] = useState('manual'); const [networkPolicy, setNetworkPolicy] = useState('public'); const [replaceURL, setReplaceURL] = useState(false); const [remoteURL, setRemoteURL] = useState(''); const [urlError, setURLError] = useState(''); const [confirmation, setConfirmation] = useState(null); useEffect(() => { if (!open) return; const nextMode = initialMode ?? (source.source_type === 'remote_url' ? 'remote_url' : 'manual'); setMode(nextMode); setNetworkPolicy( source.source_type === 'remote_url' ? source.remote_network_policy : 'public', ); setReplaceURL(source.source_type !== 'remote_url'); setRemoteURL(''); setURLError(''); setConfirmation(null); }, [initialMode, open, source]); const invalidateSourceState = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: sourceQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(projectId), }), queryClient.invalidateQueries({ queryKey: projectsQueryKey }), ]); }; const updateMutation = useMutation({ mutationFn: () => PagesService.updateSource(projectId, { source_type: 'remote_url', remote_url_set: replaceURL, remote_url: replaceURL ? remoteURL.trim() : '', remote_network_policy: networkPolicy, }), onSuccess: async (result) => { queryClient.setQueryData(sourceQueryKey(projectId), result.source); await invalidateSourceState(); toast.success('部署源已更新'); if (result.warning) toast.warning(result.warning); setConfirmation(null); onOpenChange(false); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '部署源更新失败'); }, }); const deleteMutation = useMutation({ mutationFn: () => PagesService.deleteSource(projectId), onSuccess: async (manualSource) => { queryClient.setQueryData(sourceQueryKey(projectId), manualSource); await invalidateSourceState(); toast.success('已切换回手动部署'); setConfirmation(null); onOpenChange(false); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '切换失败'); }, }); const isPending = updateMutation.isPending || deleteMutation.isPending; const submitRemote = () => { if (replaceURL) { const value = remoteURL.trim(); if (!value) { setURLError('请输入 Remote URL'); return; } try { const parsed = new URL(value); if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(); } catch { setURLError('请输入有效的 HTTP(S) URL'); return; } } setURLError(''); if (networkPolicy === 'trusted_internal') { setConfirmation('trusted_internal'); return; } updateMutation.mutate(); }; const handleSubmit = () => { if (mode === 'manual') { if (source.source_type === 'manual') { onOpenChange(false); } else { setConfirmation('manual'); } return; } submitRemote(); }; return ( <> { if (!isPending) onOpenChange(nextOpen); }} > 部署源设置 手动部署与 Remote URL 各自保持独立配置;后续仓库构建来源会作为新的来源类型接入。 来源类型 { if (value === 'manual' || value === 'remote_url') { setMode(value); } }} > 手动部署 Remote URL 手动部署由管理员上传本地包;Remote URL 通过显式同步下载并发布。 {mode === 'manual' ? ( 手动部署
保留现有部署与当前生产版本,后续通过“上传部署包”创建新部署。
) : ( <> Remote URL {source.source_type === 'remote_url' && !replaceURL ? (
{source.display_url}
) : ( { setRemoteURL(event.target.value); setURLError(''); }} /> )} {urlError || (replaceURL ? '保存后不会回显原始地址或 query token。' : '界面只显示脱敏地址;留空表示保留当前地址。')} {source.source_type === 'remote_url' && replaceURL ? ( ) : null}
网络策略 { if (value === 'public' || value === 'trusted_internal') { setNetworkPolicy(value); } }} > 公网安全模式 受信内网模式 {networkPolicy === 'public' ? '阻止内网地址、代理与不安全 TLS。' : '允许访问内网地址与自签名证书,仅用于可信来源。'} )}
{ if (!nextOpen && !isPending) setConfirmation(null); }} > {confirmation === 'manual' ? '切换回手动部署' : '启用受信内网模式'} {confirmation === 'manual' ? '当前来源配置将被删除,但已有部署与当前生产版本会保留。' : '该模式允许访问私有网络并接受自签名证书。请确认此地址属于可信内部来源,且不会被非可信用户控制。'} 取消 { event.preventDefault(); if (confirmation === 'manual') { deleteMutation.mutate(); } else if (confirmation === 'trusted_internal') { updateMutation.mutate(); } }} > {isPending ? : null} 确认 ); }