Created
April 1, 2025 09:00
-
-
Save mirzap/ec6602ef1d6f4170406491eda22d43b0 to your computer and use it in GitHub Desktop.
1 million rows table - Plain JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>Paginated Events Table</title> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
} | |
#controls { | |
width: 90%; | |
max-width: 1000px; | |
margin: 20px auto; | |
display: flex; | |
flex-wrap: wrap; | |
gap: 10px; | |
justify-content: space-between; | |
} | |
#controls > * { | |
flex: 1 1 22%; | |
} | |
#table-container { | |
width: 90%; | |
max-width: 1000px; | |
margin: auto; | |
overflow-x: auto; | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
th, td { | |
padding: 8px; | |
border: 1px solid #ddd; | |
text-align: left; | |
font-size: 14px; | |
} | |
#pagination { | |
display: flex; | |
justify-content: center; | |
gap: 10px; | |
margin: 20px; | |
} | |
#pagination button:disabled { | |
opacity: 0.5; | |
cursor: not-allowed; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="controls"> | |
<input type="text" id="searchInput" placeholder="Search by name, email or message" /> | |
<select id="sortSelect"> | |
<option value="id">Sort by ID</option> | |
<option value="cc">Sort by Country Code</option> | |
<option value="plan">Sort by Plan</option> | |
<option value="size">Sort by Size</option> | |
</select> | |
<select id="planFilter"> | |
<option value="">All Plans</option> | |
<option value="free">Free</option> | |
<option value="pro">Pro</option> | |
<option value="enterprise">Enterprise</option> | |
</select> | |
<select id="sizeFilter"> | |
<option value="">All Sizes</option> | |
<option value="s">S</option> | |
<option value="m">M</option> | |
<option value="l">L</option> | |
<option value="xl">XL</option> | |
</select> | |
<button onclick="applyFiltersAndSort()">Apply</button> | |
</div> | |
<div id="table-container"> | |
<table> | |
<thead> | |
<tr> | |
<th>ID</th> | |
<th>Name</th> | |
<th>Email</th> | |
<th>Message</th> | |
<th>Country</th> | |
<th>Plan</th> | |
<th>Size</th> | |
</tr> | |
</thead> | |
<tbody id="table-body"></tbody> | |
</table> | |
</div> | |
<div id="pagination"> | |
<button id="firstPageBtn" onclick="firstPage()">First</button> | |
<button id="prevPageBtn" onclick="prevPage()">Previous</button> | |
<span id="page-info"></span> | |
<button id="nextPageBtn" onclick="nextPage()">Next</button> | |
<button id="lastPageBtn" onclick="lastPage()">Last</button> | |
</div> | |
<script> | |
const events = []; | |
let currentEntries = events; | |
let currentPage = 0; | |
const pageSize = 100; | |
function add_events(input) { | |
const lines = input.split('\n'); | |
for (let line of lines) { | |
if (line.trim()) { | |
try { | |
events.push(JSON.parse(line.trim())); | |
} catch (e) { | |
console.warn('Skipping malformed line:', line); | |
} | |
} | |
} | |
currentEntries = events; | |
renderTable(); | |
} | |
function renderTable() { | |
const tbody = document.getElementById('table-body'); | |
tbody.innerHTML = ''; | |
const totalPages = Math.ceil(currentEntries.length / pageSize); | |
const start = currentPage * pageSize; | |
const end = Math.min(currentEntries.length, start + pageSize); | |
document.getElementById('page-info').textContent = `Page ${currentPage + 1} of ${totalPages}`; | |
document.getElementById('firstPageBtn').disabled = currentPage === 0; | |
document.getElementById('prevPageBtn').disabled = currentPage === 0; | |
document.getElementById('nextPageBtn').disabled = currentPage >= totalPages - 1; | |
document.getElementById('lastPageBtn').disabled = currentPage >= totalPages - 1; | |
for (let i = start; i < end; i++) { | |
const e = currentEntries[i]; | |
const row = document.createElement('tr'); | |
row.innerHTML = ` | |
<td>${e.data.id}</td> | |
<td>${e.data.name}</td> | |
<td>${e.data.email}</td> | |
<td>${e.data.message}</td> | |
<td>${e.data.cc}</td> | |
<td>${e.data.plan}</td> | |
<td>${e.data.size}</td> | |
`; | |
tbody.appendChild(row); | |
} | |
} | |
function nextPage() { | |
const totalPages = Math.ceil(currentEntries.length / pageSize); | |
if (currentPage < totalPages - 1) { | |
currentPage++; | |
renderTable(); | |
} | |
} | |
function prevPage() { | |
if (currentPage > 0) { | |
currentPage--; | |
renderTable(); | |
} | |
} | |
function firstPage() { | |
currentPage = 0; | |
renderTable(); | |
} | |
function lastPage() { | |
const totalPages = Math.ceil(currentEntries.length / pageSize); | |
currentPage = totalPages - 1; | |
renderTable(); | |
} | |
function sortEntries(sort_by = 'id', ascending = true) { | |
const planOrder = ['free', 'pro', 'enterprise']; | |
const sizeOrder = ['s', 'm', 'l', 'xl']; | |
currentEntries = [...currentEntries].sort((a, b) => { | |
let compare = 0; | |
if (sort_by === 'id') compare = a.data.id - b.data.id; | |
else if (sort_by === 'cc') compare = a.data.cc.localeCompare(b.data.cc); | |
else if (sort_by === 'size') compare = sizeOrder.indexOf(a.data.size || 's') - sizeOrder.indexOf(b.data.size || 's'); | |
else if (sort_by === 'plan') compare = planOrder.indexOf(a.data.plan) - planOrder.indexOf(b.data.plan); | |
return ascending ? compare : -compare; | |
}); | |
} | |
function filterEntries({ type, plan, size }) { | |
currentEntries = events.filter(el => { | |
return (!type || el.type === type) && | |
(!plan || el.data.plan === plan) && | |
(!size || el.data.size === size); | |
}); | |
} | |
function search(query) { | |
const q = query.toLowerCase(); | |
currentEntries = currentEntries.filter(event => { | |
return (event.data.message || '').toLowerCase().includes(q) || | |
(event.data.name || '').toLowerCase().includes(q) || | |
(event.data.email || '').toLowerCase().includes(q); | |
}); | |
} | |
function applyFiltersAndSort() { | |
const sortBy = document.getElementById('sortSelect').value; | |
const plan = document.getElementById('planFilter').value; | |
const size = document.getElementById('sizeFilter').value; | |
const searchQuery = document.getElementById('searchInput').value; | |
currentEntries = [...events]; | |
filterEntries({ plan, size }); | |
sortEntries(sortBy); | |
if (searchQuery) { | |
search(searchQuery); | |
} | |
currentPage = 0; | |
renderTable(); | |
} | |
const simulatedInputLines = []; | |
for (let i = 0; i < 1000000; i++) { | |
simulatedInputLines.push(JSON.stringify({ | |
type: "question", | |
data: { | |
cc: "fr", | |
email: `user${i + 1}@example.com`, | |
id: i + 1, | |
message: `Message number ${i + 1}`, | |
name: `User #${i + 1}`, | |
plan: ['free', 'pro', 'enterprise'][i % 3], | |
size: ['s', 'm', 'l', 'xl'][i % 4] | |
} | |
})); | |
} | |
function addInChunks(lines, chunkSize = 5000) { | |
let index = 0; | |
function processChunk() { | |
const chunk = lines.slice(index, index + chunkSize); | |
add_events(chunk.join('\n')); | |
index += chunkSize; | |
if (index < lines.length) { | |
setTimeout(processChunk, 0); | |
} | |
} | |
processChunk(); | |
} | |
addInChunks(simulatedInputLines); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment