feat: Оптимизация маршрутов для получения данных из S3 с использованием вспомогательных функций getS3JsonWithHeaders и getS3TextWithHeaders для улучшения читаемости и обработки ошибок. Упрощение кода и улучшение обработки конфигураций серверов и фильтров.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Универсальный хук для управления данными с CRUD операциями
|
||||
* Устраняет дублирование кода в DomainsNewManager, ASNsNewManager, IPRangesManager
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import api from '../lib/api.js';
|
||||
|
||||
/**
|
||||
* @param {Object} config - Конфигурация хука
|
||||
* @param {string} config.apiEndpoint - API endpoint (например '/domains-new')
|
||||
* @param {string} config.itemKey - Ключ элемента для идентификации (например 'domain', 'asn', 'ipRange')
|
||||
* @param {function} config.validateItem - Функция валидации элемента (item) => boolean
|
||||
* @param {function} config.validateCommunity - Функция валидации community (value) => boolean
|
||||
* @param {function} config.transformResponse - Функция трансформации ответа API
|
||||
* @param {function} config.transformForSave - Функция трансформации данных перед сохранением
|
||||
* @param {string} config.saveBodyKey - Ключ для body при сохранении (по умолчанию 'domains')
|
||||
*/
|
||||
export function useDataManager(config) {
|
||||
const {
|
||||
apiEndpoint,
|
||||
itemKey,
|
||||
validateItem,
|
||||
validateCommunity,
|
||||
transformResponse = (data) => data,
|
||||
transformForSave = (data) => data,
|
||||
saveBodyKey = 'domains',
|
||||
} = config;
|
||||
|
||||
const [items, setItems] = useState([]);
|
||||
const [originalItems, setOriginalItems] = useState([]);
|
||||
const [etag, setEtag] = useState('');
|
||||
const [lastModified, setLastModified] = useState('');
|
||||
const [contentLength, setContentLength] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalItems, setTotalItems] = useState(0);
|
||||
const [sortField, setSortField] = useState(itemKey);
|
||||
const [sortOrder, setSortOrder] = useState('asc');
|
||||
const [filterCommunity, setFilterCommunity] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [editingValue, setEditingValue] = useState('');
|
||||
|
||||
const didInit = useRef(false);
|
||||
const pageSize = 10;
|
||||
|
||||
// Загрузка данных
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.get(apiEndpoint, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
|
||||
const transformed = transformResponse(payload);
|
||||
const total = transformed.length;
|
||||
|
||||
setItems(transformed);
|
||||
setOriginalItems(transformed);
|
||||
setTotalItems(Number.isFinite(total) ? total : 0);
|
||||
setEtag(response.headers?.etag || '');
|
||||
setLastModified(response.headers?.['last-modified'] || '');
|
||||
const lengthHeader = response.headers?.['content-length-source'];
|
||||
setContentLength(typeof lengthHeader !== 'undefined' ? Number(lengthHeader) : null);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${apiEndpoint}:`, error);
|
||||
return { success: false, error };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!didInit.current) {
|
||||
didInit.current = true;
|
||||
fetchItems();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Добавление элемента
|
||||
const addItem = (newItem) => {
|
||||
const key = newItem[itemKey];
|
||||
if (!key) return { success: false, error: 'Invalid item key' };
|
||||
|
||||
setItems(prev => [...prev, newItem]);
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
// Редактирование элемента
|
||||
const updateItem = (itemKeyValue, updates) => {
|
||||
setItems(prev => prev.map(item =>
|
||||
item[itemKey] === itemKeyValue ? { ...item, ...updates } : item
|
||||
));
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
// Удаление элемента
|
||||
const deleteItem = (itemKeyValue) => {
|
||||
setItems(prev => prev.filter(item => item[itemKey] !== itemKeyValue));
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
// Дедупликация
|
||||
const deduplicate = (arr) => {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const item of arr) {
|
||||
const key = String(item[itemKey]).trim().toLowerCase();
|
||||
if (!key) continue;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ ...item, [itemKey]: key });
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// Вычисление diff
|
||||
const computeDiff = (before, after) => {
|
||||
const mapBefore = new Map(before.map(i => [i[itemKey], i]));
|
||||
const mapAfter = new Map(after.map(i => [i[itemKey], i]));
|
||||
const added = [];
|
||||
const removed = [];
|
||||
const changed = [];
|
||||
|
||||
for (const [k, v] of mapAfter) {
|
||||
if (!mapBefore.has(k)) {
|
||||
added.push(v);
|
||||
continue;
|
||||
}
|
||||
const prev = mapBefore.get(k);
|
||||
if (String(prev.community) !== String(v.community)) {
|
||||
changed.push({ from: prev, to: v });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of mapBefore) {
|
||||
if (!mapAfter.has(k)) removed.push(v);
|
||||
}
|
||||
|
||||
return { added, removed, changed };
|
||||
};
|
||||
|
||||
// Сохранение изменений
|
||||
const saveChanges = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Валидируем и дедуплицируем
|
||||
const valid = items.filter(item => {
|
||||
const itemValid = validateItem ? validateItem(item[itemKey]) : true;
|
||||
const communityValid = validateCommunity ? validateCommunity(item.community) : true;
|
||||
return itemValid && communityValid;
|
||||
});
|
||||
const unique = deduplicate(valid);
|
||||
|
||||
// Загружаем полный список, мержим и сохраняем
|
||||
const fullRes = await api.get(apiEndpoint, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||
const fullPayload = Array.isArray(fullRes.data?.items) ? fullRes.data.items : [];
|
||||
const full = transformResponse(fullPayload);
|
||||
const fullMap = new Map(full.map(i => [String(i[itemKey]).trim().toLowerCase(), i]));
|
||||
const originalPageMap = new Map(originalItems.map(i => [String(i[itemKey]).trim().toLowerCase(), true]));
|
||||
const uniqueMap = new Map(unique.map(i => [String(i[itemKey]).trim().toLowerCase(), i]));
|
||||
|
||||
// Удаляем то, что было на странице но отсутствует в изменённом наборе
|
||||
for (const key of originalPageMap.keys()) {
|
||||
if (!uniqueMap.has(key)) fullMap.delete(key);
|
||||
}
|
||||
|
||||
// Добавляем/обновляем
|
||||
for (const [key, val] of uniqueMap.entries()) {
|
||||
fullMap.set(key, val);
|
||||
}
|
||||
|
||||
const fullToSave = Array.from(fullMap.values());
|
||||
const transformed = transformForSave(fullToSave);
|
||||
|
||||
const et = fullRes?.headers?.etag || etag;
|
||||
const response = await api.post(apiEndpoint, { [saveBodyKey]: transformed, etag: et }, { validateStatus: () => true });
|
||||
|
||||
if (response.status === 412) {
|
||||
return { success: false, error: 'ETag mismatch', code: 412 };
|
||||
}
|
||||
|
||||
if (response.status >= 400) {
|
||||
return { success: false, error: `Save failed with status ${response.status}` };
|
||||
}
|
||||
|
||||
setEtag(response.headers?.etag || et);
|
||||
await fetchItems();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error saving changes:', error);
|
||||
return { success: false, error };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Сортировка
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
let valA = a[sortField] || '';
|
||||
let valB = b[sortField] || '';
|
||||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||||
if (typeof valB === 'string') valB = valB.toLowerCase();
|
||||
if (valA < valB) return sortOrder === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return sortOrder === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Фильтрация
|
||||
const filtered = sortedItems.filter(item => {
|
||||
const byCommunity = !filterCommunity || item.community === filterCommunity;
|
||||
const term = String(searchTerm || '').toLowerCase();
|
||||
const bySearch = !term || String(item[itemKey]).toLowerCase().includes(term);
|
||||
return byCommunity && bySearch;
|
||||
});
|
||||
|
||||
// Пагинация
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
|
||||
// Уникальные community для фильтра
|
||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
||||
|
||||
// Управление сортировкой
|
||||
const handleSort = (field) => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder('asc');
|
||||
}
|
||||
};
|
||||
|
||||
// Инлайн редактирование
|
||||
const startEdit = (item) => {
|
||||
setEditingItem(item[itemKey]);
|
||||
setEditingValue(item.community);
|
||||
};
|
||||
|
||||
const saveEdit = () => {
|
||||
if (!editingItem) return { success: false };
|
||||
updateItem(editingItem, { community: editingValue.trim() });
|
||||
setEditingItem(null);
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingItem(null);
|
||||
};
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
items,
|
||||
originalItems,
|
||||
setItems,
|
||||
loading,
|
||||
etag,
|
||||
lastModified,
|
||||
contentLength,
|
||||
|
||||
// Пагинация
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalItems,
|
||||
totalPages,
|
||||
pageSize,
|
||||
paginatedItems,
|
||||
|
||||
// Сортировка
|
||||
sortField,
|
||||
sortOrder,
|
||||
handleSort,
|
||||
|
||||
// Фильтрация
|
||||
filterCommunity,
|
||||
setFilterCommunity,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
filtered,
|
||||
allCommunities,
|
||||
|
||||
// CRUD
|
||||
fetchItems,
|
||||
addItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
saveChanges,
|
||||
deduplicate,
|
||||
computeDiff,
|
||||
|
||||
// Инлайн редактирование
|
||||
editingItem,
|
||||
editingValue,
|
||||
setEditingValue,
|
||||
startEdit,
|
||||
saveEdit,
|
||||
cancelEdit,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user