commit 5eaf61f779f6a465f9d39c47d7e2a2bb304c0a3d
parent 1b6f3f4c960548c29ff300ba1627d2bd0b100dfc
Author: William Lindholm <william_lindholm@outlook.com>
Date: Thu, 9 Nov 2023 11:05:22 +0100
Updated ajax table to include debounce function.
Diffstat:
13 files changed, 231 insertions(+), 201 deletions(-)
diff --git a/WebAPI/namespaces/messages.py b/WebAPI/namespaces/messages.py
@@ -58,7 +58,7 @@ class MessageList(Resource):
if sort_order not in ['ASC', 'DESC']:
messages_ns.abort(400, "sort_order must be either 'ASC' or 'DESC'")
- valid_sort_fields = ['id', 'name', 'email', 'subject', 'timestamp', 'relevance']
+ valid_sort_fields = ['id', 'name', 'email', 'subject', 'timestamp', 'relevance', 'content']
if sort_by not in valid_sort_fields:
messages_ns.abort(400, f"Invalid sort_by field. Must be one of {valid_sort_fields}")
diff --git a/WebInterface/routes.py b/WebInterface/routes.py
@@ -15,22 +15,12 @@ def home():
if not session.get('logged_in'):
return redirect(url_for('web_interface.login'))
- messages = db.get_all_messages()
-
- formatted_messages = [
- {
- "id": message[0],
- "name": message[1],
- "email": message[2],
- "subject": message[3],
- "message_content": message[4],
- "timestamp": message[5],
- "relevance": message[6]
- }
- for message in messages
- ]
-
- return render_template('home.html', messages=formatted_messages, page_title="Messages")
+ page_number = request.args.get('page', 1, type=int)
+
+ tables = TableFactory()
+ message_table = tables.get_message_table(page_number)
+
+ return render_template('home.html', page_title="home", table=message_table)
@web_interface.route('/integrations')
diff --git a/database/database.db b/database/database.db
Binary files differ.
diff --git a/database/repository.py b/database/repository.py
@@ -8,22 +8,23 @@ class DbRepository:
def __init__(self):
self.connection = sqlite3.connect(self.DB_PATH, check_same_thread=False)
- self.cursor = self.connection.cursor()
self.setup_database()
def execute_query(self, query, parameters=(), expect_result=False):
- self.cursor.execute(query, parameters)
+ with self.connection:
+ cursor = self.connection.cursor()
+ cursor.execute(query, parameters)
- if expect_result:
- return self.cursor.fetchall()
- else:
- self.connection.commit()
+ if expect_result:
+ result = cursor.fetchall()
+ cursor.close()
+ return result
def setup_database(self):
with open(os.path.join(self.CURRENT_DIR, 'db.sql'), 'r') as sql_file:
sql_script = sql_file.read()
- self.connection.executescript(sql_script)
- self.connection.commit()
+ with self.connection:
+ self.connection.executescript(sql_script)
def close_connection(self):
- self.connection.close()
+ self.connection.close()
+\ No newline at end of file
diff --git a/static/css/main.css b/static/css/main.css
@@ -108,7 +108,6 @@ h1.mobile {
}
table tr, table td {
- padding: 0;
margin: 0 !important;
}
@@ -119,3 +118,12 @@ table tr, table td {
.pagination .active {
background-color: var(--primary-color) !important;
}
+
+.sortable-header {
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.sortable-header:hover {
+ text-decoration: underline;
+}
+\ No newline at end of file
diff --git a/static/html/tabletest.html b/static/html/tabletest.html
@@ -6,48 +6,48 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
</head>
<body>
- <div class="container">
- <h2>Messages</h2>
-
- <div class="input-field">
- <input type="text" id="search-bar" placeholder="Search Messages..." onkeyup="searchData()">
- <div id="loading-indicator" style="display: none;">Loading...</div>
- </div>
-
- <table class="highlight responsive-table">
- <thead id="table-headers">
- <tr>
- <th data-field="id" data-order="asc">ID</th>
- <th data-field="name" data-order="asc">Name</th>
- <th data-field="email" data-order="asc">Email</th>
- <th data-field="subject" data-order="asc">Subject</th>
- <th data-field="timestamp" data-order="asc">Timestamp</th>
- <th data-field="relevance" data-order="asc">Relevance</th>
- </tr>
- </thead>
- <tbody id="table-body">
- <!-- Table data -->
- </tbody>
- </table>
-
- <ul class="pagination" id="pagination-container">
- <!-- Pagination -->
- </ul>
-
- <select id="page-size" onchange="searchData('messages')">
- <option value="5">5</option>
- <option value="10" selected>10</option>
- <option value="25">25</option>
- <option value="50">50</option>
- </select>
+<div class="container">
+ <h2>Messages</h2>
+ <div class="input-field">
+ <input type="text" id="search-bar" placeholder="Search Messages..." onkeyup="searchData()">
+ <div id="loading-indicator" style="display: none;">Loading...</div>
</div>
- <!-- Materialize JS -->
- <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
-
- <script>const API_ENDPOINT = '/api/messages/';</script>
-
- <script src="/static/js/pagination.js"></script>
+ <table class="highlight responsive-table">
+ <thead id="table-headers">
+ <tr>
+ <th data-field="id" data-order="asc">ID</th>
+ <th data-field="name" data-order="asc">Name</th>
+ <th data-field="email" data-order="asc">Email</th>
+ <th data-field="subject" data-order="asc">Subject</th>
+ <th data-field="timestamp" data-order="asc">Timestamp</th>
+ <th data-field="relevance" data-order="asc">Relevance</th>
+ </tr>
+ </thead>
+ <tbody id="table-body">
+ <!-- Table data -->
+ </tbody>
+ </table>
+
+ <ul class="pagination" id="pagination-container">
+ <!-- Pagination -->
+ </ul>
+
+ <select id="page-size" onchange="searchData('messages')">
+ <option value="5">5</option>
+ <option value="10" selected>10</option>
+ <option value="25">25</option>
+ <option value="50">50</option>
+ </select>
+
+</div>
+
+<!-- Materialize JS -->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
+
+<script>const API_ENDPOINT = '/api/messages/';</script>
+
+<script src="/static/js/pagination.js"></script>
</body>
</html>
\ No newline at end of file
diff --git a/static/js/ajaxtable.js b/static/js/ajaxtable.js
@@ -0,0 +1,98 @@
+// Function to fetch data and update the table
+function fetchData(endpoint, queryParams) {
+ const url = new URL(endpoint, window.location.origin);
+ Object.keys(queryParams).forEach(key => url.searchParams.append(key, queryParams[key]));
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.setRequestHeader('Accept', 'text/html');
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === 4 && xhr.status === 200) {
+ document.getElementById('table-body').innerHTML = xhr.responseText;
+ const totalPages = xhr.getResponseHeader('X-total-pages');
+ const currentPage = xhr.getResponseHeader('X-current-page');
+ updatePagination(parseInt(currentPage, 10), parseInt(totalPages, 10), queryParams.page_size, endpoint);
+ }
+ };
+ xhr.send();
+}
+
+function searchData() {
+ const searchInput = document.getElementById('search-bar').value;
+ const pageSize = document.getElementById('page-size').value;
+ const queryParams = {
+ query: searchInput,
+ page_size: pageSize
+ };
+ fetchData(API_ENDPOINT, queryParams);
+}
+
+function updatePagination(currentPage, totalPages, pageSize) {
+ const container = document.getElementById('pagination-container');
+ container.innerHTML = ''; // Clear existing pagination
+
+ // create back button
+ const backLi = document.createElement('li');
+ backLi.className = currentPage === 1 ? 'disabled' : 'waves-effect';
+ const backA = document.createElement('a');
+ const backIcon = document.createElement('i');
+ backIcon.className = 'material-icons';
+ backIcon.textContent = 'chevron_left';
+ backA.appendChild(backIcon);
+ backA.onclick = function() {
+ if (currentPage > 1) {
+ fetchData(API_ENDPOINT, { page: currentPage - 1, page_size: pageSize });
+ }
+ };
+ backLi.appendChild(backA);
+ container.appendChild(backLi);
+
+ // Create pagination buttons
+ for (let i = 1; i <= totalPages; i++) {
+ const li = document.createElement('li');
+ li.className = i === currentPage ? 'active' : 'waves-effect';
+ const a = document.createElement('a');
+ a.textContent = i;
+ a.onclick = function() {
+ fetchData(API_ENDPOINT, { page: i, page_size: pageSize });
+ };
+ li.appendChild(a);
+ container.appendChild(li);
+ }
+
+ // create forward button
+ const forwardLi = document.createElement('li');
+ forwardLi.className = currentPage === totalPages ? 'disabled' : 'waves-effect';
+ const forwardA = document.createElement('a');
+ const forwardIcon = document.createElement('i');
+ forwardIcon.className = 'material-icons';
+ forwardIcon.textContent = 'chevron_right';
+ forwardA.appendChild(forwardIcon);
+ forwardA.onclick = function() {
+ if (currentPage < totalPages) {
+ fetchData(API_ENDPOINT, { page: currentPage + 1, page_size: pageSize });
+ }
+ };
+ forwardLi.appendChild(forwardA);
+ container.appendChild(forwardLi);
+}
+
+// Function to sort table by clicking on headers
+function sortTableByHeader() {
+ document.querySelectorAll('#table-headers th').forEach(header => {
+ header.addEventListener('click', () => {
+ const sortField = header.getAttribute('data-field');
+ const sortOrder = header.getAttribute('data-order');
+ const searchInput = document.getElementById('search-bar').value;
+ const pageSize = document.getElementById('page-size').value || 10;
+ const queryParams = {
+ query: searchInput,
+ sort_by: sortField,
+ sort_order: sortOrder,
+ page_size: pageSize
+ };
+ fetchData(API_ENDPOINT, queryParams);
+ header.setAttribute('data-order', sortOrder === 'asc' ? 'desc' : 'asc');
+ });
+ });
+}
+\ No newline at end of file
diff --git a/static/js/pagination.js b/static/js/pagination.js
@@ -1,105 +0,0 @@
-// Function to fetch data and update the table
-function fetchData(endpoint, queryParams) {
- const url = new URL(endpoint, window.location.origin);
- Object.keys(queryParams).forEach(key => url.searchParams.append(key, queryParams[key]));
-
- const xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.setRequestHeader('Accept', 'text/html');
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4 && xhr.status === 200) {
- document.getElementById('table-body').innerHTML = xhr.responseText;
- const totalPages = xhr.getResponseHeader('X-total-pages');
- const currentPage = xhr.getResponseHeader('X-current-page');
- updatePagination(parseInt(currentPage, 10), parseInt(totalPages, 10), queryParams.page_size, endpoint);
- }
- };
- xhr.send();
-}
-
-// handle search input for messages or users
-function searchData() {
- const searchInput = document.getElementById('search-bar').value;
- const pageSize = document.getElementById('page-size').value;
- const queryParams = {
- query: searchInput,
- page_size: pageSize
- };
- fetchData(API_ENDPOINT, queryParams);
-}
-
-// Function to update pagination
-function updatePagination(currentPage, totalPages, pageSize) {
- const container = document.getElementById('pagination-container');
- container.innerHTML = ''; // Clear existing pagination
-
- // Create the 'Back' button
- const backLi = document.createElement('li');
- backLi.className = currentPage === 1 ? 'disabled' : 'waves-effect';
- const backA = document.createElement('a');
- backA.href = '#!';
- backA.innerHTML = '«';
- backA.onclick = function() {
- if (currentPage > 1) {
- fetchData(API_ENDPOINT, { page: currentPage - 1, page_size: pageSize });
- }
- };
- backLi.appendChild(backA);
- container.appendChild(backLi);
-
- // Create the page number buttons
- for (let i = 1; i <= totalPages; i++) {
- const li = document.createElement('li');
- li.className = i === currentPage ? 'active' : 'waves-effect';
- const a = document.createElement('a');
- a.href = '#!';
- a.textContent = i;
- a.onclick = function() {
- fetchData(API_ENDPOINT, { page: i, page_size: pageSize });
- };
- li.appendChild(a);
- container.appendChild(li);
- }
-
- // Create the 'Forward' button
- const forwardLi = document.createElement('li');
- forwardLi.className = currentPage === totalPages ? 'disabled' : 'waves-effect';
- const forwardA = document.createElement('a');
- forwardA.href = '#!';
- forwardA.innerHTML = '»';
- forwardA.onclick = function() {
- if (currentPage < totalPages) {
- fetchData(API_ENDPOINT, { page: currentPage + 1, page_size: pageSize });
- }
- };
- forwardLi.appendChild(forwardA);
- container.appendChild(forwardLi);
-}
-
-// Function to sort table by clicking on headers
-function sortTableByHeader(type) {
- document.querySelectorAll('#table-headers th').forEach(header => {
- header.addEventListener('click', () => {
- const sortField = header.getAttribute('data-field');
- const sortOrder = header.getAttribute('data-order');
- const searchInput = document.getElementById('search-bar').value;
- const pageSize = document.getElementById('page-size').value || 10;
- const queryParams = {
- query: searchInput,
- sort_by: sortField,
- sort_order: sortOrder,
- page_size: pageSize
- };
- fetchData(`/api/${type}`, queryParams);
- // Toggle sort order for the next click
- header.setAttribute('data-order', sortOrder === 'asc' ? 'desc' : 'asc');
- });
- });
-}
-
-// Initial fetch for messages and setup headers sorting
-document.addEventListener('DOMContentLoaded', function() {
- M.AutoInit(); // Initialize all Materialize components
- fetchData(API_ENDPOINT, { page: 1, page_size: 10 });
- sortTableByHeader('messages');
-});
-\ No newline at end of file
diff --git a/templates/ajaxtable.html b/templates/ajaxtable.html
@@ -0,0 +1,57 @@
+<div class="row">
+ <div class="input-field col s10">
+ <input type="text" id="search-bar" placeholder="Search Messages...">
+ </div>
+ <div class="input-field col s2">
+ <select id="page-size" onchange="searchData()">
+ <option value="5">5</option>
+ <option value="10" selected>10</option>
+ <option value="25">25</option>
+ <option value="50">50</option>
+ </select>
+ </div>
+</div>
+
+<table class="highlight table-instance">
+ <thead id="table-headers">
+ <tr>
+ <th data-field="id" data-order="asc" class="sortable-header">ID <i class="material-icons tiny">unfold_more</i></th>
+ <th data-field="relevance" data-order="asc" class="sortable-header">Relevance <i class="material-icons tiny">unfold_more</i></th>
+ <th data-field="name" data-order="asc" class="sortable-header">Name <i class="material-icons tiny">unfold_more</i></th>
+ <th data-field="content" data-order="asc" class="sortable-header">Content <i class="material-icons tiny">unfold_more</i></th>
+ </tr>
+ </thead>
+ <tbody id="table-body">
+ <!-- Table data -->
+ </tbody>
+</table>
+
+<ul class="pagination" id="pagination-container">
+ <!-- Pagination -->
+</ul>
+
+<script>
+
+ const API_ENDPOINT = '/api/messages/';
+
+ document.addEventListener('DOMContentLoaded', function() {
+ M.AutoInit();
+ const debouncedSearchData = debounce(searchData, 500);
+ document.getElementById('search-bar').addEventListener('keyup', debouncedSearchData);
+ sortTableByHeader('id');
+
+ fetchData(API_ENDPOINT, { page: 1, page_size: 10 });
+ sortTableByHeader('id');
+ });
+
+ function debounce(func, delay) {
+ let debounceTimer;
+ return function() {
+ const context = this;
+ const args = arguments;
+ clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(() => func.apply(context, args), delay);
+ };
+ }
+
+</script>
+\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
@@ -6,12 +6,12 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
- <script src="{{ url_for('static', filename='js/materialize.min.js') }}"></script>
+ <script defer src="{{ url_for('static', filename='js/materialize.min.js') }}"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
- <script src="{{ url_for('static', filename='js/pagination.js') }}"></script>
+ <script defer src="{{ url_for('static', filename='js/ajaxtable.js') }}"></script>
</head>
<body>
diff --git a/templates/home.html b/templates/home.html
@@ -1,27 +1,5 @@
{% extends 'base.html' %}
{% block content %}
-<br>
-<table class="highlight">
- <thead>
- <tr>
- <th>Sender</th>
- <th>Email</th>
- <th>Timestamp</th>
- <th>Relevance</th>
- <th class="truncate">Message</th>
- </tr>
- </thead>
- <tbody>
- {% for message in messages %}
- <tr>
- <td>{{ message.name }}</td>
- <td>{{ message.email }}</td>
- <td>{{ message.timestamp }}</td>
- <td>{{ message.relevance }}</td>
- <td class="truncate-td">{{ message.message_content }}</td>
- </tr>
- {% endfor %}
- </tbody>
-</table>
+{% include 'ajaxtable.html' %}
{% endblock %}
\ No newline at end of file
diff --git a/templates/messages_template.html b/templates/messages_template.html
@@ -1 +1 @@
-{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.name }}</td><td>{{ message.email }}</td><td>{{ message.subject }}</td><td>{{ message.timestamp }}</td><td>{{ message.relevance }}</td></tr>{% endfor %}
-\ No newline at end of file
+{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.relevance }}</td><td>{{ message.name }}</td><td>{{ message.content }}</td></tr>{% endfor %}
+\ No newline at end of file
diff --git a/templates/table.html b/templates/table.html
@@ -12,7 +12,7 @@
{% endfor %}
</tr>
</thead>
- <tbody>
+ <tbody id="table-body">
{% for row in table.rows %}
<tr onclick="window.location.href='{{ table.row_click_url_template.format(row[0]) }}';">
{% for cell in row %}