feat: Add ConfirmDialog component for enhanced user confirmation in DataManager, FilterManager, and ServerManager; integrate html-to-image library for image conversion functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m35s

This commit is contained in:
2025-08-11 21:22:30 +07:00
parent af9ef12ba8
commit f44acbff98
5 changed files with 93 additions and 31 deletions
+7
View File
@@ -13,6 +13,7 @@
"@tanstack/react-query": "^5.56.2",
"@xyflow/react": "^12.3.4",
"axios": "^1.10.0",
"html-to-image": "^1.11.11",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^6.30.1",
@@ -1688,6 +1689,12 @@
"node": ">= 0.4"
}
},
"node_modules/html-to-image": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
"license": "MIT"
},
"node_modules/ignore": {
"version": "5.3.2",
"dev": true,
+1
View File
@@ -14,6 +14,7 @@
"@tabler/icons-react": "^3.34.0",
"axios": "^1.10.0",
"@tanstack/react-query": "^5.56.2",
"html-to-image": "^1.11.11",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^6.30.1",
+20 -8
View File
@@ -14,6 +14,7 @@ import {
IconUpload,
IconAlertTriangle
} from '@tabler/icons-react';
import ConfirmDialog from './components/ConfirmDialog.jsx'
const API_URL = '/api';
@@ -36,6 +37,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
const [filterType, setFilterType] = useState('');
const editInputRef = useRef(null);
const pageSize = 10;
const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null });
useEffect(() => {
fetchItems();
@@ -118,14 +120,16 @@ function DataManager({ entityName, entityKey, placeholder }) {
setError('Оба поля для массового обновления должны быть заполнены.');
return;
}
const confirm = window.confirm(`Вы уверены, что хотите заменить все шлюзы "${bulkFrom}" на "${bulkTo}"? Это действие необратимо.`);
if (confirm) {
const updatedItems = items.map(i =>
i.type === bulkFrom ? { ...i, type: bulkTo } : i
);
setItems(updatedItems);
setSuccess(`Шлюзы "${bulkFrom}" были успешно заменены на "${bulkTo}". Не забудьте сохранить изменения.`);
}
setConfirmState({
open: true,
text: `Вы уверены, что хотите заменить все шлюзы "${bulkFrom}" на "${bulkTo}"? Это действие необратимо.`,
onConfirm: () => {
const updatedItems = items.map(i => i.type === bulkFrom ? { ...i, type: bulkTo } : i);
setItems(updatedItems);
setSuccess(`Шлюзы "${bulkFrom}" были успешно заменены на "${bulkTo}". Не забудьте сохранить изменения.`);
setConfirmState({ open: false, text: '', onConfirm: null });
},
});
};
// Сортировка
@@ -500,6 +504,14 @@ function DataManager({ entityName, entityKey, placeholder }) {
</div>
</div>
)}
<ConfirmDialog
open={confirmState.open}
title="Подтверждение"
message={confirmState.text}
onCancel={() => setConfirmState({ open: false, text: '', onConfirm: null })}
onConfirm={() => confirmState.onConfirm?.()}
/>
</>
);
}
+45 -15
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import api from './lib/api.js';
import ConfirmDialog from './components/ConfirmDialog.jsx';
import {
IconPlus,
IconSearch,
@@ -75,6 +76,7 @@ function FilterManager() {
const [editingKey, setEditingKey] = useState(null);
const [editingDraft, setEditingDraft] = useState({ community: '', gateway: '', description: '' });
const [selectedFilterKeys, setSelectedFilterKeys] = useState(new Set());
const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null });
// Communities directory for autocomplete
const [communities, setCommunities] = useState([]);
@@ -239,12 +241,15 @@ function FilterManager() {
return;
}
const confirm = window.confirm(
`Скопировать конфигурацию и фильтры с сервера "${sourceServer.name}" на ${targetServers.length} сервер(ов)?\n\n` +
`Целевые серверы: ${targetServers.map(s => s.name).join(', ')}`
);
if (!confirm) return;
let proceed = false;
await new Promise((resolve) => {
setConfirmState({
open: true,
text: `Скопировать конфигурацию и фильтры с сервера "${sourceServer.name}" на ${targetServers.length} сервер(ов)?\n\nЦелевые серверы: ${targetServers.map(s => s.name).join(', ')}`,
onConfirm: () => { proceed = true; setConfirmState({ open: false, text: '', onConfirm: null }); resolve(); },
});
});
if (!proceed) return;
setLoading(true);
try {
@@ -334,12 +339,15 @@ function FilterManager() {
return;
}
const confirm = window.confirm(
`Развернуть фильтры и конфигурацию с сервера "${selectedServer.name}" на ${enabledServers.length} сервер(ов)?\n\n` +
`Целевые серверы: ${enabledServers.map(s => s.name).join(', ')}`
);
if (!confirm) return;
let proceed = false;
await new Promise((resolve) => {
setConfirmState({
open: true,
text: `Развернуть фильтры и конфигурацию с сервера "${selectedServer.name}" на ${enabledServers.length} сервер(ов)?\n\nЦелевые серверы: ${enabledServers.map(s => s.name).join(', ')}`,
onConfirm: () => { proceed = true; setConfirmState({ open: false, text: '', onConfirm: null }); resolve(); },
});
});
if (!proceed) return;
setLoading(true);
try {
@@ -690,8 +698,15 @@ function FilterManager() {
const deleteSelectedFilters = async () => {
if (selectedFilterKeys.size === 0) return;
const confirmed = window.confirm(`Удалить выбранные фильтры (${selectedFilterKeys.size})?`);
if (!confirmed) return;
let proceed = false;
await new Promise((resolve) => {
setConfirmState({
open: true,
text: `Удалить выбранные фильтры (${selectedFilterKeys.size})?`,
onConfirm: () => { proceed = true; setConfirmState({ open: false, text: '', onConfirm: null }); resolve(); },
});
});
if (!proceed) return;
const updated = serverFilters.filter((f) => !selectedFilterKeys.has(filterKey(f)));
setServerFilters(updated);
setSelectedFilterKeys(new Set());
@@ -1679,7 +1694,15 @@ function FilterManager() {
<button
className="btn btn-outline-danger"
onClick={async () => {
if (window.confirm('Вы уверены, что хотите очистить все фильтры?')) {
let proceed = false;
await new Promise((resolve) => {
setConfirmState({
open: true,
text: 'Вы уверены, что хотите очистить все фильтры?',
onConfirm: () => { proceed = true; setConfirmState({ open: false, text: '', onConfirm: null }); resolve(); },
});
});
if (proceed) {
try {
await api.post(`/simple-filters`, { filters: [] });
setSimpleFilters([]);
@@ -1706,6 +1729,13 @@ function FilterManager() {
{/* ... всё как было ... */}
</>
)}
<ConfirmDialog
open={confirmState.open}
title="Подтверждение"
message={confirmState.text}
onCancel={() => setConfirmState({ open: false, text: '', onConfirm: null })}
onConfirm={() => confirmState.onConfirm?.()}
/>
</div>
);
}
+20 -8
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import api from './lib/api.js';
import ConfirmDialog from './components/ConfirmDialog.jsx';
import {
IconPlus,
IconSearch,
@@ -92,6 +93,7 @@ function ServerManager() {
const [graphTunnelFilters, setGraphTunnelFilters] = useState({ GRE: true, IPSec: true, WireGuard: true, OpenVPN: true });
const [graphCountryFilter, setGraphCountryFilter] = useState('');
const [graphProviderFilter, setGraphProviderFilter] = useState('');
const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null });
useEffect(() => {
fetchServers();
@@ -332,14 +334,16 @@ function ServerManager() {
setError('Оба поля для массового обновления должны быть заполнены.');
return;
}
const confirm = window.confirm(`Вы уверены, что хотите заменить все туннели "${bulkFrom}" на "${bulkTo}"? Это действие необратимо.`);
if (confirm) {
const updatedServers = servers.map(s =>
s.tunnel === bulkFrom ? { ...s, tunnel: bulkTo } : s
);
setServers(updatedServers);
setSuccess(`Туннели "${bulkFrom}" были успешно заменены на "${bulkTo}". Не забудьте сохранить изменения.`);
}
setConfirmState({
open: true,
text: `Вы уверены, что хотите заменить все туннели "${bulkFrom}" на "${bulkTo}"? Это действие необратимо.`,
onConfirm: () => {
const updatedServers = servers.map(s => s.tunnel === bulkFrom ? { ...s, tunnel: bulkTo } : s);
setServers(updatedServers);
setSuccess(`Туннели "${bulkFrom}" были успешно заменены на "${bulkTo}". Не забудьте сохранить изменения.`);
setConfirmState({ open: false, text: '', onConfirm: null });
},
});
};
// Сортировка
@@ -890,6 +894,14 @@ function ServerManager() {
</div>
</div>
)}
<ConfirmDialog
open={confirmState.open}
title="Подтверждение"
message={confirmState.text}
onCancel={() => setConfirmState({ open: false, text: '', onConfirm: null })}
onConfirm={() => confirmState.onConfirm?.()}
/>
</>
);
}