码桶

发现社区成员的开源项目

main
cloudflare-dns-manager/src/components/BulkOperationsModal.jsx
BulkOperationsModal.jsx18.6 KB
import React, { useState, useEffect } from 'react';
import { Layers, Globe, CheckCircle, AlertCircle, RefreshCw, X, Settings } from 'lucide-react';
import { getAuthHeaders } from '../utils/auth.ts';

const SETTING_OPTIONS = [
    { value: 'security_level', label: 'Security Level', type: 'select', options: ['essentially_off', 'low', 'medium', 'high', 'under_attack'] },
    { value: 'ssl', label: 'SSL Mode', type: 'select', options: ['off', 'flexible', 'full', 'strict'] },
    { value: 'min_tls_version', label: 'Min TLS Version', type: 'select', options: ['1.0', '1.1', '1.2', '1.3'] },
    { value: 'always_use_https', label: 'Always Use HTTPS', type: 'toggle' },
    { value: 'http3', label: 'HTTP/3', type: 'toggle' },
    { value: 'ipv6', label: 'IPv6', type: 'toggle' },
    { value: 'websockets', label: 'WebSockets', type: 'toggle' },
    { value: 'email_obfuscation', label: 'Email Obfuscation', type: 'toggle' },
    { value: 'server_side_exclude', label: 'Server-Side Excludes', type: 'toggle' },
    { value: 'hotlink_protection', label: 'Hotlink Protection', type: 'toggle' },
    { value: 'browser_check', label: 'Browser Check', type: 'toggle' },
    { value: 'bot_fight_mode', label: 'Bot Fight Mode', type: 'toggle' },
    { value: 'privacy_pass', label: 'Privacy Pass', type: 'toggle' },
];

const BulkOperationsModal = ({ show, onClose, auth, authFetch, t, showToast, zones }) => {
    const af = authFetch || fetch;
    const [operation, setOperation] = useState('create');
    const [zoneMode, setZoneMode] = useState('all');
    const [selectedZones, setSelectedZones] = useState([]);
    const [recordType, setRecordType] = useState('TXT');
    const [recordName, setRecordName] = useState('');
    const [recordContent, setRecordContent] = useState('');
    const [loading, setLoading] = useState(false);
    const [results, setResults] = useState(null);

    // Bulk settings state
    const [selectedSetting, setSelectedSetting] = useState('always_use_https');
    const [settingValue, setSettingValue] = useState('on');

    useEffect(() => {
        if (!show) return;
        const handleKeyDown = (e) => { if (e.key === 'Escape') onClose(); };
        document.addEventListener('keydown', handleKeyDown);
        return () => document.removeEventListener('keydown', handleKeyDown);
    }, [show, onClose]);

    useEffect(() => {
        const meta = SETTING_OPTIONS.find(s => s.value === selectedSetting);
        if (meta?.type === 'toggle') setSettingValue('on');
        else if (meta?.type === 'select') setSettingValue(meta.options[0]);
    }, [selectedSetting]);

    if (!show) return null;

    const handleClose = () => {
        setResults(null);
        onClose();
    };

    const handleExecute = async () => {
        if (operation === 'create' && (!recordType || !recordName || !recordContent)) return;
        if (operation === 'delete_matching' && !recordType && !recordName && !recordContent) return;
        setLoading(true);
        setResults(null);
        try {
            if (operation === 'apply_setting') {
                const body = {
                    zones: zoneMode === 'all' ? 'all' : selectedZones,
                    setting: selectedSetting,
                    value: settingValue
                };
                const res = await af('/api/bulk_settings', {
                    method: 'POST',
                    headers: getAuthHeaders(auth, true),
                    body: JSON.stringify(body)
                });
                const data = await res.json();
                if (res.ok) {
                    setResults(data.results || []);
                    showToast(t('bulkSuccess'), 'success');
                } else {
                    showToast(data.error || 'Bulk operation failed', 'error');
                }
            } else {
                const record = {};
                if (recordType) record.type = recordType;
                if (recordName) record.name = recordName;
                if (recordContent) record.content = recordContent;

                const body = {
                    operation,
                    zones: zoneMode === 'all' ? 'all' : selectedZones,
                    record
                };

                const res = await af('/api/dns_bulk', {
                    method: 'POST',
                    headers: getAuthHeaders(auth, true),
                    body: JSON.stringify(body)
                });
                const data = await res.json();
                if (res.ok) {
                    setResults(data.results || []);
                    showToast(t('bulkSuccess'), 'success');
                } else {
                    showToast(data.error || 'Bulk operation failed', 'error');
                }
            }
        } catch (err) {
            console.error('Bulk operation failed:', err);
            showToast('Bulk operation failed', 'error');
        }
        setLoading(false);
    };

    const settingMeta = SETTING_OPTIONS.find(s => s.value === selectedSetting);

    return (
        <div className="modal-overlay" style={{ zIndex: 200 }}
            onClick={(e) => { if (e.target === e.currentTarget) handleClose(); }}>
            <div className="glass-card fade-in modal-content" role="dialog" aria-label={t('bulkOperations')} style={{ width: '100%', maxWidth: '560px', padding: '1.5rem', display: 'flex', flexDirection: 'column' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
                    <h3 style={{ fontSize: '1rem', margin: 0, display: 'flex', alignItems: 'center', gap: '8px' }}>
                        <Layers size={18} color="var(--primary)" /> {t('bulkOperations')}
                    </h3>
                    <button onClick={handleClose} style={{ border: 'none', background: 'transparent', cursor: 'pointer', padding: '4px', display: 'flex' }} aria-label="Close">
                        <X size={18} color="var(--text-muted)" />
                    </button>
                </div>
                <p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>{t('bulkDesc')}</p>

                {results ? (
                    <div className="fade-in" style={{ overflowY: 'auto', flex: 1 }}>
                        <h4 style={{ fontSize: '0.85rem', marginBottom: '0.75rem' }}>{t('bulkResults')}</h4>
                        <div style={{ overflowX: 'auto' }}>
                            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem' }}>
                                <thead>
                                    <tr style={{ borderBottom: '2px solid var(--border)' }}>
                                        <th style={{ padding: '0.4rem 0.5rem', textAlign: 'left', color: 'var(--text-muted)', fontWeight: 600 }}>Zone</th>
                                        <th style={{ padding: '0.4rem 0.5rem', textAlign: 'center', color: 'var(--text-muted)', fontWeight: 600 }}>Status</th>
                                        <th style={{ padding: '0.4rem 0.5rem', textAlign: 'center', color: 'var(--text-muted)', fontWeight: 600 }}>Count</th>
                                        <th style={{ padding: '0.4rem 0.5rem', textAlign: 'left', color: 'var(--text-muted)', fontWeight: 600 }}>Errors</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    {results.map((r, i) => (
                                        <tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
                                            <td style={{ padding: '0.4rem 0.5rem', fontWeight: 500 }}>{r.zoneName}</td>
                                            <td style={{ padding: '0.4rem 0.5rem', textAlign: 'center' }}>
                                                {r.success ? <CheckCircle size={14} color="var(--success)" /> : <AlertCircle size={14} color="var(--error)" />}
                                            </td>
                                            <td style={{ padding: '0.4rem 0.5rem', textAlign: 'center' }}>{r.count}</td>
                                            <td style={{ padding: '0.4rem 0.5rem', fontSize: '0.7rem', color: 'var(--error)', maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                                                {r.errors?.join(', ')}
                                            </td>
                                        </tr>
                                    ))}
                                </tbody>
                            </table>
                        </div>
                        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '1rem' }}>
                            <button className="btn btn-outline" onClick={() => setResults(null)}>{t('cancel')}</button>
                        </div>
                    </div>
                ) : (
                    <div style={{ overflowY: 'auto', flex: 1 }}>
                        <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.75rem', flexWrap: 'wrap' }}>
                            <button className={`btn ${operation === 'create' ? 'btn-primary' : 'btn-outline'}`}
                                style={{ flex: 1, padding: '0.4rem', fontSize: '0.8rem', minWidth: '100px' }}
                                onClick={() => setOperation('create')}>
                                {t('bulkCreate')}
                            </button>
                            <button className={`btn ${operation === 'delete_matching' ? 'btn-primary' : 'btn-outline'}`}
                                style={{ flex: 1, padding: '0.4rem', fontSize: '0.8rem', minWidth: '100px' }}
                                onClick={() => setOperation('delete_matching')}>
                                {t('bulkDelete')}
                            </button>
                            <button className={`btn ${operation === 'apply_setting' ? 'btn-primary' : 'btn-outline'}`}
                                style={{ flex: 1, padding: '0.4rem', fontSize: '0.8rem', minWidth: '100px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '4px' }}
                                onClick={() => setOperation('apply_setting')}>
                                <Settings size={13} /> {t('bulkApplySetting') || 'Apply Setting'}
                            </button>
                        </div>

                        <div style={{ marginBottom: '0.75rem' }}>
                            <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem', display: 'block' }}>{t('bulkSelectZones')}</label>
                            <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
                                <button className={`btn ${zoneMode === 'all' ? 'btn-primary' : 'btn-outline'}`}
                                    style={{ padding: '3px 10px', fontSize: '0.75rem' }}
                                    onClick={() => setZoneMode('all')}>
                                    {t('bulkAllZones')}
                                </button>
                                <button className={`btn ${zoneMode === 'selected' ? 'btn-primary' : 'btn-outline'}`}
                                    style={{ padding: '3px 10px', fontSize: '0.75rem' }}
                                    onClick={() => setZoneMode('selected')}>
                                    {t('bulkSelectedZones')}
                                </button>
                            </div>
                            {zoneMode === 'selected' && (
                                <div style={{ maxHeight: '120px', overflowY: 'auto', border: '1px solid var(--border)', borderRadius: '6px', padding: '0.25rem' }}>
                                    {zones.map(z => (
                                        <label key={z.id} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '0.3rem 0.5rem', cursor: 'pointer', fontSize: '0.8rem', borderRadius: '4px' }}
                                            onMouseEnter={e => e.currentTarget.style.background = 'var(--hover-bg)'}
                                            onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                                            <input type="checkbox" checked={selectedZones.includes(z.id)}
                                                onChange={(e) => {
                                                    if (e.target.checked) setSelectedZones([...selectedZones, z.id]);
                                                    else setSelectedZones(selectedZones.filter(id => id !== z.id));
                                                }}
                                                style={{ width: '14px', height: '14px' }} />
                                            <Globe size={12} color="var(--primary)" />
                                            {z.name}
                                        </label>
                                    ))}
                                </div>
                            )}
                        </div>

                        {operation === 'apply_setting' ? (
                            <>
                                <div style={{ marginBottom: '0.75rem' }}>
                                    <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem', display: 'block' }}>{t('bulkSettingName') || 'Setting'}</label>
                                    <select value={selectedSetting} onChange={e => setSelectedSetting(e.target.value)}
                                        style={{ width: '100%', padding: '6px 8px', borderRadius: '6px', border: '1px solid var(--border)', fontSize: '0.85rem' }}>
                                        {SETTING_OPTIONS.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
                                    </select>
                                </div>
                                <div style={{ marginBottom: '0.75rem' }}>
                                    <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem', display: 'block' }}>{t('bulkSettingValue') || 'Value'}</label>
                                    {settingMeta?.type === 'toggle' ? (
                                        <div style={{ display: 'flex', gap: '0.5rem' }}>
                                            <button className={`btn ${settingValue === 'on' ? 'btn-primary' : 'btn-outline'}`}
                                                style={{ padding: '4px 16px', fontSize: '0.8rem' }}
                                                onClick={() => setSettingValue('on')}>ON</button>
                                            <button className={`btn ${settingValue === 'off' ? 'btn-primary' : 'btn-outline'}`}
                                                style={{ padding: '4px 16px', fontSize: '0.8rem' }}
                                                onClick={() => setSettingValue('off')}>OFF</button>
                                        </div>
                                    ) : (
                                        <select value={settingValue} onChange={e => setSettingValue(e.target.value)}
                                            style={{ width: '100%', padding: '6px 8px', borderRadius: '6px', border: '1px solid var(--border)', fontSize: '0.85rem' }}>
                                            {settingMeta?.options?.map(o => <option key={o} value={o}>{o}</option>)}
                                        </select>
                                    )}
                                </div>
                            </>
                        ) : (
                            <>
                                <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.75rem' }}>
                                    <div style={{ flex: 1 }}>
                                        <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem', display: 'block' }}>{t('bulkRecordType')}</label>
                                        <select value={recordType} onChange={e => setRecordType(e.target.value)}
                                            style={{ width: '100%', padding: '6px 8px', borderRadius: '6px', border: '1px solid var(--border)', fontSize: '0.85rem' }}>
                                            {['A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SRV', 'CAA'].map(type => <option key={type} value={type}>{type}</option>)}
                                        </select>
                                    </div>
                                    <div style={{ flex: 2 }}>
                                        <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem', display: 'block' }}>{t('bulkRecordName')}</label>
                                        <input type="text" value={recordName} onChange={e => setRecordName(e.target.value)}
                                            placeholder={operation === 'create' ? '_dmarc' : `(${t('bulkRecordName')})`}
                                            style={{ width: '100%', fontSize: '0.85rem' }} />
                                    </div>
                                </div>

                                <div style={{ marginBottom: '0.75rem' }}>
                                    <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.4rem', display: 'block' }}>{t('bulkRecordContent')}</label>
                                    <input type="text" value={recordContent} onChange={e => setRecordContent(e.target.value)}
                                        placeholder={operation === 'create' ? 'v=DMARC1; p=reject' : `(${t('bulkRecordContent')})`}
                                        style={{ width: '100%', fontSize: '0.85rem' }} />
                                </div>
                            </>
                        )}

                        <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
                            <button className="btn btn-outline" onClick={handleClose}>{t('cancel')}</button>
                            <button className="btn btn-primary" onClick={handleExecute}
                                disabled={loading || (zoneMode === 'selected' && selectedZones.length === 0)}
                                style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
                                {loading ? <RefreshCw className="spin" size={14} /> : <Layers size={14} />}
                                {loading ? t('bulkExecuting') : t('bulkExecute')}
                            </button>
                        </div>
                    </div>
                )}
            </div>
        </div>
    );
};

export default BulkOperationsModal;