feat: Refactor FilterManager to streamline gateway selection and remove inline editing, enhancing user experience and simplifying filter management.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m4s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m4s
This commit is contained in:
+171
-137
@@ -140,9 +140,7 @@ function FilterManager() {
|
|||||||
const [serverListModalOpen, setServerListModalOpen] = useState(false);
|
const [serverListModalOpen, setServerListModalOpen] = useState(false);
|
||||||
const [serverSearch, setServerSearch] = useState('');
|
const [serverSearch, setServerSearch] = useState('');
|
||||||
|
|
||||||
// Состояние для инлайн-редактирования и массовых действий
|
// Состояние для массовых действий
|
||||||
const [editingKey, setEditingKey] = useState(null);
|
|
||||||
const [editingDraft, setEditingDraft] = useState({ community: '', gateway: '', description: '' });
|
|
||||||
const [selectedFilterKeys, setSelectedFilterKeys] = useState(new Set());
|
const [selectedFilterKeys, setSelectedFilterKeys] = useState(new Set());
|
||||||
const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null });
|
const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null });
|
||||||
|
|
||||||
@@ -151,6 +149,39 @@ function FilterManager() {
|
|||||||
// Inventory servers (from /servers endpoint)
|
// Inventory servers (from /servers endpoint)
|
||||||
const [inventoryServers, setInventoryServers] = useState([]);
|
const [inventoryServers, setInventoryServers] = useState([]);
|
||||||
|
|
||||||
|
// Собираем все gateways из всех серверов для выбора
|
||||||
|
const allGateways = (() => {
|
||||||
|
const gws = [];
|
||||||
|
inventoryServers.forEach(srv => {
|
||||||
|
if (Array.isArray(srv.gateways)) {
|
||||||
|
srv.gateways.forEach(gw => {
|
||||||
|
if (gw.name && !gws.some(g => g.name === gw.name)) {
|
||||||
|
gws.push({
|
||||||
|
name: gw.name,
|
||||||
|
ip: gw.ip || '',
|
||||||
|
comment: gw.comment || '',
|
||||||
|
serverDns: srv.dns || srv.hostName || '',
|
||||||
|
country: srv.country || '',
|
||||||
|
primary: gw.primary
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Также добавляем старое поле gateway если есть
|
||||||
|
if (srv.gateway && !gws.some(g => g.name === srv.gateway)) {
|
||||||
|
gws.push({
|
||||||
|
name: srv.gateway,
|
||||||
|
ip: '',
|
||||||
|
comment: 'Legacy gateway',
|
||||||
|
serverDns: srv.dns || srv.hostName || '',
|
||||||
|
country: srv.country || '',
|
||||||
|
primary: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return gws.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
})();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -713,44 +744,6 @@ function FilterManager() {
|
|||||||
|
|
||||||
// Убрано сохранение режима в localStorage по просьбе пользователя
|
// Убрано сохранение режима в localStorage по просьбе пользователя
|
||||||
|
|
||||||
const startInlineEdit = (filter) => {
|
|
||||||
if (!filter) return;
|
|
||||||
setEditingKey(filterKey(filter));
|
|
||||||
setEditingDraft({
|
|
||||||
community: filter.community || '',
|
|
||||||
gateway: filter.gateway || '',
|
|
||||||
description: filter.description || ''
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelInlineEdit = () => {
|
|
||||||
setEditingKey(null);
|
|
||||||
setEditingDraft({ community: '', gateway: '', description: '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveInlineEdit = async () => {
|
|
||||||
if (!editingDraft.community.trim() || !editingDraft.gateway.trim()) {
|
|
||||||
setError('Поля Community и Gateway должны быть заполнены.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const updated = serverFilters.map((f) =>
|
|
||||||
filterKey(f) === editingKey
|
|
||||||
? { community: editingDraft.community.trim(), gateway: editingDraft.gateway.trim(), description: editingDraft.description }
|
|
||||||
: f
|
|
||||||
);
|
|
||||||
setServerFilters(updated);
|
|
||||||
cancelInlineEdit();
|
|
||||||
setSelectedFilterKeys(new Set());
|
|
||||||
try {
|
|
||||||
await api.post(`/server-filters/${selectedServer.id}`, { filters: updated });
|
|
||||||
setSuccess('Фильтр успешно обновлен!');
|
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving server filters:', error);
|
|
||||||
setError('Не удалось сохранить фильтр.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSelectFilter = (key) => {
|
const toggleSelectFilter = (key) => {
|
||||||
setSelectedFilterKeys((prev) => {
|
setSelectedFilterKeys((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -1250,10 +1243,9 @@ function FilterManager() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{processedFilters.filter(f => f != null).map((filter, index) => {
|
{processedFilters.filter(f => f != null).map((filter, index) => {
|
||||||
const key = filterKey(filter);
|
const key = filterKey(filter);
|
||||||
const isEditing = editingKey === key;
|
|
||||||
const isSelected = selectedFilterKeys.has(key);
|
const isSelected = selectedFilterKeys.has(key);
|
||||||
return (
|
return (
|
||||||
<tr key={`${filter?.community || ''}-${index}`} className={isEditing ? 'table-info' : ''}>
|
<tr key={`${filter?.community || ''}-${index}`}>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
className="form-check-input m-0 align-middle"
|
className="form-check-input m-0 align-middle"
|
||||||
@@ -1264,87 +1256,49 @@ function FilterManager() {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{isEditing ? (
|
<div className="d-flex align-items-center">
|
||||||
<input
|
<span className="badge bg-blue-lt text-blue me-2">
|
||||||
className="form-control form-control-sm"
|
<IconHash size={12} />
|
||||||
value={editingDraft.community}
|
</span>
|
||||||
onChange={(e) => setEditingDraft({ ...editingDraft, community: e.target.value })}
|
<code className="text-blue">{filter?.community || ''}</code>
|
||||||
autoFocus
|
</div>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<span className="badge bg-blue-lt text-blue me-2">
|
|
||||||
<IconHash size={12} />
|
|
||||||
</span>
|
|
||||||
<code className="text-blue">{filter?.community || ''}</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{isEditing ? (
|
<div className="d-flex align-items-center">
|
||||||
<input
|
<span className="badge bg-green-lt text-green me-2">
|
||||||
className="form-control form-control-sm"
|
<IconServer size={12} />
|
||||||
value={editingDraft.gateway}
|
</span>
|
||||||
onChange={(e) => setEditingDraft({ ...editingDraft, gateway: e.target.value })}
|
<code className="text-green">{filter?.gateway || ''}</code>
|
||||||
/>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<span className="badge bg-green-lt text-green me-2">
|
|
||||||
<IconServer size={12} />
|
|
||||||
</span>
|
|
||||||
<code className="text-green">{filter?.gateway || ''}</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{isEditing ? (
|
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter?.description || ''}>
|
||||||
<input
|
{filter?.description ? (
|
||||||
className="form-control form-control-sm"
|
<span className="text-muted">{filter.description}</span>
|
||||||
value={editingDraft.description || ''}
|
) : (
|
||||||
onChange={(e) => setEditingDraft({ ...editingDraft, description: e.target.value })}
|
<span className="text-muted fst-italic">Без описания</span>
|
||||||
/>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter?.description || ''}>
|
|
||||||
{filter?.description ? (
|
|
||||||
<span className="text-muted">{filter.description}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted fst-italic">Без описания</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
{isEditing ? (
|
<button
|
||||||
<>
|
className="btn btn-outline-primary btn-icon btn-sm"
|
||||||
<button className="btn btn-success btn-icon btn-sm" onClick={saveInlineEdit} title="Сохранить">
|
onClick={() => handleEditFilter(filter, index)}
|
||||||
<IconCheck size={16} />
|
title="Редактировать фильтр"
|
||||||
</button>
|
>
|
||||||
<button className="btn btn-secondary btn-icon btn-sm" onClick={cancelInlineEdit} title="Отмена">
|
<IconEdit size={16} />
|
||||||
<IconX size={16} />
|
</button>
|
||||||
</button>
|
<button
|
||||||
</>
|
className="btn btn-outline-danger btn-icon btn-sm"
|
||||||
) : (
|
onClick={() => {
|
||||||
<>
|
setFilterToDelete(filter);
|
||||||
<button
|
setDeleteFilterModalOpen(true);
|
||||||
className="btn btn-outline-primary btn-icon btn-sm"
|
}}
|
||||||
onClick={() => startInlineEdit(filter)}
|
title="Удалить фильтр"
|
||||||
title="Редактировать фильтр"
|
>
|
||||||
>
|
<IconTrash size={16} />
|
||||||
<IconEdit size={16} />
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-danger btn-icon btn-sm"
|
|
||||||
onClick={() => {
|
|
||||||
setFilterToDelete(filter);
|
|
||||||
setDeleteFilterModalOpen(true);
|
|
||||||
}}
|
|
||||||
title="Удалить фильтр"
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1403,16 +1357,50 @@ function FilterManager() {
|
|||||||
helpText="Введите community в формате AS:VALUE"
|
helpText="Введите community в формате AS:VALUE"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<div className="mb-3">
|
||||||
label="Gateway"
|
<label className="form-label required">Gateway</label>
|
||||||
name="gateway"
|
<select
|
||||||
value={newFilter.gateway}
|
className="form-select"
|
||||||
onChange={(val) => setNewFilter({ ...newFilter, gateway: val })}
|
value={newFilter.gateway}
|
||||||
placeholder="192.168.1.1 или gateway-name"
|
onChange={(e) => setNewFilter({ ...newFilter, gateway: e.target.value })}
|
||||||
required
|
required
|
||||||
icon={IconServer}
|
>
|
||||||
helpText="IP адрес или имя шлюза"
|
<option value="">Выберите gateway...</option>
|
||||||
/>
|
{allGateways.map((gw) => (
|
||||||
|
<option key={gw.name} value={gw.name}>
|
||||||
|
{gw.name}{gw.ip ? ` (${gw.ip})` : ''}{gw.serverDns ? ` — ${gw.serverDns}` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{allGateways.length === 0 && (
|
||||||
|
<div className="form-text text-warning">
|
||||||
|
<IconAlertTriangle size={14} className="me-1" />
|
||||||
|
Нет доступных gateway. Добавьте шлюзы в разделе Серверы.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{newFilter.gateway && (
|
||||||
|
<div className="mt-2">
|
||||||
|
{(() => {
|
||||||
|
const selected = allGateways.find(g => g.name === newFilter.gateway);
|
||||||
|
if (!selected) return null;
|
||||||
|
return (
|
||||||
|
<div className="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
|
<span className="avatar avatar-xs bg-primary-lt">
|
||||||
|
{selected.country ? countryToFlag(selected.country) : <IconServer size={14} />}
|
||||||
|
</span>
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
<div className="fw-bold">{selected.name}</div>
|
||||||
|
<div className="text-muted small">
|
||||||
|
{[selected.ip, selected.serverDns, selected.comment].filter(Boolean).join(' · ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selected.primary && <span className="badge bg-green-lt text-green">Основной</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Описание"
|
label="Описание"
|
||||||
@@ -1430,7 +1418,7 @@ function FilterManager() {
|
|||||||
setEditFilterModalOpen(false);
|
setEditFilterModalOpen(false);
|
||||||
setEditingFilter(null);
|
setEditingFilter(null);
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSaveEditFilter}
|
onSubmit={() => handleSaveEditFilter(editingFilter)}
|
||||||
title="Редактировать фильтр"
|
title="Редактировать фильтр"
|
||||||
submitLabel="Сохранить изменения"
|
submitLabel="Сохранить изменения"
|
||||||
submitIcon={IconCheck}
|
submitIcon={IconCheck}
|
||||||
@@ -1448,15 +1436,61 @@ function FilterManager() {
|
|||||||
icon={IconHash}
|
icon={IconHash}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<div className="mb-3">
|
||||||
label="Gateway"
|
<label className="form-label required">Gateway</label>
|
||||||
name="gateway"
|
<select
|
||||||
value={editingFilter.gateway}
|
className="form-select"
|
||||||
onChange={(val) => setEditingFilter({ ...editingFilter, gateway: val })}
|
value={editingFilter.gateway}
|
||||||
placeholder="192.168.1.1 или gateway-name"
|
onChange={(e) => setEditingFilter({ ...editingFilter, gateway: e.target.value })}
|
||||||
required
|
required
|
||||||
icon={IconServer}
|
>
|
||||||
/>
|
<option value="">Выберите gateway...</option>
|
||||||
|
{allGateways.map((gw) => (
|
||||||
|
<option key={gw.name} value={gw.name}>
|
||||||
|
{gw.name}{gw.ip ? ` (${gw.ip})` : ''}{gw.serverDns ? ` — ${gw.serverDns}` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
{/* Если текущий gateway не в списке, добавляем его */}
|
||||||
|
{editingFilter.gateway && !allGateways.some(g => g.name === editingFilter.gateway) && (
|
||||||
|
<option value={editingFilter.gateway}>{editingFilter.gateway} (не найден в списке)</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{allGateways.length === 0 && (
|
||||||
|
<div className="form-text text-warning">
|
||||||
|
<IconAlertTriangle size={14} className="me-1" />
|
||||||
|
Нет доступных gateway. Добавьте шлюзы в разделе Серверы.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{editingFilter.gateway && (
|
||||||
|
<div className="mt-2">
|
||||||
|
{(() => {
|
||||||
|
const selected = allGateways.find(g => g.name === editingFilter.gateway);
|
||||||
|
if (!selected) {
|
||||||
|
return (
|
||||||
|
<div className="d-flex align-items-center gap-2 p-2 bg-warning-lt rounded">
|
||||||
|
<IconAlertTriangle size={16} className="text-warning" />
|
||||||
|
<div className="text-muted small">Gateway "{editingFilter.gateway}" не найден в списке серверов</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="d-flex align-items-center gap-2 p-2 bg-light rounded">
|
||||||
|
<span className="avatar avatar-xs bg-primary-lt">
|
||||||
|
{selected.country ? countryToFlag(selected.country) : <IconServer size={14} />}
|
||||||
|
</span>
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
<div className="fw-bold">{selected.name}</div>
|
||||||
|
<div className="text-muted small">
|
||||||
|
{[selected.ip, selected.serverDns, selected.comment].filter(Boolean).join(' · ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selected.primary && <span className="badge bg-green-lt text-green">Основной</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Описание"
|
label="Описание"
|
||||||
|
|||||||
Reference in New Issue
Block a user