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
|
// The "catchall" handler: for any request that doesn't
|
||||||
// match one above, send back React's index.html file.
|
// match one above, send back React's index.html file.
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
|
|||||||
+305
-97
@@ -16,7 +16,10 @@ import {
|
|||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
IconEye
|
IconEye,
|
||||||
|
IconServer,
|
||||||
|
IconSettings,
|
||||||
|
IconSave
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
@@ -53,8 +56,16 @@ function FilterManager() {
|
|||||||
// Состояние для модального окна добавления фильтра
|
// Состояние для модального окна добавления фильтра
|
||||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchFilters();
|
fetchFilters();
|
||||||
|
fetchServers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchFilters = async () => {
|
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 = () => {
|
const handleAddFilter = () => {
|
||||||
if (!newFilter.community.trim() || !newFilter.gateway.trim()) {
|
if (!newFilter.community.trim() || !newFilter.gateway.trim()) {
|
||||||
setError('Поля Community и Gateway должны быть заполнены.');
|
setError('Поля Community и Gateway должны быть заполнены.');
|
||||||
@@ -355,6 +449,102 @@ function FilterManager() {
|
|||||||
</div>
|
</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 w-100">
|
||||||
<div className="card-header d-flex justify-content-between align-items-center">
|
<div className="card-header d-flex justify-content-between align-items-center">
|
||||||
@@ -401,107 +591,34 @@ function FilterManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{minWidth: '180px', maxWidth: '300px'}}>DESCRIPTION</th>
|
<th>ОПИСАНИЕ</th>
|
||||||
<th className="text-end w-1" style={{ minWidth: 110 }}></th>
|
<th className="text-end">ДЕЙСТВИЯ</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredFilters.map((filter, index) => (
|
{filteredFilters.map((filter) => (
|
||||||
<tr key={index}>
|
<tr key={filter.community}>
|
||||||
<td className="font-monospace align-middle">
|
<td>
|
||||||
{editingFilter === filter.community ? (
|
<span className="badge bg-blue-lt text-blue">{filter.community}</span>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="align-middle text-center">
|
<td>
|
||||||
{editingFilter === filter.community ? (
|
<span className="badge bg-green-lt text-green">{filter.gateway}</span>
|
||||||
<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>
|
</td>
|
||||||
<td className="align-middle text-muted" style={{fontSize: '14px', maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>
|
<td>{filter.description || '-'}</td>
|
||||||
{editingFilter === filter.community ? (
|
<td className="text-end">
|
||||||
<input
|
<div className="btn-list">
|
||||||
type="text"
|
<button
|
||||||
className="form-control form-control-sm"
|
className="btn btn-outline-primary btn-icon btn-sm"
|
||||||
value={editingValues[filter.community]?.description || ''}
|
onClick={() => handleEdit(filter)}
|
||||||
onChange={(e) => setEditingValues({
|
>
|
||||||
...editingValues,
|
<IconEdit size={16} />
|
||||||
[filter.community]: {
|
</button>
|
||||||
...editingValues[filter.community],
|
<button
|
||||||
description: e.target.value
|
className="btn btn-outline-danger btn-icon btn-sm"
|
||||||
}
|
onClick={() => handleDelete(filter)}
|
||||||
})}
|
>
|
||||||
onKeyDown={(e) => handleEditKeyDown(e, filter.community)}
|
<IconTrash size={16} />
|
||||||
style={{minWidth: '120px'}}
|
</button>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -542,6 +659,19 @@ function FilterManager() {
|
|||||||
onClose={() => setPreviewModalOpen(false)}
|
onClose={() => setPreviewModalOpen(false)}
|
||||||
onCopy={copyConfigToClipboard}
|
onCopy={copyConfigToClipboard}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Модальное окно добавления сервера */}
|
||||||
|
<AddServerModal
|
||||||
|
show={serverConfigModalOpen}
|
||||||
|
newServer={newServer}
|
||||||
|
onNewServerChange={setNewServer}
|
||||||
|
onAddServer={handleAddServer}
|
||||||
|
onClose={() => {
|
||||||
|
setServerConfigModalOpen(false);
|
||||||
|
setNewServer({ name: '', description: '', enabled: true });
|
||||||
|
}}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
</div>
|
</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;
|
export default FilterManager;
|
||||||
Reference in New Issue
Block a user