feat: Implement date formatting utility functions and update components to use formatted timestamps for improved readability and consistency across the application.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s
This commit is contained in:
@@ -31,6 +31,12 @@ import {
|
||||
needsGateways as NEEDS_GATEWAYS,
|
||||
SERVER_TYPE_OPTIONS
|
||||
} from './utils/serverUtils.js';
|
||||
import {
|
||||
EditServerModal,
|
||||
DeleteServerModal,
|
||||
LinkGeneratorModal,
|
||||
AddServerModal
|
||||
} from './components/server/index.js';
|
||||
|
||||
const API_URL = '/api';
|
||||
|
||||
@@ -1407,677 +1413,4 @@ function ServerManager() {
|
||||
);
|
||||
}
|
||||
|
||||
// Модальное окно для редактирования сервера с backdrop и анимацией
|
||||
function EditServerModal({ show, server, onChange, onSave, onClose }) {
|
||||
const modalRef = useRef(null);
|
||||
const initializedRef = useRef(false);
|
||||
// Локальное состояние для формы
|
||||
const [localServer, setLocalServer] = useState({});
|
||||
|
||||
// Функция для создания сервера с гарантированными gateways
|
||||
const ensureGateways = (srv) => {
|
||||
if (!srv) return { gateways: [] };
|
||||
const needs = NEEDS_GATEWAYS(srv.type);
|
||||
let gateways = [];
|
||||
if (needs) {
|
||||
gateways = normalizeGateways(srv.gateways, srv.gateway || srv.dns || srv.ip);
|
||||
if (gateways.length === 0) {
|
||||
gateways = [makeGateway({ primary: true })];
|
||||
}
|
||||
}
|
||||
return { ...srv, gateways };
|
||||
};
|
||||
|
||||
// Синхронизируем локальное состояние с пропсами ТОЛЬКО при открытии модалки
|
||||
useEffect(() => {
|
||||
if (show && server && !initializedRef.current) {
|
||||
const prepared = ensureGateways(server);
|
||||
console.log('[EditServerModal] Инициализация с сервером:', prepared);
|
||||
setLocalServer(prepared);
|
||||
initializedRef.current = true;
|
||||
}
|
||||
// Сбрасываем флаг при закрытии
|
||||
if (!show) {
|
||||
initializedRef.current = false;
|
||||
}
|
||||
}, [show, server]);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.Tabler && window.Tabler.Modal && modalRef.current) {
|
||||
const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current);
|
||||
if (show) {
|
||||
modalInstance.show();
|
||||
} else {
|
||||
modalInstance.hide();
|
||||
}
|
||||
// Закрытие по событию Tabler
|
||||
const handler = () => onClose && onClose();
|
||||
modalRef.current.addEventListener('hide.bs.modal', handler);
|
||||
return () => {
|
||||
if (modalRef.current) {
|
||||
modalRef.current.removeEventListener('hide.bs.modal', handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [show, onClose]);
|
||||
|
||||
// Универсальная функция обновления поля
|
||||
const handleFieldChange = (field, value) => {
|
||||
setLocalServer(prev => {
|
||||
const updated = { ...prev, [field]: value };
|
||||
onChange && onChange(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Функция обновления шлюза по индексу
|
||||
const handleGatewayChange = (idx, field, value) => {
|
||||
setLocalServer(prev => {
|
||||
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
|
||||
const updatedGateways = currentGateways.map((gw, i) =>
|
||||
i === idx ? { ...gw, [field]: value } : gw
|
||||
);
|
||||
const updated = { ...prev, gateways: updatedGateways };
|
||||
onChange && onChange(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Функция переключения primary шлюза
|
||||
const handlePrimaryChange = (idx, checked) => {
|
||||
setLocalServer(prev => {
|
||||
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
|
||||
const updatedGateways = currentGateways.map((gw, i) => ({
|
||||
...gw,
|
||||
primary: i === idx ? checked : false
|
||||
}));
|
||||
// Убедимся, что хотя бы один primary
|
||||
if (!updatedGateways.some(g => g.primary) && updatedGateways.length > 0) {
|
||||
updatedGateways[idx].primary = true;
|
||||
}
|
||||
const updated = { ...prev, gateways: updatedGateways };
|
||||
onChange && onChange(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Функция добавления нового шлюза
|
||||
const handleAddGateway = () => {
|
||||
console.log('[EditServerModal] handleAddGateway вызвана, текущий localServer:', localServer);
|
||||
setLocalServer(prev => {
|
||||
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
|
||||
const newGw = makeGateway({ primary: currentGateways.length === 0 });
|
||||
const updatedGateways = [...currentGateways, newGw];
|
||||
const updated = { ...prev, gateways: updatedGateways };
|
||||
console.log('[EditServerModal] Новый localServer после добавления шлюза:', updated);
|
||||
onChange && onChange(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Функция удаления шлюза
|
||||
const handleRemoveGateway = (idx) => {
|
||||
setLocalServer(prev => {
|
||||
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
|
||||
if (currentGateways.length <= 1) return prev; // Не удаляем последний
|
||||
const updatedGateways = currentGateways.filter((_, i) => i !== idx);
|
||||
// Убедимся, что есть primary
|
||||
if (!updatedGateways.some(g => g.primary) && updatedGateways.length > 0) {
|
||||
updatedGateways[0].primary = true;
|
||||
}
|
||||
const updated = { ...prev, gateways: updatedGateways };
|
||||
onChange && onChange(updated);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Получаем текущие шлюзы для рендера
|
||||
const currentGateways = Array.isArray(localServer.gateways) ? localServer.gateways : [];
|
||||
const showGateways = NEEDS_GATEWAYS(localServer.type);
|
||||
|
||||
return (
|
||||
<div className="modal" tabIndex="-1" ref={modalRef}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Редактировать сервер</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form onSubmit={e => { e.preventDefault(); onSave && onSave(localServer); }}>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">ID (опционально)</label>
|
||||
<input type="text" className="form-control" value={localServer.id || ''} onChange={e => handleFieldChange('id', e.target.value)} placeholder="srv-1" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">IP адрес</label>
|
||||
<input type="text" className="form-control" value={localServer.ip || ''} onChange={e => handleFieldChange('ip', e.target.value)} />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Внешний IP (опционально)</label>
|
||||
<input type="text" className="form-control" value={localServer.extIp || ''} onChange={e => handleFieldChange('extIp', e.target.value)} placeholder="публичный IP, если отличается" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Внутренний IP (опционально)</label>
|
||||
<input type="text" className="form-control" value={localServer.internalIp || ''} onChange={e => handleFieldChange('internalIp', e.target.value)} placeholder="10.x.x.x / 192.168.x.x" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">DNS имя</label>
|
||||
<input type="text" className="form-control" value={localServer.dns || ''} onChange={e => handleFieldChange('dns', e.target.value)} />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Страна</label>
|
||||
<input type="text" className="form-control" value={localServer.country || ''} onChange={e => handleFieldChange('country', e.target.value)} />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Провайдер</label>
|
||||
<input type="text" className="form-control" value={localServer.provider || ''} onChange={e => handleFieldChange('provider', e.target.value)} />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип сервера</label>
|
||||
<select className="form-select" value={localServer.type || 'jumphost'} onChange={e => handleFieldChange('type', e.target.value)}>
|
||||
{SERVER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип туннеля</label>
|
||||
<input type="text" className="form-control" value={localServer.tunnel || ''} onChange={e => handleFieldChange('tunnel', e.target.value)} />
|
||||
</div>
|
||||
{showGateways && (
|
||||
<>
|
||||
<div className="form-label">Шлюзы ({currentGateways.length})</div>
|
||||
<div className="list-group list-group-flush border rounded mb-2">
|
||||
{currentGateways.map((gw, idx) => (
|
||||
<div key={gw.id || `gw-${idx}`} className="list-group-item">
|
||||
<div className="row g-2 align-items-end">
|
||||
<div className="col-12 col-md-4">
|
||||
<label className="form-label small mb-1">Имя *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={gw.name || ''}
|
||||
onChange={(e) => handleGatewayChange(idx, 'name', e.target.value)}
|
||||
placeholder="GW-MSK"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<label className="form-label small mb-1">IP</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={gw.ip || ''}
|
||||
onChange={(e) => handleGatewayChange(idx, 'ip', e.target.value)}
|
||||
placeholder="10.0.0.1"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-3">
|
||||
<label className="form-label small mb-1">Комментарий</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={gw.comment || ''}
|
||||
onChange={(e) => handleGatewayChange(idx, 'comment', e.target.value)}
|
||||
placeholder="Основной шлюз"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-1 d-flex align-items-center justify-content-center">
|
||||
<div className="form-check form-switch m-0">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={!!gw.primary}
|
||||
onChange={(e) => handlePrimaryChange(idx, e.target.checked)}
|
||||
title="Основной шлюз"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
||||
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
|
||||
{currentGateways.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link text-danger px-0"
|
||||
onClick={() => handleRemoveGateway(idx)}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={handleAddGateway}
|
||||
>
|
||||
<IconPlus className="icon" />
|
||||
Добавить шлюз
|
||||
</button>
|
||||
<div className="form-text">Для jumphost/exit должен быть один основной gateway.</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary d-flex align-items-center" data-bs-dismiss="modal">
|
||||
<span className="fw-bold">Отмена</span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary d-flex align-items-center" onClick={() => onSave && onSave(localServer)}>
|
||||
<span className="fw-bold">Сохранить</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteServerModal({ show, server, onDelete, onClose }) {
|
||||
const modalRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.Tabler && window.Tabler.Modal && modalRef.current) {
|
||||
const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current);
|
||||
if (show) {
|
||||
modalInstance.show();
|
||||
} else {
|
||||
modalInstance.hide();
|
||||
}
|
||||
const handler = () => onClose && onClose();
|
||||
modalRef.current.addEventListener('hide.bs.modal', handler);
|
||||
return () => {
|
||||
if (modalRef.current) {
|
||||
modalRef.current.removeEventListener('hide.bs.modal', handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [show, onClose]);
|
||||
|
||||
return (
|
||||
<div className="modal" tabIndex="-1" ref={modalRef}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-status bg-danger"></div>
|
||||
<div className="modal-header">
|
||||
<IconAlertTriangle className="icon text-danger me-2" size={28} />
|
||||
<h5 className="modal-title">Подтверждение удаления</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>Вы уверены, что хотите удалить сервер <strong>{server?.ip}</strong>?</p>
|
||||
<p className="text-muted">Это действие нельзя отменить.</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary d-flex align-items-center" data-bs-dismiss="modal">
|
||||
<span className="fw-bold">Отмена</span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-danger d-flex align-items-center" onClick={onDelete}>
|
||||
<span className="fw-bold">Удалить</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Модальное окно для генератора ссылок
|
||||
function LinkGeneratorModal({ show, server, urlSettings, onUrlSettingsChange, onGenerateUrl, onCopyToClipboard, onClose }) {
|
||||
const modalRef = useRef(null);
|
||||
const [localUrlSettings, setLocalUrlSettings] = useState(urlSettings);
|
||||
|
||||
// Синхронизируем локальное состояние с пропсами
|
||||
useEffect(() => {
|
||||
setLocalUrlSettings(urlSettings);
|
||||
}, [urlSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.Tabler && window.Tabler.Modal && modalRef.current) {
|
||||
const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current);
|
||||
if (show) {
|
||||
modalInstance.show();
|
||||
} else {
|
||||
modalInstance.hide();
|
||||
}
|
||||
const handler = () => onClose && onClose();
|
||||
modalRef.current.addEventListener('hide.bs.modal', handler);
|
||||
return () => {
|
||||
if (modalRef.current) {
|
||||
modalRef.current.removeEventListener('hide.bs.modal', handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [show, onClose]);
|
||||
|
||||
const handleSettingChange = (key, value) => {
|
||||
const updated = { ...localUrlSettings, [key]: value };
|
||||
setLocalUrlSettings(updated);
|
||||
onUrlSettingsChange(updated);
|
||||
};
|
||||
|
||||
const handleSaveSettings = () => {
|
||||
onUrlSettingsChange(localUrlSettings);
|
||||
// Сохраняем в localStorage
|
||||
localStorage.setItem('urlSettings', JSON.stringify(localUrlSettings));
|
||||
};
|
||||
|
||||
const generatedUrl = server ? onGenerateUrl(server) : '';
|
||||
|
||||
return (
|
||||
<div className="modal modal-lg" tabIndex="-1" ref={modalRef}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">
|
||||
<IconLink className="icon me-2" />
|
||||
Генератор ссылок для {server?.ip}
|
||||
</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<h6 className="mb-3">Настройки URL</h6>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Базовый URL</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.baseUrl}
|
||||
onChange={e => handleSettingChange('baseUrl', e.target.value)}
|
||||
placeholder="https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Cloudflare Gateway</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.cloudflare_gateway}
|
||||
onChange={e => handleSettingChange('cloudflare_gateway', e.target.value)}
|
||||
placeholder="SWE-IHOR или IP"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Bunny Gateway</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.bunny_gateway}
|
||||
onChange={e => handleSettingChange('bunny_gateway', e.target.value)}
|
||||
placeholder="SWE-IHOR или IP"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Fastly Gateway</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.fastly_gateway}
|
||||
onChange={e => handleSettingChange('fastly_gateway', e.target.value)}
|
||||
placeholder="SWE-IHOR или IP"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Telegram Gateway</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.telegram_gateway}
|
||||
onChange={e => handleSettingChange('telegram_gateway', e.target.value)}
|
||||
placeholder="IP адрес"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Hetzner Gateway</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.hetzner_gateway}
|
||||
onChange={e => handleSettingChange('hetzner_gateway', e.target.value)}
|
||||
placeholder="IP адрес"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.type}
|
||||
onChange={e => handleSettingChange('type', e.target.value)}
|
||||
placeholder="routes"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Версия</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={localUrlSettings.version}
|
||||
onChange={e => handleSettingChange('version', e.target.value)}
|
||||
placeholder="v4.rsc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h6 className="mb-3">Сгенерированная ссылка</h6>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={generatedUrl}
|
||||
readOnly
|
||||
style={{ fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-outline-secondary d-flex align-items-center"
|
||||
type="button"
|
||||
onClick={() => onCopyToClipboard(generatedUrl)}
|
||||
title="Копировать в буфер обмена"
|
||||
>
|
||||
<span className="me-2 d-flex align-items-center"><IconDownload size={18} /></span>
|
||||
<span className="fw-bold">Копировать</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary d-flex align-items-center" data-bs-dismiss="modal">
|
||||
<span className="fw-bold">Закрыть</span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary d-flex align-items-center" onClick={handleSaveSettings}>
|
||||
<span className="fw-bold">Сохранить настройки</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Модальное окно для добавления нового сервера
|
||||
function AddServerModal({ show, newServer, customProvider, onNewServerChange, onCustomProviderChange, onAddServer, onClose, error }) {
|
||||
const modalRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.Tabler && window.Tabler.Modal && modalRef.current) {
|
||||
const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current);
|
||||
if (show) {
|
||||
modalInstance.show();
|
||||
} else {
|
||||
modalInstance.hide();
|
||||
}
|
||||
const handler = () => onClose && onClose();
|
||||
modalRef.current.addEventListener('hide.bs.modal', handler);
|
||||
return () => {
|
||||
if (modalRef.current) {
|
||||
modalRef.current.removeEventListener('hide.bs.modal', handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [show, onClose]);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
onNewServerChange({ ...newServer, [field]: value });
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onAddServer();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal" tabIndex="-1" ref={modalRef}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">
|
||||
<IconPlus className="icon me-2" />
|
||||
Добавить новый сервер
|
||||
</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">IP адрес *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="192.168.1.1"
|
||||
value={newServer.ip}
|
||||
onChange={(e) => handleChange('ip', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">DNS имя *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="srv1.shx.su"
|
||||
value={newServer.dns}
|
||||
onChange={(e) => handleChange('dns', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Страна *</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={newServer.country}
|
||||
onChange={(e) => handleChange('country', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Выберите страну</option>
|
||||
<option value="RU">Россия (RU)</option>
|
||||
<option value="SWE">Швеция (SWE)</option>
|
||||
<option value="US">США (US)</option>
|
||||
<option value="DE">Германия (DE)</option>
|
||||
<option value="NL">Нидерланды (NL)</option>
|
||||
<option value="SG">Сингапур (SG)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Провайдер *</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={newServer.provider}
|
||||
onChange={(e) => handleChange('provider', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Выберите провайдера</option>
|
||||
<option value="Yandex">Yandex Cloud</option>
|
||||
<option value="AWS">Amazon Web Services</option>
|
||||
<option value="Google">Google Cloud</option>
|
||||
<option value="Azure">Microsoft Azure</option>
|
||||
<option value="Hetzner">Hetzner</option>
|
||||
<option value="OVH">OVH</option>
|
||||
<option value="DigitalOcean">DigitalOcean</option>
|
||||
<option value="Vultr">Vultr</option>
|
||||
<option value="Linode">Linode</option>
|
||||
<option value="VPSVILLE">VPSVILLE</option>
|
||||
<option value="HIPHOST">HIPHOST</option>
|
||||
<option value="Другой">Другой</option>
|
||||
</select>
|
||||
{newServer.provider === 'Другой' && (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mt-2"
|
||||
placeholder="Введите название провайдера"
|
||||
value={customProvider}
|
||||
onChange={(e) => onCustomProviderChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип сервера *</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={newServer.type}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Выберите тип</option>
|
||||
{SERVER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Тип туннеля *</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={newServer.tunnel}
|
||||
onChange={(e) => handleChange('tunnel', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="GRE">GRE (Generic Routing Encapsulation)</option>
|
||||
<option value="IPSec">IPSec</option>
|
||||
<option value="WireGuard">WireGuard</option>
|
||||
<option value="OpenVPN">OpenVPN</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Основной шлюз *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Cloudflare, Hetzner, Мой шлюз и т.д."
|
||||
value={newServer.gateway}
|
||||
onChange={(e) => handleChange('gateway', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary d-flex align-items-center" data-bs-dismiss="modal">
|
||||
<span className="fw-bold">Отмена</span>
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary d-flex align-items-center" onClick={onAddServer}>
|
||||
<span className="me-2 d-flex align-items-center"><IconPlus className="icon" /></span>
|
||||
<span className="fw-bold">Добавить сервер</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServerManager;
|
||||
Reference in New Issue
Block a user