feat: Add link generator functionality in ServerManager with URL settings management and CSV export feature
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 18m44s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 18m44s
This commit is contained in:
@@ -14,7 +14,8 @@ import {
|
|||||||
IconUpload,
|
IconUpload,
|
||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
IconServer,
|
IconServer,
|
||||||
IconChevronDown
|
IconChevronDown,
|
||||||
|
IconLink
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
@@ -50,10 +51,37 @@ function ServerManager() {
|
|||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
const [editModalServer, setEditModalServer] = useState(null);
|
const [editModalServer, setEditModalServer] = useState(null);
|
||||||
|
|
||||||
|
// Состояние для генератора ссылок
|
||||||
|
const [linkGeneratorOpen, setLinkGeneratorOpen] = useState(false);
|
||||||
|
const [linkGeneratorServer, setLinkGeneratorServer] = useState(null);
|
||||||
|
const [urlSettings, setUrlSettings] = useState({
|
||||||
|
baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo',
|
||||||
|
cloudflare_gateway: 'SWE-IHOR',
|
||||||
|
bunny_gateway: 'SWE-IHOR',
|
||||||
|
fastly_gateway: 'SWE-IHOR',
|
||||||
|
telegram_gateway: '94.142.140.1',
|
||||||
|
hetzner_gateway: '94.142.140.1',
|
||||||
|
type: 'routes',
|
||||||
|
version: 'v4.rsc'
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchServers();
|
fetchServers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Загружаем настройки URL из localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const savedSettings = localStorage.getItem('urlSettings');
|
||||||
|
if (savedSettings) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedSettings);
|
||||||
|
setUrlSettings(prev => ({ ...prev, ...parsed }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при загрузке настроек URL:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchServers = async () => {
|
const fetchServers = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -98,6 +126,72 @@ function ServerManager() {
|
|||||||
setEditModalServer(null);
|
setEditModalServer(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Функции для генератора ссылок
|
||||||
|
const handleGenerateLink = (server) => {
|
||||||
|
setLinkGeneratorServer(server);
|
||||||
|
setLinkGeneratorOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLinkGeneratorClose = () => {
|
||||||
|
setLinkGeneratorOpen(false);
|
||||||
|
setLinkGeneratorServer(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateServerUrl = (server) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
// Добавляем все параметры из настроек
|
||||||
|
Object.entries(urlSettings).forEach(([key, value]) => {
|
||||||
|
if (key !== 'baseUrl' && value) {
|
||||||
|
params.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем IP сервера как параметр server
|
||||||
|
params.append('server', server.ip);
|
||||||
|
|
||||||
|
return `${urlSettings.baseUrl}?${params.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setSuccess('Ссылка скопирована в буфер обмена!');
|
||||||
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Не удалось скопировать ссылку');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const quickCopyLink = async (server) => {
|
||||||
|
const url = generateServerUrl(server);
|
||||||
|
await copyToClipboard(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportAllLinks = () => {
|
||||||
|
const csvContent = [
|
||||||
|
['IP', 'DNS', 'Country', 'Provider', 'Tunnel', 'Generated URL'],
|
||||||
|
...servers.map(server => [
|
||||||
|
server.ip,
|
||||||
|
server.dns,
|
||||||
|
server.country,
|
||||||
|
server.provider,
|
||||||
|
server.tunnel,
|
||||||
|
generateServerUrl(server)
|
||||||
|
])
|
||||||
|
].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `server_links_${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setEditingServer(null);
|
setEditingServer(null);
|
||||||
};
|
};
|
||||||
@@ -340,7 +434,7 @@ function ServerManager() {
|
|||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-success w-100"
|
className="btn btn-success w-100 mb-2"
|
||||||
onClick={handleSaveChanges}
|
onClick={handleSaveChanges}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
@@ -356,6 +450,15 @@ function ServerManager() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary w-100"
|
||||||
|
onClick={exportAllLinks}
|
||||||
|
disabled={servers.length === 0}
|
||||||
|
>
|
||||||
|
<IconDownload className="icon me-2" />
|
||||||
|
Экспорт всех ссылок (CSV)
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -444,6 +547,7 @@ function ServerManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
|
<th className="text-end">Ссылка</th>
|
||||||
<th className="text-end">Действия</th>
|
<th className="text-end">Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -455,6 +559,19 @@ function ServerManager() {
|
|||||||
<td><span className="badge bg-blue-lt text-blue">{countryToFlag(server.country)} {server.country}</span></td>
|
<td><span className="badge bg-blue-lt text-blue">{countryToFlag(server.country)} {server.country}</span></td>
|
||||||
<td>{server.provider}</td>
|
<td>{server.provider}</td>
|
||||||
<td><span className="badge bg-green-lt text-green">{server.tunnel}</span></td>
|
<td><span className="badge bg-green-lt text-green">{server.tunnel}</span></td>
|
||||||
|
<td className="text-end">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-info btn-icon"
|
||||||
|
onClick={() => handleGenerateLink(server)}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
quickCopyLink(server);
|
||||||
|
}}
|
||||||
|
title="Левый клик - настройки, правый клик - быстрое копирование"
|
||||||
|
>
|
||||||
|
<IconLink size={18} />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
<button className="btn btn-outline-primary btn-icon me-1" onClick={() => handleEdit(server)}><IconEdit size={18} /></button>
|
<button className="btn btn-outline-primary btn-icon me-1" onClick={() => handleEdit(server)}><IconEdit size={18} /></button>
|
||||||
<button className="btn btn-outline-danger btn-icon" onClick={() => confirmDelete(server)}><IconTrash size={18} /></button>
|
<button className="btn btn-outline-danger btn-icon" onClick={() => confirmDelete(server)}><IconTrash size={18} /></button>
|
||||||
@@ -506,6 +623,17 @@ function ServerManager() {
|
|||||||
onSave={handleEditModalSave}
|
onSave={handleEditModalSave}
|
||||||
onClose={handleEditModalClose}
|
onClose={handleEditModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Link Generator Modal */}
|
||||||
|
<LinkGeneratorModal
|
||||||
|
show={linkGeneratorOpen}
|
||||||
|
server={linkGeneratorServer}
|
||||||
|
urlSettings={urlSettings}
|
||||||
|
onUrlSettingsChange={setUrlSettings}
|
||||||
|
onGenerateUrl={generateServerUrl}
|
||||||
|
onCopyToClipboard={copyToClipboard}
|
||||||
|
onClose={handleLinkGeneratorClose}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -633,4 +761,177 @@ function DeleteServerModal({ show, server, onDelete, onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Модальное окно для генератора ссылок
|
||||||
|
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"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onCopyToClipboard(generatedUrl)}
|
||||||
|
title="Копировать в буфер обмена"
|
||||||
|
>
|
||||||
|
<IconDownload size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={handleSaveSettings}>Сохранить настройки</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default ServerManager;
|
export default ServerManager;
|
||||||
Reference in New Issue
Block a user