UNPKG

wise-json-db

Version:

Blazing fast, crash-proof embedded JSON database for Node.js with batch operations, TTL, indexes, and segmented checkpointing.

278 lines (249 loc) 11.9 kB
// explorer/views/script.js document.addEventListener('DOMContentLoaded', () => { // --- Глобальные переменные и состояние --- const state = { collections: [], currentCollection: null, documents: [], indexes: [], currentPage: 0, pageSize: 10, totalDocs: 0, // Приблизительное количество для пагинации filter: {}, sort: { field: '_id', order: 'asc' }, writeMode: false, }; // --- DOM элементы --- const collectionSelect = document.getElementById('collectionSelect'); const refreshBtn = document.getElementById('refreshBtn'); const applyBtn = document.getElementById('applyBtn'); const filterInput = document.getElementById('filterInput'); const sortInput = document.getElementById('sortInput'); const orderSelect = document.getElementById('orderSelect'); const pageSizeInput = document.getElementById('pageSizeInput'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); const pageInfo = document.getElementById('pageInfo'); const dataTable = document.getElementById('dataTable'); const documentViewer = document.getElementById('documentViewer'); const indexList = document.getElementById('index-list'); const createIndexForm = document.getElementById('create-index-form'); const indexFieldInput = document.getElementById('indexFieldInput'); const uniqueCheckbox = document.getElementById('uniqueCheckbox'); const createIndexBtn = document.getElementById('createIndexBtn'); const serverModeEl = document.getElementById('server-mode'); // --- API Функции --- async function apiFetch(url, options = {}) { try { const response = await fetch(url, options); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `HTTP error! status: ${response.status}`); } // Для DELETE запросов, которые могут не вернуть тело const contentType = response.headers.get("content-type"); if (contentType && contentType.indexOf("application/json") !== -1) { return response.json(); } return {}; } catch (error) { alert(`API Error: ${error.message}`); console.error('API Fetch Error:', error); throw error; } } // --- Функции рендеринга --- function renderCollections() { collectionSelect.innerHTML = '<option value="">-- Select a collection --</option>'; state.collections.forEach(col => { const option = document.createElement('option'); option.value = col.name; option.textContent = `${col.name} (${col.count} docs)`; collectionSelect.appendChild(option); }); if (state.currentCollection) { collectionSelect.value = state.currentCollection; } } function renderDocuments() { const thead = dataTable.querySelector('thead'); const tbody = dataTable.querySelector('tbody'); thead.innerHTML = ''; tbody.innerHTML = ''; if (state.documents.length === 0) { tbody.innerHTML = '<tr><td colspan="100%">No documents found for the current selection.</td></tr>'; return; } const headers = new Set(['_actions']); state.documents.forEach(doc => Object.keys(doc).forEach(key => headers.add(key))); const headerRow = document.createElement('tr'); headers.forEach(key => { const th = document.createElement('th'); th.textContent = key; headerRow.appendChild(th); }); thead.appendChild(headerRow); state.documents.forEach(doc => { const row = document.createElement('tr'); row.addEventListener('click', () => { documentViewer.value = JSON.stringify(doc, null, 2); document.querySelectorAll('#dataTable tr').forEach(r => r.classList.remove('selected')); row.classList.add('selected'); }); headers.forEach(header => { const td = document.createElement('td'); if (header === '_actions') { const viewBtn = document.createElement('button'); viewBtn.textContent = 'View'; viewBtn.onclick = (e) => { e.stopPropagation(); documentViewer.value = JSON.stringify(doc, null, 2); }; td.appendChild(viewBtn); if (state.writeMode) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = 'Delete'; deleteBtn.className = 'delete-btn write-op'; deleteBtn.onclick = (e) => { e.stopPropagation(); handleDeleteDocument(doc._id); }; td.appendChild(deleteBtn); } } else { const value = doc[header]; td.textContent = (typeof value === 'object' && value !== null) ? JSON.stringify(value) : value; } row.appendChild(td); }); tbody.appendChild(row); }); } function renderIndexes() { indexList.innerHTML = ''; if (state.indexes.length === 0) { indexList.innerHTML = '<p>No indexes defined for this collection.</p>'; } else { state.indexes.forEach(index => { const item = document.createElement('div'); item.className = `index-item ${index.type}`; item.textContent = index.fieldName; if (state.writeMode) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '×'; deleteBtn.className = 'delete-btn write-op'; deleteBtn.title = `Delete index on ${index.fieldName}`; deleteBtn.onclick = () => handleDeleteIndex(index.fieldName); item.appendChild(deleteBtn); } indexList.appendChild(item); }); } } function renderPagination() { pageInfo.textContent = `Page ${state.currentPage + 1}`; prevBtn.disabled = state.currentPage === 0; // Приблизительная логика для кнопки "Next" nextBtn.disabled = state.documents.length < state.pageSize; } function updateWriteModeUI() { document.querySelectorAll('.write-op').forEach(el => { el.style.display = state.writeMode ? 'inline-block' : 'none'; }); if (state.writeMode) { serverModeEl.textContent = 'Server Mode: Write-Enabled'; serverModeEl.className = 'server-mode-write-enabled'; } } // --- Логика обработчиков событий --- async function loadInitialData() { try { state.collections = await apiFetch('/api/collections'); renderCollections(); // Попытка определить, включен ли режим записи const testWrite = await fetch(`/api/collections/test/indexes/test`, { method: 'DELETE' }); state.writeMode = testWrite.status !== 403; updateWriteModeUI(); } catch (e) { /* ignore */ } } async function loadCollectionData() { if (!state.currentCollection) return; // Загрузка документов const offset = state.currentPage * state.pageSize; const query = new URLSearchParams({ limit: state.pageSize, offset: offset, sort: state.sort.field, order: state.sort.order, filter: JSON.stringify(state.filter) }); state.documents = await apiFetch(`/api/collections/${state.currentCollection}?${query}`); // Загрузка статистики и индексов const statsData = await apiFetch(`/api/collections/${state.currentCollection}/stats`); state.indexes = statsData.indexes || []; renderDocuments(); renderIndexes(); renderPagination(); } async function handleSelectCollection() { state.currentCollection = collectionSelect.value; state.currentPage = 0; if (state.currentCollection) { await loadCollectionData(); } else { // Очистить все state.documents = []; state.indexes = []; renderDocuments(); renderIndexes(); } } function handleApplyFilters() { state.currentPage = 0; try { state.filter = filterInput.value ? JSON.parse(filterInput.value) : {}; } catch (e) { alert('Invalid JSON in filter input.'); return; } state.sort.field = sortInput.value || '_id'; state.sort.order = orderSelect.value; state.pageSize = parseInt(pageSizeInput.value, 10) || 10; loadCollectionData(); } async function handleDeleteDocument(docId) { if (!confirm(`Are you sure you want to delete document with ID: ${docId}?`)) return; await apiFetch(`/api/collections/${state.currentCollection}/doc/${docId}`, { method: 'DELETE' }); loadCollectionData(); // Перезагрузить данные } async function handleCreateIndex() { const fieldName = indexFieldInput.value.trim(); if (!fieldName) { alert('Index field name cannot be empty.'); return; } const isUnique = uniqueCheckbox.checked; await apiFetch(`/api/collections/${state.currentCollection}/indexes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fieldName: fieldName, unique: isUnique }) }); indexFieldInput.value = ''; uniqueCheckbox.checked = false; loadCollectionData(); // Перезагрузить данные для отображения нового индекса } async function handleDeleteIndex(fieldName) { if (!confirm(`Are you sure you want to delete the index on field: "${fieldName}"?`)) return; await apiFetch(`/api/collections/${state.currentCollection}/indexes/${fieldName}`, { method: 'DELETE' }); loadCollectionData(); // Перезагрузить } // --- Назначение обработчиков --- collectionSelect.addEventListener('change', handleSelectCollection); refreshBtn.addEventListener('click', loadInitialData); applyBtn.addEventListener('click', handleApplyFilters); prevBtn.addEventListener('click', () => { if(state.currentPage > 0) { state.currentPage--; loadCollectionData(); }}); nextBtn.addEventListener('click', () => { state.currentPage++; loadCollectionData(); }); createIndexBtn.addEventListener('click', handleCreateIndex); // --- Инициализация --- loadInitialData(); });