mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 09:06:36 +08:00
feat(cloudflare): add custom targets and node failover (#43)
* feat(cloudflare): add custom targets and node failover * fix(cloudflare): propagate batch sync errors --------- Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import type {
|
||||
CloudflareGroup,
|
||||
CloudflareGroupPayload,
|
||||
@@ -50,7 +51,15 @@ export function GroupDialog({
|
||||
() => nodes.filter((node) => node.node_type === 'edge_node'),
|
||||
[nodes],
|
||||
);
|
||||
const recordContentPlaceholders = {
|
||||
CNAME: t('recordContentPlaceholder.CNAME'),
|
||||
A: t('recordContentPlaceholder.A'),
|
||||
AAAA: t('recordContentPlaceholder.AAAA'),
|
||||
};
|
||||
const [name, setName] = useState('');
|
||||
const [targetMode, setTargetMode] = useState<'node' | 'custom'>('node');
|
||||
const [recordType, setRecordType] = useState<'CNAME' | 'A' | 'AAAA'>('A');
|
||||
const [recordContent, setRecordContent] = useState('');
|
||||
const [primaryNodeID, setPrimaryNodeID] = useState('');
|
||||
const [backupNodeID, setBackupNodeID] = useState('none');
|
||||
const [defaultProxied, setDefaultProxied] = useState(true);
|
||||
@@ -59,8 +68,19 @@ export function GroupDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setName(group?.name ?? '');
|
||||
setPrimaryNodeID(group ? String(group.primary_node.id) : '');
|
||||
setBackupNodeID(group?.backup_node ? String(group.backup_node.id) : 'none');
|
||||
setTargetMode(group?.target_mode === 'custom' ? 'custom' : 'node');
|
||||
setRecordType(group?.record_type ?? 'A');
|
||||
setRecordContent(group?.record_content ?? '');
|
||||
setPrimaryNodeID(
|
||||
group?.target_mode !== 'custom' && group
|
||||
? String(group.primary_node.id)
|
||||
: '',
|
||||
);
|
||||
setBackupNodeID(
|
||||
group?.target_mode !== 'custom' && group?.backup_node
|
||||
? String(group.backup_node.id)
|
||||
: 'none',
|
||||
);
|
||||
setDefaultProxied(group?.default_proxied ?? true);
|
||||
setEnabled(group?.enabled ?? true);
|
||||
}, [group, open]);
|
||||
@@ -82,46 +102,113 @@ export function GroupDialog({
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-primary-node'>{t('primary')}</FieldLabel>
|
||||
<Select value={primaryNodeID} onValueChange={setPrimaryNodeID}>
|
||||
<SelectTrigger id='cf-primary-node' className='w-full'>
|
||||
<SelectValue placeholder={t('primaryPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{edgeNodes.map((node) => (
|
||||
<SelectItem
|
||||
key={node.id}
|
||||
value={String(node.id)}
|
||||
disabled={!node.ip}
|
||||
>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-backup-node'>{t('backup')}</FieldLabel>
|
||||
<Select value={backupNodeID} onValueChange={setBackupNodeID}>
|
||||
<SelectTrigger id='cf-backup-node' className='w-full'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='none'>{t('none')}</SelectItem>
|
||||
{edgeNodes
|
||||
.filter((node) => String(node.id) !== primaryNodeID)
|
||||
.map((node) => (
|
||||
<SelectItem key={node.id} value={String(node.id)}>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldLabel id='cf-target-mode-label'>{t('targetMode')}</FieldLabel>
|
||||
<ToggleGroup
|
||||
type='single'
|
||||
variant='outline'
|
||||
value={targetMode}
|
||||
aria-labelledby='cf-target-mode-label'
|
||||
className='grid w-full grid-cols-2'
|
||||
onValueChange={(value) => {
|
||||
if (value === 'node' || value === 'custom') {
|
||||
setTargetMode(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem value='node' className='w-full'>
|
||||
{t('bindNode')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value='custom' className='w-full'>
|
||||
{t('customTarget')}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
{targetMode === 'node' ? (
|
||||
<>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-primary-node'>
|
||||
{t('primary')}
|
||||
</FieldLabel>
|
||||
<Select value={primaryNodeID} onValueChange={setPrimaryNodeID}>
|
||||
<SelectTrigger id='cf-primary-node' className='w-full'>
|
||||
<SelectValue placeholder={t('primaryPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{edgeNodes.map((node) => (
|
||||
<SelectItem
|
||||
key={node.id}
|
||||
value={String(node.id)}
|
||||
disabled={!node.ip}
|
||||
>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-backup-node'>{t('backup')}</FieldLabel>
|
||||
<Select value={backupNodeID} onValueChange={setBackupNodeID}>
|
||||
<SelectTrigger id='cf-backup-node' className='w-full'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='none'>{t('none')}</SelectItem>
|
||||
{edgeNodes
|
||||
.filter((node) => String(node.id) !== primaryNodeID)
|
||||
.map((node) => (
|
||||
<SelectItem key={node.id} value={String(node.id)}>
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-record-type'>
|
||||
{t('recordType')}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={recordType}
|
||||
onValueChange={(value: 'CNAME' | 'A' | 'AAAA') =>
|
||||
setRecordType(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id='cf-record-type' className='w-full'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='CNAME'>CNAME</SelectItem>
|
||||
<SelectItem value='A'>A</SelectItem>
|
||||
<SelectItem value='AAAA'>AAAA</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-record-content'>
|
||||
{t('recordContent')}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id='cf-record-content'
|
||||
value={recordContent}
|
||||
placeholder={recordContentPlaceholders[recordType]}
|
||||
onChange={(event) => setRecordContent(event.target.value)}
|
||||
/>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
{t('recordContentHint')}
|
||||
</p>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<Field orientation='horizontal'>
|
||||
<FieldLabel htmlFor='cf-default-proxied'>
|
||||
{t('defaultProxied')}
|
||||
@@ -146,13 +233,23 @@ export function GroupDialog({
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pending || !name.trim() || !primaryNodeID}
|
||||
disabled={
|
||||
pending ||
|
||||
!name.trim() ||
|
||||
(targetMode === 'node' ? !primaryNodeID : !recordContent.trim())
|
||||
}
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
name: name.trim(),
|
||||
primary_node_id: Number(primaryNodeID),
|
||||
target_mode: targetMode,
|
||||
record_type: recordType,
|
||||
record_content: recordContent.trim(),
|
||||
primary_node_id:
|
||||
targetMode === 'node' ? Number(primaryNodeID) : 0,
|
||||
backup_node_id:
|
||||
backupNodeID === 'none' ? null : Number(backupNodeID),
|
||||
targetMode === 'node' && backupNodeID !== 'none'
|
||||
? Number(backupNodeID)
|
||||
: null,
|
||||
default_proxied: defaultProxied,
|
||||
enabled,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user