feat: Implement server configuration management API and integrate server management features in FilterManager for enhanced server handling capabilities
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 4m8s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 4m8s
This commit is contained in:
@@ -516,6 +516,138 @@ app.post('/api/filters/export-config', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Server Configs Routes ---
|
||||
|
||||
// Get server configs list
|
||||
app.get('/api/server-configs', async (req, res) => {
|
||||
const params = {
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: 'server-configs.json',
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await s3.getObject(params).promise();
|
||||
const fileContent = data.Body.toString('utf-8');
|
||||
let servers = [];
|
||||
|
||||
try {
|
||||
servers = JSON.parse(fileContent);
|
||||
if (!Array.isArray(servers)) {
|
||||
servers = [];
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing server-configs.json:', parseError);
|
||||
servers = [];
|
||||
}
|
||||
|
||||
res.json(servers);
|
||||
} catch (error) {
|
||||
if (error.code === 'NoSuchKey') {
|
||||
res.json([]); // Return empty array if file does not exist
|
||||
} else {
|
||||
console.error(error);
|
||||
res.status(500).send('Error reading server configs from S3');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update server configs list
|
||||
app.post('/api/server-configs', async (req, res) => {
|
||||
const { servers } = req.body;
|
||||
|
||||
// Validate servers structure
|
||||
if (!Array.isArray(servers)) {
|
||||
return res.status(400).send('Servers must be an array');
|
||||
}
|
||||
|
||||
// Validate each server has required fields
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
const server = servers[i];
|
||||
if (!server.id || !server.name) {
|
||||
return res.status(400).send(`Server at index ${i} is missing required fields`);
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: 'server-configs.json',
|
||||
Body: JSON.stringify(servers, null, 2), // Pretty print JSON
|
||||
ContentType: 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
await s3.putObject(params).promise();
|
||||
res.send('Server configs updated successfully');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send('Error writing server configs to S3');
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific server config
|
||||
app.get('/api/server-configs/:serverId', async (req, res) => {
|
||||
const { serverId } = req.params;
|
||||
const params = {
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: `config-${serverId}.txt`,
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await s3.getObject(params).promise();
|
||||
const config = data.Body.toString('utf-8');
|
||||
res.json({ config });
|
||||
} catch (error) {
|
||||
if (error.code === 'NoSuchKey') {
|
||||
res.json({ config: '// Конфигурация не найдена' });
|
||||
} else {
|
||||
console.error(error);
|
||||
res.status(500).send('Error reading server config from S3');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save specific server config
|
||||
app.post('/api/server-configs/:serverId', async (req, res) => {
|
||||
const { serverId } = req.params;
|
||||
const { config } = req.body;
|
||||
|
||||
if (!config) {
|
||||
return res.status(400).send('Config is required');
|
||||
}
|
||||
|
||||
const params = {
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: `config-${serverId}.txt`,
|
||||
Body: config,
|
||||
ContentType: 'text/plain',
|
||||
};
|
||||
|
||||
try {
|
||||
await s3.putObject(params).promise();
|
||||
res.send('Server config saved successfully');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send('Error writing server config to S3');
|
||||
}
|
||||
});
|
||||
|
||||
// Delete specific server config
|
||||
app.delete('/api/server-configs/:serverId', async (req, res) => {
|
||||
const { serverId } = req.params;
|
||||
const params = {
|
||||
Bucket: BUCKET_NAME,
|
||||
Key: `config-${serverId}.txt`,
|
||||
};
|
||||
|
||||
try {
|
||||
await s3.deleteObject(params).promise();
|
||||
res.send('Server config deleted successfully');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send('Error deleting server config from S3');
|
||||
}
|
||||
});
|
||||
|
||||
// The "catchall" handler: for any request that doesn't
|
||||
// match one above, send back React's index.html file.
|
||||
app.get('*', (req, res) => {
|
||||
|
||||
+309
-101
@@ -16,7 +16,10 @@ import {
|
||||
IconChevronDown,
|
||||
IconLink,
|
||||
IconCopy,
|
||||
IconEye
|
||||
IconEye,
|
||||
IconServer,
|
||||
IconSettings,
|
||||
IconSave
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
const API_URL = '/api';
|
||||
@@ -53,8 +56,16 @@ function FilterManager() {
|
||||
// Состояние для модального окна добавления фильтра
|
||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||
|
||||
// Новые состояния для управления серверами
|
||||
const [servers, setServers] = useState([]);
|
||||
const [selectedServer, setSelectedServer] = useState(null);
|
||||
const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false);
|
||||
const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true });
|
||||
const [serverConfigs, setServerConfigs] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchFilters();
|
||||
fetchServers();
|
||||
}, []);
|
||||
|
||||
const fetchFilters = async () => {
|
||||
@@ -71,6 +82,89 @@ function FilterManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// Функции для управления серверами
|
||||
const fetchServers = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/server-configs`);
|
||||
setServers(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
// Если файл не существует, создаем пустой массив
|
||||
setServers([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddServer = async () => {
|
||||
if (!newServer.name.trim()) {
|
||||
setError('Имя сервера обязательно для заполнения.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const serverToAdd = {
|
||||
id: Date.now().toString(),
|
||||
...newServer,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedServers = [...servers, serverToAdd];
|
||||
await axios.post(`${API_URL}/server-configs`, { servers: updatedServers });
|
||||
|
||||
setServers(updatedServers);
|
||||
setNewServer({ name: '', description: '', enabled: true });
|
||||
setServerConfigModalOpen(false);
|
||||
setSuccess('Сервер успешно добавлен!');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error adding server:', error);
|
||||
setError('Не удалось добавить сервер.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteServer = async (serverId) => {
|
||||
try {
|
||||
const updatedServers = servers.filter(s => s.id !== serverId);
|
||||
await axios.post(`${API_URL}/server-configs`, { servers: updatedServers });
|
||||
|
||||
// Также удаляем файл конфигурации сервера
|
||||
await axios.delete(`${API_URL}/server-configs/${serverId}`);
|
||||
|
||||
setServers(updatedServers);
|
||||
if (selectedServer?.id === serverId) {
|
||||
setSelectedServer(null);
|
||||
}
|
||||
setSuccess('Сервер успешно удален!');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error deleting server:', error);
|
||||
setError('Не удалось удалить сервер.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveServerConfig = async (serverId) => {
|
||||
try {
|
||||
const config = await generateMikrotikConfig();
|
||||
await axios.post(`${API_URL}/server-configs/${serverId}`, { config });
|
||||
|
||||
setSuccess(`Конфигурация для сервера "${servers.find(s => s.id === serverId)?.name}" сохранена!`);
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error saving server config:', error);
|
||||
setError('Не удалось сохранить конфигурацию сервера.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadServerConfig = async (serverId) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/server-configs/${serverId}`);
|
||||
setGeneratedConfig(response.data.config);
|
||||
setPreviewModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Error loading server config:', error);
|
||||
setError('Не удалось загрузить конфигурацию сервера.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFilter = () => {
|
||||
if (!newFilter.community.trim() || !newFilter.gateway.trim()) {
|
||||
setError('Поля Community и Gateway должны быть заполнены.');
|
||||
@@ -355,6 +449,102 @@ function FilterManager() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Секция управления серверами */}
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<IconServer className="me-2" />
|
||||
Готовые конфигурации серверов
|
||||
</h3>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setServerConfigModalOpen(true)}
|
||||
>
|
||||
<IconPlus className="me-2" />
|
||||
Добавить сервер
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{servers.length === 0 ? (
|
||||
<div className="text-center text-muted py-4">
|
||||
<IconServer className="icon-lg mb-2" />
|
||||
<p>Нет добавленных серверов</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setServerConfigModalOpen(true)}
|
||||
>
|
||||
<IconPlus className="me-2" />
|
||||
Добавить первый сервер
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row g-3">
|
||||
{servers.map(server => (
|
||||
<div key={server.id} className="col-md-6 col-lg-4">
|
||||
<div className={`card ${selectedServer?.id === server.id ? 'border-primary' : ''}`}>
|
||||
<div className="card-body">
|
||||
<div className="d-flex justify-content-between align-items-start mb-2">
|
||||
<h4 className="card-title mb-0">{server.name}</h4>
|
||||
<div className="form-check form-switch">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={server.enabled}
|
||||
onChange={async () => {
|
||||
const updatedServers = servers.map(s =>
|
||||
s.id === server.id ? { ...s, enabled: !s.enabled } : s
|
||||
);
|
||||
try {
|
||||
await axios.post(`${API_URL}/server-configs`, { servers: updatedServers });
|
||||
setServers(updatedServers);
|
||||
} catch (error) {
|
||||
setError('Не удалось обновить статус сервера');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{server.description && (
|
||||
<p className="text-muted small mb-3">{server.description}</p>
|
||||
)}
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => handleLoadServerConfig(server.id)}
|
||||
>
|
||||
<IconEye className="me-1" />
|
||||
Просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success btn-sm"
|
||||
onClick={() => handleSaveServerConfig(server.id)}
|
||||
>
|
||||
<IconSave className="me-1" />
|
||||
Сохранить
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => handleDeleteServer(server.id)}
|
||||
>
|
||||
<IconTrash className="me-1" />
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Фильтры и поиск */}
|
||||
<div className="card w-100">
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
@@ -401,107 +591,34 @@ function FilterManager() {
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{minWidth: '180px', maxWidth: '300px'}}>DESCRIPTION</th>
|
||||
<th className="text-end w-1" style={{ minWidth: 110 }}></th>
|
||||
<th>ОПИСАНИЕ</th>
|
||||
<th className="text-end">ДЕЙСТВИЯ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredFilters.map((filter, index) => (
|
||||
<tr key={index}>
|
||||
<td className="font-monospace align-middle">
|
||||
{editingFilter === filter.community ? (
|
||||
<input
|
||||
ref={editInputRef}
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
value={editingValues[filter.community]?.community || ''}
|
||||
onChange={(e) => setEditingValues({
|
||||
...editingValues,
|
||||
[filter.community]: {
|
||||
...editingValues[filter.community],
|
||||
community: e.target.value
|
||||
}
|
||||
})}
|
||||
onKeyDown={(e) => handleEditKeyDown(e, filter.community)}
|
||||
style={{minWidth: '120px'}}
|
||||
/>
|
||||
) : (
|
||||
<span>{filter.community}</span>
|
||||
)}
|
||||
{filteredFilters.map((filter) => (
|
||||
<tr key={filter.community}>
|
||||
<td>
|
||||
<span className="badge bg-blue-lt text-blue">{filter.community}</span>
|
||||
</td>
|
||||
<td className="align-middle text-center">
|
||||
{editingFilter === filter.community ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
value={editingValues[filter.community]?.gateway || ''}
|
||||
onChange={(e) => setEditingValues({
|
||||
...editingValues,
|
||||
[filter.community]: {
|
||||
...editingValues[filter.community],
|
||||
gateway: e.target.value
|
||||
}
|
||||
})}
|
||||
onKeyDown={(e) => handleEditKeyDown(e, filter.community)}
|
||||
style={{minWidth: '120px'}}
|
||||
/>
|
||||
) : (
|
||||
<span className="badge bg-blue-lt text-blue" style={{letterSpacing: '0.5px'}}>{filter.gateway}</span>
|
||||
)}
|
||||
<td>
|
||||
<span className="badge bg-green-lt text-green">{filter.gateway}</span>
|
||||
</td>
|
||||
<td className="align-middle text-muted" style={{fontSize: '14px', maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>
|
||||
{editingFilter === filter.community ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
value={editingValues[filter.community]?.description || ''}
|
||||
onChange={(e) => setEditingValues({
|
||||
...editingValues,
|
||||
[filter.community]: {
|
||||
...editingValues[filter.community],
|
||||
description: e.target.value
|
||||
}
|
||||
})}
|
||||
onKeyDown={(e) => handleEditKeyDown(e, filter.community)}
|
||||
style={{minWidth: '120px'}}
|
||||
/>
|
||||
) : (
|
||||
<span>{filter.description}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end w-1 align-middle" style={{ minWidth: 110 }}>
|
||||
<div className="btn-list gap-1 mb-0">
|
||||
{editingFilter === filter.community ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-icon btn-sm btn-success rounded"
|
||||
onClick={() => handleSaveEdit(filter.community)}
|
||||
>
|
||||
<IconCheck size={18} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-icon btn-sm btn-secondary rounded"
|
||||
onClick={() => handleCancelEdit(filter.community)}
|
||||
>
|
||||
<IconX size={18} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-icon btn-sm btn-outline-primary rounded"
|
||||
onClick={() => startEdit(filter)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-icon btn-sm btn-outline-danger rounded"
|
||||
onClick={() => handleDelete(filter)}
|
||||
>
|
||||
<IconTrash size={18} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<td>{filter.description || '-'}</td>
|
||||
<td className="text-end">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-icon btn-sm"
|
||||
onClick={() => handleEdit(filter)}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-icon btn-sm"
|
||||
onClick={() => handleDelete(filter)}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -512,7 +629,7 @@ function FilterManager() {
|
||||
</div>
|
||||
|
||||
{/* Модальные окна */}
|
||||
<AddFilterModal
|
||||
<AddFilterModal
|
||||
show={addFilterModalOpen}
|
||||
newFilter={newFilter}
|
||||
onNewFilterChange={setNewFilter}
|
||||
@@ -521,7 +638,7 @@ function FilterManager() {
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<EditFilterModal
|
||||
<EditFilterModal
|
||||
show={editModalOpen}
|
||||
filter={editModalFilter}
|
||||
onChange={handleEditModalChange}
|
||||
@@ -529,19 +646,32 @@ function FilterManager() {
|
||||
onClose={handleEditModalClose}
|
||||
/>
|
||||
|
||||
<DeleteFilterModal
|
||||
<DeleteFilterModal
|
||||
show={showDeleteModal}
|
||||
filter={filterToDelete}
|
||||
onDelete={confirmDelete}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
|
||||
<PreviewConfigModal
|
||||
<PreviewConfigModal
|
||||
show={previewModalOpen}
|
||||
config={generatedConfig}
|
||||
onClose={() => setPreviewModalOpen(false)}
|
||||
onCopy={copyConfigToClipboard}
|
||||
/>
|
||||
|
||||
{/* Модальное окно добавления сервера */}
|
||||
<AddServerModal
|
||||
show={serverConfigModalOpen}
|
||||
newServer={newServer}
|
||||
onNewServerChange={setNewServer}
|
||||
onAddServer={handleAddServer}
|
||||
onClose={() => {
|
||||
setServerConfigModalOpen(false);
|
||||
setNewServer({ name: '', description: '', enabled: true });
|
||||
}}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -773,4 +903,82 @@ function PreviewConfigModal({ show, config, onClose, onCopy }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Модальное окно добавления сервера
|
||||
function AddServerModal({ show, newServer, onNewServerChange, onAddServer, onClose, error }) {
|
||||
if (!show) return null;
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
onNewServerChange({ ...newServer, [field]: value });
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onAddServer();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Добавить новый сервер</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
{error && (
|
||||
<div className="alert alert-danger">
|
||||
<IconAlertTriangle className="me-2" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Имя сервера *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Например, SWE-HIPHOST"
|
||||
value={newServer.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Описание</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Необязательное описание сервера"
|
||||
value={newServer.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Включен</label>
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={newServer.enabled}
|
||||
onChange={(e) => handleChange('enabled', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
<IconPlus className="me-2" />
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterManager;
|
||||
Reference in New Issue
Block a user