feat: add search and sort feature
All checks were successful
/ deploy_site (push) Successful in 2m18s

This commit is contained in:
2025-01-15 20:53:34 +09:00
parent 1bd5a5bac3
commit f6e86cb4bc
11 changed files with 584 additions and 198 deletions

View File

@ -59,6 +59,9 @@ export class WebServer {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Transfer-Encoding', 'chunked')
res.write('<script src="https://cdn.tailwindcss.com"></script>')
res.write('<body class="bg-gray-100 text-gray-800">')
res.write('<div class="container mx-auto p-4">')
res.write('<script>setInterval(() => window.scrollTo(0, document.body.scrollHeight), 100)</script>')
res.write('<pre>Start Checking....\n')
@ -72,7 +75,8 @@ export class WebServer {
Memorizer.reset()
await this.bpManager.runCheckOnce(name)
res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page`)
res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page</a>`)
res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`)
res.end()
}
@ -82,6 +86,9 @@ export class WebServer {
const { hidePass } = req.query
res.write('<script src="https://cdn.tailwindcss.com"></script>')
res.write('<body class="bg-gray-100 text-gray-800">')
res.write('<div class="container mx-auto p-4">')
res.write('<script>setInterval(() => window.scrollTo(0, document.body.scrollHeight), 100)</script>')
res.write('<pre>Start Checking....\n')
@ -89,7 +96,8 @@ export class WebServer {
await this.bpManager.runCheckAll((name) =>
res.write(`${name} - FINISHED\n`))
res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page`)
res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page</a>`)
res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`)
res.end()
}
@ -97,6 +105,9 @@ export class WebServer {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Transfer-Encoding', 'chunked')
res.write('<script src="https://cdn.tailwindcss.com"></script>')
res.write('<body class="bg-gray-100 text-gray-800">')
res.write('<div class="container mx-auto p-4">')
res.write('<pre>Start Fixing....\n')
const { name, hidePass } = req.query
@ -117,6 +128,7 @@ export class WebServer {
})
res.write(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page`)
res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`)
res.end()
}

View File

@ -1,48 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<title>BPSets</title>
</head>
<body>
<div class="container p-3">
<body class="bg-gray-100 text-gray-800">
<div class="container mx-auto p-4">
<%- include('partial/page_header.ejs') %>
<table class="table">
<thead>
<div class="relative mb-4">
<input
type="text"
id="tableFilter"
placeholder="Search..."
class="w-full px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<ul
id="autocompleteSuggestions"
class="absolute bg-white border border-gray-300 rounded shadow-md w-full max-h-48 overflow-y-auto hidden"
></ul>
</div>
<table class="min-w-full bg-white shadow rounded-lg overflow-hidden" id="bpTable">
<thead class="bg-gray-200 text-gray-600">
<tr>
<th>#</th>
<th>Name</th>
<th>B.P. Category</th>
<th>Priority</th>
<th>Fail/Pass</th>
<th>Pass Percent</th>
<th>Action</th>
<th class="py-3 px-4 cursor-pointer" data-sort="number">
# <span class="sort-indicator"><i class="fas fa-sort"></i></span>
</th>
<th class="py-3 px-4 cursor-pointer" data-sort="string">
Name <span class="sort-indicator"><i class="fas fa-sort"></i></span>
</th>
<th class="py-3 px-4 cursor-pointer" data-sort="string">
B.P. Category <span class="sort-indicator"><i class="fas fa-sort"></i></span>
</th>
<th class="py-3 px-4 cursor-pointer" data-sort="number">
Priority <span class="sort-indicator"><i class="fas fa-sort"></i></span>
</th>
<th class="py-3 px-4">Fail/Pass</th>
<th class="py-3 px-4 cursor-pointer" data-sort="number">
Pass Percent <span class="sort-indicator"><i class="fas fa-sort"></i></span>
</th>
<th class="py-3 px-4">Action</th>
</tr>
</thead>
<tbody>
<% bpStatus.forEach(({ category, metadatas }) => { %>
<tr>
<th colspan="7"><%= category %> (<%= metadatas.length %>)</th>
<tr class="bg-gray-50" data-category="<%= category %>">
<th colspan="7" class="py-3 px-4 text-left text-lg font-semibold text-gray-700">
<%= category %> (<%= metadatas.length %>)
</th>
</tr>
<% metadatas.forEach((metadata) => { %>
<%- include('./partial/bpset_item.ejs', { metadata }) %>
<%- include('./partial/bpset_item.ejs', { metadata, category }) %>
<%- include('./partial/bpset_details.ejs', { metadata }) %>
<%- include('./partial/bpset_logs.ejs', { metadata }) %>
<%- include('./partial/bpset_fixdialog.ejs', { metadata }) %>
<% }) %>
<% }) %>
</tbody>
</table>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
</script>
<%- include('./style.ejs') %>
<%- include('./script.ejs') %>
</body>
</html>

View File

@ -1,8 +1,30 @@
<td>
<div class="btn-group">
<button type="button" class="btn btn-primary" data-bs-toggle="offcanvas" data-bs-target="#fixdialog-<%= metadata.idx %>">Fix</button>
<a href="/check?name=<%= metadata.name %>&hidePass=<% hidePass %>" type="button" class="btn btn-secondary">Recheck</a>
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse" data-bs-target="#detail-<%= metadata.idx %>">Details</button>
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse" data-bs-target="#logs-<%= metadata.idx %>">Logs</button>
<td class="py-3 px-4">
<div class="flex space-x-2">
<button
type="button"
class="bg-blue-500 text-white px-3 py-2 rounded shadow hover:bg-blue-600"
data-bs-toggle="offcanvas"
data-bs-target="#fixdialog-<%= metadata.idx %>">
Fix
</button>
<a
href="/check?name=<%= metadata.name %>&hidePass=<%= hidePass %>"
class="bg-gray-500 text-white px-3 py-2 rounded shadow hover:bg-gray-600">
Recheck
</a>
<button
type="button"
class="bg-gray-500 text-white px-3 py-2 rounded shadow hover:bg-gray-600"
data-bs-toggle="collapse"
data-bs-target="#detail-<%= metadata.idx %>">
Details
</button>
<button
type="button"
class="bg-gray-500 text-white px-3 py-2 rounded shadow hover:bg-gray-600"
data-bs-toggle="collapse"
data-bs-target="#logs-<%= metadata.idx %>">
Logs
</button>
</div>
</td>

View File

@ -1,59 +1,53 @@
<%
const priorityLabel = ['CRITICAL', 'Required', 'Recommend'][metadata.priority-1] || 'Recommend'
const priorityColor = ['danger', 'warning', 'secondary'][metadata.priority-1] || 'secondary'
const priorityLabel = ['CRITICAL', 'Required', 'Recommend'][metadata.priority - 1] || 'Recommend';
const priorityColor = ['red', 'yellow', 'gray'][metadata.priority - 1] || 'gray';
%>
<tr>
<td colspan="7" class="p-0">
<div class="collapse" id="detail-<%= metadata.idx %>">
<div class="bg-light p-3">
<h3><%= metadata.name %></h3>
<p><%= metadata.description %></p>
<div class="overflow-hidden max-h-0 hidden" id="detail-<%= metadata.idx %>">
<div class="bg-gray-100 p-4">
<h3 class="text-xl font-semibold"><%= metadata.name %></h3>
<p class="text-gray-700"><%= metadata.description %></p>
<p>
Category:
<span class="badge text-bg-secondary"><%= metadata.bestPracticeCategory %></span>
<%= metadata.awsServiceCategory %> - <%= metadata.awsService %>
<p class="mt-2">
<span class="font-bold">Category:</span>
<span class="bg-gray-200 text-gray-700 px-2 py-1 rounded"><%= metadata.bestPracticeCategory %></span>
<span class="text-gray-500"><%= metadata.awsServiceCategory %> - <%= metadata.awsService %></span>
</p>
<p>
Priority:
<span class="badge text-bg-<%= priorityColor %>">
<p class="mt-2">
<span class="font-bold">Priority:</span>
<span class="bg-<%= priorityColor %>-200 text-<%= priorityColor %>-800 px-2 py-1 rounded">
<%= metadata.priority %> - <%= priorityLabel %>
</span>
<%= metadata.priorityReason %>
<span class="text-gray-500"><%= metadata.priorityReason %></span>
</p>
<h4>Operations used in check function</h4>
<div class="row row-cols-3">
<div class="mt-4">
<h4 class="text-lg font-semibold">Operations used in check function</h4>
<div class="grid grid-cols-3 gap-4 mt-2">
<% metadata.commandUsedInCheckFunction.forEach(({ name, reason }) => { %>
<div class="col">
<div class="card">
<h5 class="card-header"><%= name %></h5>
<div class="card-body">
<%= reason %>
</div>
</div>
<div class="bg-white shadow rounded p-3">
<h5 class="text-sm font-bold"><%= name %></h5>
<p class="text-sm text-gray-600"><%= reason %></p>
</div>
<% }) %>
</div>
<br>
</div>
<h4>Operations used in fix function</h4>
<div class="row row-cols-3">
<div class="mt-4">
<h4 class="text-lg font-semibold">Operations used in fix function</h4>
<div class="grid grid-cols-3 gap-4 mt-2">
<% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %>
<div class="col">
<div class="card">
<h5 class="card-header"><%= name %></h5>
<div class="card-body">
<%= reason %>
</div>
</div>
<div class="bg-white shadow rounded p-3">
<h5 class="text-sm font-bold"><%= name %></h5>
<p class="text-sm text-gray-600"><%= reason %></p>
</div>
<% }) %>
</div>
</div>
</div>
</div>
</td>
</tr>

View File

@ -1,48 +1,57 @@
<div class="offcanvas offcanvas-end" data-bs-scroll="true" tabindex="-1" id="fixdialog-<%= metadata.idx %>">
<div class="offcanvas-header d-flex gap-3">
<h3 class="offcanvas-title" style="word-break: break-all;">Fixing <%= metadata.name %></h3>
<div class="fixed inset-0 flex items-end justify-center sm:items-center bg-gray-800 bg-opacity-50 z-50 hidden opacity-0 transition-opacity duration-300" id="fixdialog-<%= metadata.idx %>">
<div class="bg-white rounded-lg shadow-lg w-full max-w-md mx-auto overflow-y-auto transform translate-y-full transition-transform duration-300">
<div class="p-4 border-b relative">
<button data-close-offcanvas class="absolute top-4 right-4 text-gray-500">&times;</button>
<h3 class="text-lg font-bold text-gray-700">Fixing <%= metadata.name %></h3>
</div>
<div class="offcanvas-body">
<h3 class="mb-3">Pending operations</h3>
<div class="p-4">
<h4 class="text-md font-semibold mb-3">Pending operations</h4>
<% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %>
<div class="card mb-3">
<h5 class="card-header"><%= name %></h5>
<div class="card-body">
<%= reason %>
</div>
<div class="bg-gray-100 p-3 rounded mb-3">
<h5 class="font-semibold text-gray-700"><%= name %></h5>
<p class="text-gray-600"><%= reason %></p>
</div>
<% }) %>
<br>
<h3 class="mb-3">Required Parameters</h3>
<h4 class="text-md font-semibold mb-3">Required Parameters</h4>
<form method="POST" action="/fix?name=<%= metadata.name %>&hidePass=<%= hidePass %>">
<% metadata.requiredParametersForFix.forEach((input) => { %>
<div class="mb-3">
<label for="<%= `${metadata.name}-${input.name}` %>" class="form-label"><%= input.name %></label>
<input type="text" class="form-control" name="<%= input.name %>" id="<%= `${metadata.name}-${input.name}` %>" value="<%= input.default %>" required>
<div class="form-text">
<div class="mb-4">
<label for="<%= `${metadata.name}-${input.name}` %>" class="block text-sm font-medium text-gray-700">
<%= input.name %>
</label>
<input
type="text"
class="outline-none mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
name="<%= input.name %>"
id="<%= `${metadata.name}-${input.name}` %>"
value="<%= input.default %>"
required
/>
<p class="mt-1 text-xs text-gray-500">
<%= input.description %><br />
ex) <code><%= input.example %></code>
</div>
<span class="font-mono"><%= input.example %></span>
</p>
</div>
<% }) %>
<br>
<% if (metadata.isFixFunctionUsesDestructiveCommand) { %>
<div class="alert alert-danger" role="alert">
This Fix Function Has DESTRUCTIVE Command! please review pending operations carefully!
<div class="p-3 bg-red-100 text-red-700 rounded mb-3">
This Fix Function Has DESTRUCTIVE Commands! Please review pending operations carefully.
</div>
<% } %>
<div class="alert alert-warning" role="alert">
<div class="p-3 bg-yellow-100 text-yellow-700 rounded mb-3">
<%= metadata.adviseBeforeFixFunction %>
</div>
<br>
<button class="btn btn-primary" type="submit">Fix!</button>
<button
type="submit"
class="w-full bg-blue-500 text-white px-4 py-2 rounded shadow hover:bg-blue-600">
Fix!
</button>
</form>
</div>
</div>
</div>

View File

@ -1,27 +1,24 @@
<%
const priorityLabel = ['CRITICAL', 'Required', 'Recommend'][metadata.priority-1] || 'Recommend'
const priorityColor = ['danger', 'warning', 'secondary'][metadata.priority-1] || 'secondary'
const priorityColor = ['red', 'yellow', 'gray'][metadata.priority-1] || 'secondary'
const failResources = metadata.nonCompliantResources.length
const isPass = failResources < 1
%>
<tr>
<td class="fw-bold">#<%= metadata.idx + 1 %></td>
<td>
<span
class="m-0"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="<%= metadata.description %>">
<tr class="border-b" data-status="<%= isPass ? 'pass' : 'fail' %>" data-category-items="<%= category %>">
<td class="font-semibold py-3 px-4">#<%= metadata.idx + 1 %></td>
<td class="py-3 px-4">
<span class="cursor-help" data-tooltip="<%= metadata.description %>">
<%= metadata.name %>
</span>
</td>
<td><%= metadata.bestPracticeCategory %></td>
<td class="py-3 px-4"><%= metadata.bestPracticeCategory %></td>
<td>
<td class="py-3 px-4">
<span
class="badge text-bg-<%= priorityColor %>"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="<%= metadata.priorityReason %>">
class="inline-block px-2 py-1 text-xs font-medium rounded-full bg-<%= priorityColor %>-200 text-<%= priorityColor %>-800 cursor-help"
data-tooltip="<%= metadata.priorityReason %>">
<%= metadata.priority %> - <%= priorityLabel %>
</span>
</td>
@ -29,5 +26,3 @@
<%- include('./bpset_progress.ejs', { metadata }) %>
<%- include('./bpset_actions.ejs', { metadata }) %>
</tr>
<%- include('./bpset_fixdialog.ejs', { metadata }) %>

View File

@ -1,30 +1,27 @@
<tr>
<td colspan="7" class="p-0">
<div class="collapse" id="logs-<%= metadata.idx %>">
<div class="bg-light p-3">
<h4>Non-Compliant Resources</h4>
<ul>
<div class="overflow-hidden max-h-0 hidden" id="logs-<%= metadata.idx %>">
<div class="bg-gray-50 p-4">
<h4 class="text-lg font-semibold">Non-Compliant Resources</h4>
<ul class="list-disc list-inside">
<% metadata.nonCompliantResources.forEach((id) => { %>
<li><%= id %></li>
<li class="text-gray-700"><%= id %></li>
<% }) %>
</ul>
<br>
<h4>Compliant Resources</h4>
<ul>
<h4 class="text-lg font-semibold mt-4">Compliant Resources</h4>
<ul class="list-disc list-inside">
<% metadata.compliantResources.forEach((id) => { %>
<li><%= id %></li>
<li class="text-gray-700"><%= id %></li>
<% }) %>
</ul>
<br>
<h4>Error Logs</h4>
<ul>
<h4 class="text-lg font-semibold mt-4">Error Logs</h4>
<ul class="list-disc list-inside">
<% metadata.errorMessage.forEach((log) => { %>
<li>
<p><%= log.date %></p>
<pre><%= log.message %></pre>
<p class="font-semibold text-gray-700"><%= log.date %></p>
<pre class="bg-gray-200 p-3 rounded"><%= log.message %></pre>
</li>
<% }) %>
</ul>

View File

@ -12,46 +12,36 @@
%>
<% if (metadata.status === 'FINISHED') { %>
<td>
<span class="badge text-bg-<%= isPass ? 'success' : 'danger' %>">
<td class="py-3 px-4">
<span
class="inline-block px-2 py-1 text-xs font-medium rounded-full bg-<%= isPass ? 'green' : 'red' %>-200 text-<%= isPass ? 'green' : 'red' %>-800">
<%= isPass ? 'Pass' : 'Fail' %>
</span>
</td>
<td>
<div class="progress" role="progressbar">
<div class="progress-bar" style="width: <%= passPercent %>%"></div>
<td class="py-3 px-4">
<div class="relative w-full bg-gray-200 h-4 rounded-md">
<div class="absolute top-0 left-0 h-4 bg-green-500 rounded-md" style="width: <%= passPercent %>%"></div>
</div>
<span class="block mt-1 text-sm text-gray-600">
(<%= passResources %>/<%= totalResources %>)
</span>
</td>
<% } %>
<% if (metadata.status === 'CHECKING') { %>
<td colspan="2">
<div class="progress" role="progressbar">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-secondary" style="width: 100%"></div>
<% } else if (metadata.status === 'CHECKING') { %>
<td colspan="2" class="py-3 px-4">
<div class="relative w-full bg-gray-200 h-4 rounded-md">
<div class="absolute top-0 left-0 h-4 bg-gray-500 animate-pulse"></div>
</div>
<p class="m-0">Progressing</p>
</td>
<% } %>
<% if (metadata.status === 'ERROR') { %>
<td colspan="2">
<div class="progress" role="progressbar">
<div class="progress-bar progress-bar-striped bg-danger" style="width: 100%"></div>
</div>
<p class="m-0">Error</p>
</td>
<% } %>
<% if (metadata.status === 'LOADED') { %>
<td colspan="2">
<div class="progress" role="progressbar">
<div class="progress-bar" style="width: 0%"></div>
</div>
<p class="m-0">Ready</p>
<p class="mt-1 text-sm text-gray-600">Progressing</p>
</td>
<% } else if (metadata.status === 'ERROR') { %>
<td colspan="2" class="py-3 px-4">
<div class="relative w-full bg-red-200 h-4 rounded-md"></div>
<p class="mt-1 text-sm text-red-500">Error</p>
</td>
<% } else if (metadata.status === 'LOADED') { %>
<td colspan="2" class="py-3 px-4">
<div class="relative w-full bg-gray-200 h-4 rounded-md"></div>
<p class="mt-1 text-sm text-gray-600">Ready</p>
</td>
<% } %>

View File

@ -10,33 +10,35 @@
const errorCount = metadatas.filter((v) => v.status === 'ERROR').length
%>
<div class="d-flex justify-content-between align-items-end">
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="fw-bold">BPSets (<%= bpLength %>)</h1>
<p>Created by Minhyeok Park</p>
<h1 class="text-2xl font-bold">BPSets (<%= bpLength %>)</h1>
<p class="text-gray-500">Created by Minhyeok Park</p>
</div>
<div class="d-flex status gap-3 text-center">
<div class="flex space-x-6 text-center">
<div>
<p class="m-0">Pass</p>
<p class="fs-3"><%= passCount %></p>
<p class="text-sm text-gray-600">Pass</p>
<p class="text-xl font-semibold text-green-500"><%= passCount %></p>
</div>
<div>
<p class="m-0">Fail</p>
<p class="fs-3"><%= failCount %></p>
<p class="text-sm text-gray-600">Fail</p>
<p class="text-xl font-semibold text-red-500"><%= failCount %></p>
</div>
<div>
<p class="m-0">Error</p>
<p class="fs-3"><%= errorCount %></p>
<p class="text-sm text-gray-600">Error</p>
<p class="text-xl font-semibold text-yellow-500"><%= errorCount %></p>
</div>
</div>
<div class="btn-group">
<a href="/check_all?hidePass=<% hidePass %>" type="button" class="btn btn-primary">Check All</a>
<% if (hidePass) { %>
<a href="/?hidePass=false" type="button" class="btn btn-secondary">Show Pass</a>
<% } else { %>
<a href="/?hidePass=true" type="button" class="btn btn-secondary">Hide Pass</a>
<% } %>
<div class="space-x-2">
<a href="/check_all?hidePass=<%= hidePass %>" class="bg-blue-500 text-white px-4 py-2 rounded-md shadow hover:bg-blue-600 inline-block">
Check All
</a>
<button
id="toggleHidePass"
class="bg-blue-500 text-white px-4 py-2 rounded-md shadow hover:bg-blue-600">
Hide Pass
</button>
</div>
</div>

333
views/script.ejs Normal file
View File

@ -0,0 +1,333 @@
<script>
document.addEventListener("DOMContentLoaded", () => {
const toggleHidePassButton = document.getElementById("toggleHidePass");
let hidePass = false;
toggleHidePassButton.addEventListener("click", () => {
hidePass = !hidePass; // Toggle state
const rows = document.querySelectorAll('tr[data-status="pass"]');
// Show or hide rows based on "Hide Pass" toggle
rows.forEach((row) => {
if (hidePass) {
row.classList.add("hidden");
} else {
row.classList.remove("hidden");
}
});
// Update button text
toggleHidePassButton.textContent = hidePass ? "Show Pass" : "Hide Pass";
// Check and hide empty category headers
updateCategoryHeaders();
});
function updateCategoryHeaders() {
const categoryHeaders = document.querySelectorAll("tr[data-category]");
categoryHeaders.forEach((header) => {
const category = header.getAttribute("data-category");
const categoryRows = document.querySelectorAll(`tr[data-category-items="${category}"]`);
// Check if all rows in the category are hidden
const hasVisibleRows = Array.from(categoryRows).some(
(row) => !row.classList.contains("hidden")
);
// Hide or show the category header based on visible rows
if (hasVisibleRows) {
header.classList.remove("hidden");
} else {
header.classList.add("hidden");
}
});
}
});
document.addEventListener("DOMContentLoaded", () => {
const filterInput = document.getElementById("tableFilter");
const suggestionsList = document.getElementById("autocompleteSuggestions");
// Collect all category names and row content for autocomplete
const categoryHeaders = Array.from(document.querySelectorAll("tr[data-category]")).map((header) =>
header.getAttribute("data-category")
);
const dataRows = Array.from(document.querySelectorAll("tr[data-category-items]")).map((row) =>
row.textContent.trim()
);
const allSuggestions = Array.from(new Set([...categoryHeaders, ...dataRows])); // Remove duplicates
// Filter functionality with autocomplete
filterInput.addEventListener("input", () => {
const filterValue = filterInput.value.toLowerCase();
suggestionsList.innerHTML = "";
if (filterValue) {
// Show autocomplete suggestions
const matchingSuggestions = allSuggestions.filter((suggestion) =>
suggestion.toLowerCase().includes(filterValue)
);
if (matchingSuggestions.length > 0) {
suggestionsList.classList.remove("hidden");
matchingSuggestions.forEach((suggestion) => {
const suggestionItem = document.createElement("li");
suggestionItem.textContent = suggestion;
suggestionItem.className =
"px-4 py-2 cursor-pointer hover:bg-blue-100 text-gray-700";
suggestionsList.appendChild(suggestionItem);
// Handle click on suggestion
suggestionItem.addEventListener("click", () => {
filterInput.value = suggestion;
suggestionsList.classList.add("hidden");
applyFilter(suggestion.toLowerCase());
});
});
} else {
suggestionsList.classList.add("hidden");
}
} else {
suggestionsList.classList.add("hidden");
}
// Apply filter based on input value
applyFilter(filterValue);
});
// Close suggestions on blur
filterInput.addEventListener("blur", () => {
setTimeout(() => suggestionsList.classList.add("hidden"), 100); // Delay to allow click on suggestions
});
// Function to filter rows and headers
function applyFilter(filterValue) {
const headers = document.querySelectorAll("tr[data-category]");
const rows = document.querySelectorAll("tr[data-category-items]");
headers.forEach((header) => {
const category = header.getAttribute("data-category");
const categoryRows = document.querySelectorAll(`tr[data-category-items="${category}"]`);
let hasVisibleRows = false;
categoryRows.forEach((row) => {
const rowText = row.textContent.toLowerCase();
if (rowText.includes(filterValue) || category.toLowerCase().includes(filterValue)) {
row.classList.remove("hidden");
hasVisibleRows = true;
} else {
row.classList.add("hidden");
}
});
if (hasVisibleRows || category.toLowerCase().includes(filterValue)) {
header.classList.remove("hidden");
} else {
header.classList.add("hidden");
}
});
}
});
document.addEventListener("DOMContentLoaded", () => {
const table = document.getElementById("bpTable");
const tbody = table.querySelector("tbody");
const originalRows = Array.from(tbody.querySelectorAll("tr"));
// Initialize sorting state for all headers
const headers = table.querySelectorAll("thead th[data-sort]");
headers.forEach((header) => {
header.classList.add("not-sorted");
});
// Sorting Functionality
headers.forEach((header) => {
header.addEventListener("click", () => {
const sortType = header.getAttribute("data-sort");
const columnIndex = Array.from(header.parentNode.children).indexOf(header);
const rows = Array.from(tbody.querySelectorAll("tr")).filter(
(row) => row.querySelector("td")
);
// Sort rows
rows.sort((a, b) => {
const cellA = a.children[columnIndex]?.textContent.trim() || "";
const cellB = b.children[columnIndex]?.textContent.trim() || "";
if (sortType === "number") {
return parseFloat(cellA) - parseFloat(cellB);
} else {
return cellA.localeCompare(cellB);
}
});
// Toggle sorting states
if (header.classList.contains("ascending")) {
rows.reverse();
updateSortIndicator(header, "descending");
} else if (header.classList.contains("descending")) {
updateSortIndicator(header, "not-sorted");
resetToOriginalOrder();
} else {
updateSortIndicator(header, "ascending");
}
// Update table with sorted rows if not "not-sorted"
if (!header.classList.contains("not-sorted")) {
tbody.innerHTML = "";
rows.forEach((row) => tbody.appendChild(row));
}
});
});
function resetToOriginalOrder() {
tbody.innerHTML = "";
originalRows.forEach((row) => tbody.appendChild(row));
}
function updateSortIndicator(header, state) {
headers.forEach((h) => {
h.classList.remove("ascending", "descending", "not-sorted");
const icon = h.querySelector(".sort-indicator i");
if (icon) icon.className = "fas fa-sort"; // Reset all icons
});
header.classList.add(state);
const icon = header.querySelector(".sort-indicator i");
if (state === "ascending") {
icon.className = "fas fa-sort-up"; // Up arrow for ascending
} else if (state === "descending") {
icon.className = "fas fa-sort-down"; // Down arrow for descending
} else {
icon.className = "fas fa-sort"; // Default sort icon
}
}
});
// Tooltip Functionality
document.querySelectorAll('[data-tooltip]').forEach((el) => {
el.addEventListener('mouseenter', () => {
const tooltipText = el.getAttribute('data-tooltip');
const tooltipId = `tooltip-${Math.random().toString(36).substring(2, 10)}`;
const tooltip = document.createElement('div');
tooltip.className = 'absolute bg-gray-800 text-white text-xs rounded py-1 px-2 shadow-lg opacity-0';
tooltip.style.transition = 'opacity 0.3s';
tooltip.style.position = 'absolute';
tooltip.style.zIndex = '1000';
tooltip.style.top = `${el.getBoundingClientRect().top - 30}px`;
tooltip.style.left = `${el.getBoundingClientRect().left}px`;
tooltip.textContent = tooltipText;
tooltip.id = tooltipId;
document.body.appendChild(tooltip);
el.setAttribute('data-tooltip-id', tooltipId); // Associate the tooltip with the element
setTimeout(() => tooltip.classList.add('opacity-100'), 10);
});
el.addEventListener('mouseleave', () => {
const tooltipId = el.getAttribute('data-tooltip-id');
const tooltip = document.getElementById(tooltipId);
if (tooltip) {
tooltip.classList.remove('opacity-100');
setTimeout(() => tooltip.remove(), 300);
}
});
});
document.addEventListener("DOMContentLoaded", () => {
// Open Offcanvas
document.querySelectorAll('[data-bs-toggle="offcanvas"]').forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.getAttribute("data-bs-target").substring(1); // Remove the `#`
const offcanvas = document.getElementById(targetId);
if (offcanvas) {
// Remove `hidden` immediately to make the element renderable
offcanvas.classList.remove("hidden");
// Add initial state for animation
offcanvas.classList.add("opacity-0");
const content = offcanvas.querySelector("div");
content.classList.add("translate-y-full");
// Trigger animation after rendering the initial state
setTimeout(() => {
offcanvas.classList.remove("opacity-0");
content.classList.remove("translate-y-full");
offcanvas.classList.add("opacity-100");
content.classList.add("translate-y-0");
}, 10); // Small delay to allow rendering
}
});
});
// Close Offcanvas
document.querySelectorAll('[data-close-offcanvas]').forEach((button) => {
button.addEventListener("click", () => {
const offcanvas = button.closest(".fixed");
if (offcanvas) {
offcanvas.classList.remove("opacity-100");
offcanvas.querySelector("div").classList.remove("translate-y-0");
offcanvas.classList.add("opacity-0");
offcanvas.querySelector("div").classList.add("translate-y-full");
// Wait for transition to complete before hiding the element
setTimeout(() => {
offcanvas.classList.add("hidden");
}, 300); // Match the duration-300 class
}
});
});
// Close offcanvas when clicking outside the modal content
document.querySelectorAll('.fixed').forEach((offcanvas) => {
offcanvas.addEventListener('click', (event) => {
if (event.target === offcanvas) {
offcanvas.classList.remove("opacity-100");
offcanvas.querySelector("div").classList.remove("translate-y-0");
offcanvas.classList.add("opacity-0");
offcanvas.querySelector("div").classList.add("translate-y-full");
// Wait for transition to complete before hiding the element
setTimeout(() => {
offcanvas.classList.add("hidden");
}, 300); // Match the duration-300 class
}
});
});
});
// Collapsible Functionality
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach((button) => {
button.addEventListener('click', () => {
const targetId = button.getAttribute('data-bs-target').substring(1);
const collapsible = document.getElementById(targetId);
if (collapsible) {
if (collapsible.classList.contains('hidden')) {
// Temporarily remove the hidden class to calculate scrollHeight
collapsible.classList.remove('hidden');
const scrollHeight = collapsible.scrollHeight;
collapsible.style.maxHeight = '0'; // Reset max-height for animation
setTimeout(() => {
collapsible.style.transition = 'max-height 0.3s ease-in-out';
collapsible.style.maxHeight = `${scrollHeight}px`;
}, 10);
} else {
// Collapse the element
collapsible.style.maxHeight = '0';
setTimeout(() => {
collapsible.classList.add('hidden'); // Fully hide after animation
collapsible.style.maxHeight = null; // Reset max-height for future toggles
}, 300); // Match the duration of the transition
}
}
});
});
</script>

9
views/style.ejs Normal file
View File

@ -0,0 +1,9 @@
<style>
#autocompleteSuggestions {
z-index: 1000;
}
#autocompleteSuggestions li {
list-style: none;
}
</style>