feat: add search and sort feature
All checks were successful
/ deploy_site (push) Successful in 2m18s
All checks were successful
/ deploy_site (push) Successful in 2m18s
This commit is contained in:
@ -59,6 +59,9 @@ export class WebServer {
|
|||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
res.setHeader('Transfer-Encoding', 'chunked')
|
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('<script>setInterval(() => window.scrollTo(0, document.body.scrollHeight), 100)</script>')
|
||||||
res.write('<pre>Start Checking....\n')
|
res.write('<pre>Start Checking....\n')
|
||||||
|
|
||||||
@ -72,7 +75,8 @@ export class WebServer {
|
|||||||
Memorizer.reset()
|
Memorizer.reset()
|
||||||
await this.bpManager.runCheckOnce(name)
|
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()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +86,9 @@ export class WebServer {
|
|||||||
|
|
||||||
const { hidePass } = req.query
|
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('<script>setInterval(() => window.scrollTo(0, document.body.scrollHeight), 100)</script>')
|
||||||
res.write('<pre>Start Checking....\n')
|
res.write('<pre>Start Checking....\n')
|
||||||
|
|
||||||
@ -89,7 +96,8 @@ export class WebServer {
|
|||||||
await this.bpManager.runCheckAll((name) =>
|
await this.bpManager.runCheckAll((name) =>
|
||||||
res.write(`${name} - FINISHED\n`))
|
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()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +105,9 @@ export class WebServer {
|
|||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||||
res.setHeader('Transfer-Encoding', 'chunked')
|
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')
|
res.write('<pre>Start Fixing....\n')
|
||||||
|
|
||||||
const { name, hidePass } = req.query
|
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(`<a href="/?hidePass=${hidePass}">Done. Return to Report Page`)
|
||||||
|
res.write(`<script>window.location.replace('/?hidePass=${hidePass}')</script>`)
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,48 +1,71 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="/style.css">
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
|
||||||
<title>BPSets</title>
|
<title>BPSets</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-gray-100 text-gray-800">
|
||||||
<div class="container p-3">
|
<div class="container mx-auto p-4">
|
||||||
<%- include('partial/page_header.ejs') %>
|
<%- include('partial/page_header.ejs') %>
|
||||||
|
|
||||||
<table class="table">
|
<div class="relative mb-4">
|
||||||
<thead>
|
<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>
|
<tr>
|
||||||
<th>#</th>
|
<th class="py-3 px-4 cursor-pointer" data-sort="number">
|
||||||
<th>Name</th>
|
# <span class="sort-indicator"><i class="fas fa-sort"></i></span>
|
||||||
<th>B.P. Category</th>
|
</th>
|
||||||
<th>Priority</th>
|
<th class="py-3 px-4 cursor-pointer" data-sort="string">
|
||||||
<th>Fail/Pass</th>
|
Name <span class="sort-indicator"><i class="fas fa-sort"></i></span>
|
||||||
<th>Pass Percent</th>
|
</th>
|
||||||
<th>Action</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% bpStatus.forEach(({ category, metadatas }) => { %>
|
<% bpStatus.forEach(({ category, metadatas }) => { %>
|
||||||
<tr>
|
<tr class="bg-gray-50" data-category="<%= category %>">
|
||||||
<th colspan="7"><%= category %> (<%= metadatas.length %>)</th>
|
<th colspan="7" class="py-3 px-4 text-left text-lg font-semibold text-gray-700">
|
||||||
|
<%= category %> (<%= metadatas.length %>)
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<% metadatas.forEach((metadata) => { %>
|
<% 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_details.ejs', { metadata }) %>
|
||||||
<%- include('./partial/bpset_logs.ejs', { metadata }) %>
|
<%- include('./partial/bpset_logs.ejs', { metadata }) %>
|
||||||
|
<%- include('./partial/bpset_fixdialog.ejs', { metadata }) %>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
<%- include('./style.ejs') %>
|
||||||
<script>
|
<%- include('./script.ejs') %>
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
|
||||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,8 +1,30 @@
|
|||||||
<td>
|
<td class="py-3 px-4">
|
||||||
<div class="btn-group">
|
<div class="flex space-x-2">
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="offcanvas" data-bs-target="#fixdialog-<%= metadata.idx %>">Fix</button>
|
<button
|
||||||
<a href="/check?name=<%= metadata.name %>&hidePass=<% hidePass %>" type="button" class="btn btn-secondary">Recheck</a>
|
type="button"
|
||||||
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse" data-bs-target="#detail-<%= metadata.idx %>">Details</button>
|
class="bg-blue-500 text-white px-3 py-2 rounded shadow hover:bg-blue-600"
|
||||||
<button type="button" class="btn btn-secondary" data-bs-toggle="collapse" data-bs-target="#logs-<%= metadata.idx %>">Logs</button>
|
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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,57 +1,51 @@
|
|||||||
<%
|
<%
|
||||||
const priorityLabel = ['CRITICAL', 'Required', 'Recommend'][metadata.priority-1] || 'Recommend'
|
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] || 'gray';
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="p-0">
|
<td colspan="7" class="p-0">
|
||||||
<div class="collapse" id="detail-<%= metadata.idx %>">
|
<div class="overflow-hidden max-h-0 hidden" id="detail-<%= metadata.idx %>">
|
||||||
<div class="bg-light p-3">
|
<div class="bg-gray-100 p-4">
|
||||||
<h3><%= metadata.name %></h3>
|
<h3 class="text-xl font-semibold"><%= metadata.name %></h3>
|
||||||
<p><%= metadata.description %></p>
|
<p class="text-gray-700"><%= metadata.description %></p>
|
||||||
|
|
||||||
<p>
|
<p class="mt-2">
|
||||||
Category:
|
<span class="font-bold">Category:</span>
|
||||||
<span class="badge text-bg-secondary"><%= metadata.bestPracticeCategory %></span>
|
<span class="bg-gray-200 text-gray-700 px-2 py-1 rounded"><%= metadata.bestPracticeCategory %></span>
|
||||||
<%= metadata.awsServiceCategory %> - <%= metadata.awsService %>
|
<span class="text-gray-500"><%= metadata.awsServiceCategory %> - <%= metadata.awsService %></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p class="mt-2">
|
||||||
Priority:
|
<span class="font-bold">Priority:</span>
|
||||||
<span class="badge text-bg-<%= priorityColor %>">
|
<span class="bg-<%= priorityColor %>-200 text-<%= priorityColor %>-800 px-2 py-1 rounded">
|
||||||
<%= metadata.priority %> - <%= priorityLabel %>
|
<%= metadata.priority %> - <%= priorityLabel %>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="text-gray-500"><%= metadata.priorityReason %></span>
|
||||||
<%= metadata.priorityReason %>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4>Operations used in check function</h4>
|
<div class="mt-4">
|
||||||
<div class="row row-cols-3">
|
<h4 class="text-lg font-semibold">Operations used in check function</h4>
|
||||||
<% metadata.commandUsedInCheckFunction.forEach(({ name, reason }) => { %>
|
<div class="grid grid-cols-3 gap-4 mt-2">
|
||||||
<div class="col">
|
<% metadata.commandUsedInCheckFunction.forEach(({ name, reason }) => { %>
|
||||||
<div class="card">
|
<div class="bg-white shadow rounded p-3">
|
||||||
<h5 class="card-header"><%= name %></h5>
|
<h5 class="text-sm font-bold"><%= name %></h5>
|
||||||
<div class="card-body">
|
<p class="text-sm text-gray-600"><%= reason %></p>
|
||||||
<%= reason %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% }) %>
|
||||||
<% }) %>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
|
<div class="mt-4">
|
||||||
<h4>Operations used in fix function</h4>
|
<h4 class="text-lg font-semibold">Operations used in fix function</h4>
|
||||||
<div class="row row-cols-3">
|
<div class="grid grid-cols-3 gap-4 mt-2">
|
||||||
<% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %>
|
<% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %>
|
||||||
<div class="col">
|
<div class="bg-white shadow rounded p-3">
|
||||||
<div class="card">
|
<h5 class="text-sm font-bold"><%= name %></h5>
|
||||||
<h5 class="card-header"><%= name %></h5>
|
<p class="text-sm text-gray-600"><%= reason %></p>
|
||||||
<div class="card-body">
|
|
||||||
<%= reason %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% }) %>
|
||||||
<% }) %>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,48 +1,57 @@
|
|||||||
<div class="offcanvas offcanvas-end" data-bs-scroll="true" tabindex="-1" id="fixdialog-<%= metadata.idx %>">
|
<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="offcanvas-header d-flex gap-3">
|
<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">
|
||||||
<h3 class="offcanvas-title" style="word-break: break-all;">Fixing <%= metadata.name %></h3>
|
<div class="p-4 border-b relative">
|
||||||
</div>
|
<button data-close-offcanvas class="absolute top-4 right-4 text-gray-500">×</button>
|
||||||
|
<h3 class="text-lg font-bold text-gray-700">Fixing <%= metadata.name %></h3>
|
||||||
<div class="offcanvas-body">
|
</div>
|
||||||
<h3 class="mb-3">Pending operations</h3>
|
|
||||||
<% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %>
|
|
||||||
<div class="card mb-3">
|
|
||||||
<h5 class="card-header"><%= name %></h5>
|
|
||||||
<div class="card-body">
|
|
||||||
<%= reason %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% }) %>
|
|
||||||
|
|
||||||
<br>
|
<div class="p-4">
|
||||||
|
<h4 class="text-md font-semibold mb-3">Pending operations</h4>
|
||||||
<h3 class="mb-3">Required Parameters</h3>
|
<% metadata.commandUsedInFixFunction.forEach(({ name, reason }) => { %>
|
||||||
<form method="POST" action="/fix?name=<%= metadata.name %>&hidePass=<%= hidePass %>">
|
<div class="bg-gray-100 p-3 rounded mb-3">
|
||||||
<% metadata.requiredParametersForFix.forEach((input) => { %>
|
<h5 class="font-semibold text-gray-700"><%= name %></h5>
|
||||||
<div class="mb-3">
|
<p class="text-gray-600"><%= reason %></p>
|
||||||
<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">
|
|
||||||
<%= input.description %><br />
|
|
||||||
ex) <code><%= input.example %></code>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<br>
|
|
||||||
|
<h4 class="text-md font-semibold mb-3">Required Parameters</h4>
|
||||||
<% if (metadata.isFixFunctionUsesDestructiveCommand) { %>
|
<form method="POST" action="/fix?name=<%= metadata.name %>&hidePass=<%= hidePass %>">
|
||||||
<div class="alert alert-danger" role="alert">
|
<% metadata.requiredParametersForFix.forEach((input) => { %>
|
||||||
This Fix Function Has DESTRUCTIVE Command! please review pending operations carefully!
|
<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 />
|
||||||
|
<span class="font-mono"><%= input.example %></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
|
||||||
|
<% if (metadata.isFixFunctionUsesDestructiveCommand) { %>
|
||||||
|
<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="p-3 bg-yellow-100 text-yellow-700 rounded mb-3">
|
||||||
|
<%= metadata.adviseBeforeFixFunction %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<div class="alert alert-warning" role="alert">
|
<button
|
||||||
<%= metadata.adviseBeforeFixFunction %>
|
type="submit"
|
||||||
</div>
|
class="w-full bg-blue-500 text-white px-4 py-2 rounded shadow hover:bg-blue-600">
|
||||||
|
Fix!
|
||||||
<br>
|
</button>
|
||||||
|
</form>
|
||||||
<button class="btn btn-primary" type="submit">Fix!</button>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,27 +1,24 @@
|
|||||||
<%
|
<%
|
||||||
const priorityLabel = ['CRITICAL', 'Required', 'Recommend'][metadata.priority-1] || 'Recommend'
|
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>
|
<tr class="border-b" data-status="<%= isPass ? 'pass' : 'fail' %>" data-category-items="<%= category %>">
|
||||||
<td class="fw-bold">#<%= metadata.idx + 1 %></td>
|
<td class="font-semibold py-3 px-4">#<%= metadata.idx + 1 %></td>
|
||||||
<td>
|
<td class="py-3 px-4">
|
||||||
<span
|
<span class="cursor-help" data-tooltip="<%= metadata.description %>">
|
||||||
class="m-0"
|
|
||||||
data-bs-toggle="tooltip"
|
|
||||||
data-bs-placement="bottom"
|
|
||||||
data-bs-title="<%= metadata.description %>">
|
|
||||||
<%= metadata.name %>
|
<%= metadata.name %>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td><%= metadata.bestPracticeCategory %></td>
|
<td class="py-3 px-4"><%= metadata.bestPracticeCategory %></td>
|
||||||
|
|
||||||
<td>
|
<td class="py-3 px-4">
|
||||||
<span
|
<span
|
||||||
class="badge text-bg-<%= priorityColor %>"
|
class="inline-block px-2 py-1 text-xs font-medium rounded-full bg-<%= priorityColor %>-200 text-<%= priorityColor %>-800 cursor-help"
|
||||||
data-bs-toggle="tooltip"
|
data-tooltip="<%= metadata.priorityReason %>">
|
||||||
data-bs-placement="bottom"
|
|
||||||
data-bs-title="<%= metadata.priorityReason %>">
|
|
||||||
<%= metadata.priority %> - <%= priorityLabel %>
|
<%= metadata.priority %> - <%= priorityLabel %>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@ -29,5 +26,3 @@
|
|||||||
<%- include('./bpset_progress.ejs', { metadata }) %>
|
<%- include('./bpset_progress.ejs', { metadata }) %>
|
||||||
<%- include('./bpset_actions.ejs', { metadata }) %>
|
<%- include('./bpset_actions.ejs', { metadata }) %>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<%- include('./bpset_fixdialog.ejs', { metadata }) %>
|
|
||||||
|
@ -1,30 +1,27 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="p-0">
|
<td colspan="7" class="p-0">
|
||||||
<div class="collapse" id="logs-<%= metadata.idx %>">
|
<div class="overflow-hidden max-h-0 hidden" id="logs-<%= metadata.idx %>">
|
||||||
<div class="bg-light p-3">
|
<div class="bg-gray-50 p-4">
|
||||||
<h4>Non-Compliant Resources</h4>
|
<h4 class="text-lg font-semibold">Non-Compliant Resources</h4>
|
||||||
<ul>
|
<ul class="list-disc list-inside">
|
||||||
<% metadata.nonCompliantResources.forEach((id) => { %>
|
<% metadata.nonCompliantResources.forEach((id) => { %>
|
||||||
<li><%= id %></li>
|
<li class="text-gray-700"><%= id %></li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<br>
|
<h4 class="text-lg font-semibold mt-4">Compliant Resources</h4>
|
||||||
|
<ul class="list-disc list-inside">
|
||||||
<h4>Compliant Resources</h4>
|
|
||||||
<ul>
|
|
||||||
<% metadata.compliantResources.forEach((id) => { %>
|
<% metadata.compliantResources.forEach((id) => { %>
|
||||||
<li><%= id %></li>
|
<li class="text-gray-700"><%= id %></li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
<br>
|
|
||||||
|
|
||||||
<h4>Error Logs</h4>
|
<h4 class="text-lg font-semibold mt-4">Error Logs</h4>
|
||||||
<ul>
|
<ul class="list-disc list-inside">
|
||||||
<% metadata.errorMessage.forEach((log) => { %>
|
<% metadata.errorMessage.forEach((log) => { %>
|
||||||
<li>
|
<li>
|
||||||
<p><%= log.date %></p>
|
<p class="font-semibold text-gray-700"><%= log.date %></p>
|
||||||
<pre><%= log.message %></pre>
|
<pre class="bg-gray-200 p-3 rounded"><%= log.message %></pre>
|
||||||
</li>
|
</li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -12,46 +12,36 @@
|
|||||||
%>
|
%>
|
||||||
|
|
||||||
<% if (metadata.status === 'FINISHED') { %>
|
<% if (metadata.status === 'FINISHED') { %>
|
||||||
<td>
|
<td class="py-3 px-4">
|
||||||
<span class="badge text-bg-<%= isPass ? 'success' : 'danger' %>">
|
<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' %>
|
<%= isPass ? 'Pass' : 'Fail' %>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td class="py-3 px-4">
|
||||||
<div class="progress" role="progressbar">
|
<div class="relative w-full bg-gray-200 h-4 rounded-md">
|
||||||
<div class="progress-bar" style="width: <%= passPercent %>%"></div>
|
<div class="absolute top-0 left-0 h-4 bg-green-500 rounded-md" style="width: <%= passPercent %>%"></div>
|
||||||
</div>
|
</div>
|
||||||
(<%= passResources %>/<%= totalResources %>)
|
<span class="block mt-1 text-sm text-gray-600">
|
||||||
</td>
|
(<%= passResources %>/<%= totalResources %>)
|
||||||
<% } %>
|
</span>
|
||||||
|
</td>
|
||||||
<% if (metadata.status === 'CHECKING') { %>
|
<% } else if (metadata.status === 'CHECKING') { %>
|
||||||
<td colspan="2">
|
<td colspan="2" class="py-3 px-4">
|
||||||
<div class="progress" role="progressbar">
|
<div class="relative w-full bg-gray-200 h-4 rounded-md">
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-secondary" style="width: 100%"></div>
|
<div class="absolute top-0 left-0 h-4 bg-gray-500 animate-pulse"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-1 text-sm text-gray-600">Progressing</p>
|
||||||
<p class="m-0">Progressing</p>
|
</td>
|
||||||
</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>
|
||||||
<% if (metadata.status === 'ERROR') { %>
|
<p class="mt-1 text-sm text-red-500">Error</p>
|
||||||
<td colspan="2">
|
</td>
|
||||||
<div class="progress" role="progressbar">
|
<% } else if (metadata.status === 'LOADED') { %>
|
||||||
<div class="progress-bar progress-bar-striped bg-danger" style="width: 100%"></div>
|
<td colspan="2" class="py-3 px-4">
|
||||||
</div>
|
<div class="relative w-full bg-gray-200 h-4 rounded-md"></div>
|
||||||
|
<p class="mt-1 text-sm text-gray-600">Ready</p>
|
||||||
<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>
|
|
||||||
</td>
|
</td>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
@ -10,33 +10,35 @@
|
|||||||
const errorCount = metadatas.filter((v) => v.status === 'ERROR').length
|
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>
|
<div>
|
||||||
<h1 class="fw-bold">BPSets (<%= bpLength %>)</h1>
|
<h1 class="text-2xl font-bold">BPSets (<%= bpLength %>)</h1>
|
||||||
<p>Created by Minhyeok Park</p>
|
<p class="text-gray-500">Created by Minhyeok Park</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex status gap-3 text-center">
|
<div class="flex space-x-6 text-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="m-0">Pass</p>
|
<p class="text-sm text-gray-600">Pass</p>
|
||||||
<p class="fs-3"><%= passCount %></p>
|
<p class="text-xl font-semibold text-green-500"><%= passCount %></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="m-0">Fail</p>
|
<p class="text-sm text-gray-600">Fail</p>
|
||||||
<p class="fs-3"><%= failCount %></p>
|
<p class="text-xl font-semibold text-red-500"><%= failCount %></p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="m-0">Error</p>
|
<p class="text-sm text-gray-600">Error</p>
|
||||||
<p class="fs-3"><%= errorCount %></p>
|
<p class="text-xl font-semibold text-yellow-500"><%= errorCount %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="space-x-2">
|
||||||
<a href="/check_all?hidePass=<% hidePass %>" type="button" class="btn btn-primary">Check All</a>
|
<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">
|
||||||
<% if (hidePass) { %>
|
Check All
|
||||||
<a href="/?hidePass=false" type="button" class="btn btn-secondary">Show Pass</a>
|
</a>
|
||||||
<% } else { %>
|
<button
|
||||||
<a href="/?hidePass=true" type="button" class="btn btn-secondary">Hide Pass</a>
|
id="toggleHidePass"
|
||||||
<% } %>
|
class="bg-blue-500 text-white px-4 py-2 rounded-md shadow hover:bg-blue-600">
|
||||||
|
Hide Pass
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
333
views/script.ejs
Normal file
333
views/script.ejs
Normal 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
9
views/style.ejs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<style>
|
||||||
|
#autocompleteSuggestions {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#autocompleteSuggestions li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
</style>
|
Reference in New Issue
Block a user