Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m9s
191 lines
6.2 KiB
React
191 lines
6.2 KiB
React
import FormField from './FormField';
|
|
import { useEffect, useState } from 'react';
|
|
import api from '../lib/api.js';
|
|
import { IconSettings, IconDeviceFloppy } from '@tabler/icons-react';
|
|
import ErrorAlert from './ErrorAlert';
|
|
|
|
/**
|
|
* SettingsModal - модальное окно настроек интерфейса
|
|
* Простой подход со встроенным backdrop (как в HistoryModal)
|
|
*/
|
|
export default function SettingsModal({ open, onClose }) {
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState('');
|
|
const [etag, setEtag] = useState('');
|
|
const [dohServer, setDohServer] = useState('');
|
|
const [wsUrl, setWsUrl] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setError('');
|
|
setSuccess('');
|
|
setLoading(true);
|
|
|
|
(async () => {
|
|
try {
|
|
const res = await api.get('/ui-settings');
|
|
const data = res?.data || {};
|
|
setDohServer(String(data?.dohServer || ''));
|
|
setWsUrl(String(data?.wsUpdateUrl || ''));
|
|
const e = res?.headers?.etag || res?.headers?.ETag || '';
|
|
setEtag(e ? String(e) : '');
|
|
} catch (e) {
|
|
setError('Не удалось загрузить настройки');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
}, [open]);
|
|
|
|
if (!open) return null;
|
|
|
|
const validateDoh = (value) => {
|
|
if (!value) return { valid: true, message: '' };
|
|
try {
|
|
const u = new URL(String(value));
|
|
return u.protocol === 'https:'
|
|
? { valid: true, message: 'Корректный HTTPS URL' }
|
|
: { valid: false, message: 'Используйте HTTPS' };
|
|
} catch {
|
|
return { valid: false, message: 'Некорректный URL' };
|
|
}
|
|
};
|
|
|
|
const validateWs = (value) => {
|
|
if (!value) return { valid: true, message: '' };
|
|
try {
|
|
const u = new URL(String(value));
|
|
return u.protocol === 'ws:' || u.protocol === 'wss:'
|
|
? { valid: true, message: 'Корректный WebSocket URL' }
|
|
: { valid: false, message: 'Используйте ws:// или wss://' };
|
|
} catch {
|
|
return { valid: false, message: 'Некорректный URL' };
|
|
}
|
|
};
|
|
|
|
const onSave = async () => {
|
|
setError('');
|
|
setSuccess('');
|
|
|
|
const dohValidation = validateDoh(dohServer);
|
|
const wsValidation = validateWs(wsUrl);
|
|
|
|
if (!dohValidation.valid) {
|
|
setError('Укажите корректный HTTPS URL для DoH');
|
|
return;
|
|
}
|
|
|
|
if (!wsValidation.valid) {
|
|
setError('Укажите корректный WebSocket URL (ws:// или wss://)');
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
const payload = {
|
|
settings: {
|
|
dohServer: String(dohServer || '').trim(),
|
|
wsUpdateUrl: String(wsUrl || '').trim()
|
|
},
|
|
etag
|
|
};
|
|
const res = await api.post('/ui-settings', payload);
|
|
const meta = res?.data || {};
|
|
setSuccess('Настройки успешно сохранены');
|
|
setEtag(String(meta?.etag || ''));
|
|
setTimeout(() => setSuccess(''), 3000);
|
|
} catch (e) {
|
|
setError(e?.response?.data?.message || 'Ошибка при сохранении настроек');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="modal show d-block"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
|
onKeyDown={(e) => { if (e.key === 'Escape') onClose?.(); }}
|
|
>
|
|
<div className="modal-dialog modal-dialog-centered" role="document">
|
|
<div className="modal-content" tabIndex={-1}>
|
|
<div className="modal-header">
|
|
<h5 className="modal-title d-flex align-items-center">
|
|
<IconSettings className="me-2" size={24} />
|
|
Настройки интерфейса
|
|
</h5>
|
|
<button type="button" className="btn-close" onClick={onClose} />
|
|
</div>
|
|
|
|
<div className="modal-body">
|
|
{error && <ErrorAlert message={error} onClose={() => setError('')} />}
|
|
|
|
{success && (
|
|
<div className="alert alert-success alert-dismissible" role="alert">
|
|
<div className="d-flex">
|
|
<div className="flex-grow-1">{success}</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="btn-close"
|
|
onClick={() => setSuccess('')}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<FormField
|
|
label="WebSocket URL (BGP Live)"
|
|
name="wsUrl"
|
|
type="text"
|
|
value={wsUrl}
|
|
onChange={setWsUrl}
|
|
onValidate={validateWs}
|
|
placeholder="ws://host:port/ws/update_bgp?api_key=..."
|
|
helpText="URL для Live-обновления BGP (ws:// или wss://). Можно оставить пустым."
|
|
disabled={loading || saving}
|
|
autoFocus
|
|
/>
|
|
|
|
<FormField
|
|
label="DoH сервер"
|
|
name="dohServer"
|
|
type="text"
|
|
value={dohServer}
|
|
onChange={setDohServer}
|
|
onValidate={validateDoh}
|
|
placeholder="https://dns.google/dns-query"
|
|
helpText="HTTPS URL для DNS-over-HTTPS"
|
|
disabled={loading || saving}
|
|
/>
|
|
</div>
|
|
|
|
<div className="modal-footer">
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
onClick={onClose}
|
|
disabled={saving}
|
|
>
|
|
Закрыть
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
onClick={onSave}
|
|
disabled={saving || loading}
|
|
>
|
|
{saving && <span className="spinner-border spinner-border-sm me-2" />}
|
|
<IconDeviceFloppy size={16} className="me-1" />
|
|
Сохранить
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|