diff --git a/process_manager/tables.py b/process_manager/tables.py
index ddbff6b6..d3f69f50 100644
--- a/process_manager/tables.py
+++ b/process_manager/tables.py
@@ -28,10 +28,12 @@ class ProcessTable(tables.Table):
uuid = tables.Column(
verbose_name="UUID",
+ orderable=True,
attrs={"td": {"class": "fw-bold text-break text-start"}},
)
name = tables.Column(
verbose_name="Process Name",
+ orderable=True,
attrs={
"td": {"class": "fw-bold text-primary text-center"},
"th": {"class": "text-center header-style"},
@@ -39,6 +41,7 @@ class ProcessTable(tables.Table):
)
user = tables.Column(
verbose_name="User",
+ orderable=True,
attrs={
"td": {"class": "text-secondary text-center"},
"th": {"class": "text-center header-style"},
@@ -46,6 +49,7 @@ class ProcessTable(tables.Table):
)
session = tables.Column(
verbose_name="Session",
+ orderable=True,
attrs={
"td": {"class": "text-secondary text-center"},
"th": {"class": "text-center header-style"},
@@ -53,6 +57,7 @@ class ProcessTable(tables.Table):
)
status_code = tables.Column(
verbose_name="Status",
+ orderable=True,
attrs={
"td": {"class": "fw-bold text-center"},
"th": {"class": "text-center header-style"},
@@ -60,6 +65,7 @@ class ProcessTable(tables.Table):
)
exit_code = tables.Column(
verbose_name="Exit Code",
+ orderable=True,
attrs={
"td": {"class": "text-center"},
"th": {"class": "text-center header-style"},
@@ -68,6 +74,7 @@ class ProcessTable(tables.Table):
logs = tables.TemplateColumn(
logs_column_template,
verbose_name="Logs",
+ orderable=False,
attrs={
"td": {"class": "text-center"},
"th": {"class": "text-center header-style"},
@@ -75,6 +82,7 @@ class ProcessTable(tables.Table):
)
select = tables.CheckBoxColumn(
accessor="uuid",
+ orderable=False,
verbose_name="Select",
attrs={
"th__input": {
diff --git a/process_manager/templates/process_manager/index.html b/process_manager/templates/process_manager/index.html
index 99de5e74..2e0a1f69 100644
--- a/process_manager/templates/process_manager/index.html
+++ b/process_manager/templates/process_manager/index.html
@@ -15,6 +15,67 @@
max-height: 80vh;
overflow-y: auto;
}
+ #search-dropdown {
+ width: auto;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+ }
+ .custom-thead {
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ }
+ .custom-th {
+ padding: 10px;
+ text-align: center;
+ font-weight: 600;
+ color: #495057;
+ }
+ .sort-link {
+ text-decoration: none;
+ color: #007bff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .sort-icon {
+ font-size: 0.9em;
+ margin-left: 5px;
+ opacity: 0.6;
+ }
+ .table-header {
+ font-weight: bold;
+ color: #6c757d;
+ }
+ .clear-sorting {
+ padding: 5px 12px;
+ background-color: #f8f9fa;
+ color: #495057;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ font-size: 0.9em;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: none;
+ transition: background-color 0.3s ease;
+ }
+ .sort-link {
+ cursor: pointer;
+ text-decoration: none;
+ color: blue;
+ }
+
+
+ .sort-link:hover {
+ text-decoration: underline;
+ color: darkblue;
+ }
+
+ .clear-sorting:hover {
+ background-color: #e2e6ea;
+ }
.table-container {
border-radius: 10px;
}
@@ -171,13 +232,28 @@
Process Control
class="btn btn-info w-100 ms-2"
_="on click hide me toggle .hide-messages on #main-content">Show Messages
-
+
+
+
+
+
diff --git a/process_manager/templates/process_manager/partials/process_table.html b/process_manager/templates/process_manager/partials/process_table.html
index 7853b54b..30486b10 100644
--- a/process_manager/templates/process_manager/partials/process_table.html
+++ b/process_manager/templates/process_manager/partials/process_table.html
@@ -1,2 +1,32 @@
+{% extends "django_tables2/table.html" %}
{% load render_table from django_tables2 %}
+{% load querystring from django_tables2 %}
+{% block table.thead %}
+
+ {% if table.show_header %}
+
+
+ {% for column in table.columns %}
+
+ {% if column.orderable %}
+
+ {{ column.header }}
+ ⇅
+
+ {% else %}
+
+ {% endif %}
+ |
+ {% endfor %}
+
+
+ {% endif %}
+{% endblock table.thead %}
{% render_table table %}
diff --git a/process_manager/views/partials.py b/process_manager/views/partials.py
index 9ed84bc0..364e0340 100644
--- a/process_manager/views/partials.py
+++ b/process_manager/views/partials.py
@@ -1,6 +1,5 @@
"""View functions for partials."""
-import django_tables2
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
@@ -13,19 +12,18 @@
def filter_table(
- search: str, table: list[dict[str, str | int]]
+ search: str, column: str, table: list[dict[str, str | int]]
) -> list[dict[str, str | int]]:
- """Filter table data based on search parameter.
+ """Filter table data based on search and column parameters.
If the search parameter is empty, the table data is returned unfiltered. Otherwise,
- the table data is filtered based on the search parameter. The search parameter can
- be a string or a string with a column name and search string separated by a colon.
- If the search parameter is a column name, the search string is matched against the
- values in that column only. Otherwise, the search string is matched against all
- columns.
+ the table data is filtered based on the search parameter. If the column parameter
+ is provided, the search string is matched against the values in that column only.
+ If no valid column is specified, the search string is matched against all columns.
Args:
search: The search string to filter the table data.
+ column: The column name to filter by, or an empty string to search all columns.
table: The table data to filter.
Returns:
@@ -35,17 +33,9 @@ def filter_table(
return table
all_cols = list(table[0].keys())
- column, _, search = search.partition(":")
- if not search:
- # No column-based filtering
- search = column
- columns = all_cols
- elif column not in all_cols:
- # If column is unknown, search all columns
- columns = all_cols
- else:
- # Search only the specified column
- columns = [column]
+ columns = [column] if column in all_cols else all_cols
+
+ # Convert search string to lowercase for case-insensitive matching
search = search.lower()
return [row for row in table if any(search in str(row[k]).lower() for k in columns)]
@@ -63,31 +53,36 @@ def process_table(request: HttpRequest) -> HttpResponse:
status_enum_lookup = dict(item[::-1] for item in ProcessInstance.StatusCode.items())
- table_data = []
- process_instances = session_info.data.values
- for process_instance in process_instances:
- metadata = process_instance.process_description.metadata
- uuid = process_instance.uuid.uuid
- table_data.append(
- {
- "uuid": uuid,
- "name": metadata.name,
- "user": metadata.user,
- "session": metadata.session,
- "status_code": status_enum_lookup[process_instance.status_code],
- "exit_code": process_instance.return_code,
- }
- )
- # Filter table data based on search parameter
- table_data = filter_table(request.GET.get("search", ""), table_data)
+ # Build the table data
+ table_data = [
+ {
+ "uuid": process_instance.uuid.uuid,
+ "name": process_instance.process_description.metadata.name,
+ "user": process_instance.process_description.metadata.user,
+ "session": process_instance.process_description.metadata.session,
+ "status_code": status_enum_lookup[process_instance.status_code],
+ "exit_code": process_instance.return_code,
+ }
+ for process_instance in session_info.data.values
+ ]
+ # Get the values from the GET request
+ search_dropdown = request.GET.get("search-drp", "")
+ search_input = request.GET.get("search", "")
+
+ # Determine the column and search values for filtering
+ column = search_dropdown if search_dropdown else ""
+ search = search_input if search_input else ""
+
+ # Apply search filtering
+ table_data = filter_table(search, column, table_data)
table = ProcessTable(table_data)
- # sort table data based on request parameters
- table_configurator = django_tables2.RequestConfig(request)
- table_configurator.configure(table)
+ # Set the order based on the 'sort' parameter in the GET request, defaulting to ''
+ sort_param = request.GET.get("sort", "")
+ table.order_by = sort_param
return render(
request=request,
- context=dict(table=table),
+ context={"table": table},
template_name="process_manager/partials/process_table.html",
)
diff --git a/tests/process_manager/views/test_partial_views.py b/tests/process_manager/views/test_partial_views.py
index ac67aa58..ee693f81 100644
--- a/tests/process_manager/views/test_partial_views.py
+++ b/tests/process_manager/views/test_partial_views.py
@@ -29,12 +29,14 @@ def test_get(self, auth_client, mocker):
def _mock_session_info(self, mocker, uuids, sessions: list[str] = []):
"""Mocks views.get_session_info with ProcessInstanceList like data."""
mock = mocker.patch("process_manager.views.partials.get_session_info")
- instance_mocks = [MagicMock() for uuid in uuids]
+ instance_mocks = [MagicMock() for _ in uuids]
sessions = sessions or [f"session{i}" for i in range(len(uuids))]
+
for instance_mock, uuid, session in zip(instance_mocks, uuids, sessions):
instance_mock.uuid.uuid = str(uuid)
instance_mock.process_description.metadata.session = session
instance_mock.status_code = 0
+
mock().data.values.__iter__.return_value = instance_mocks
return mock
@@ -43,13 +45,23 @@ def test_get_with_search(self, auth_client: Client, mocker):
uuids = [str(uuid4()) for _ in range(5)]
sessions = ["session1", "session2", "session2", "session2", "session3"]
self._mock_session_info(mocker, uuids, sessions)
- response = auth_client.get(self.endpoint, data={"search": "session2"})
+
+ # Perform the search request using 'search-drp' and 'search' parameters
+ response = auth_client.get(
+ self.endpoint, data={"search-drp": "session", "search": "session2"}
+ )
assert response.status_code == HTTPStatus.OK
+
+ # Retrieve the filtered table data
table = response.context["table"]
assert isinstance(table, ProcessTable)
+
+ # Check that each row in the table contains "session2" as the session value
for row, uuid in zip(table.data.data, uuids[1:4]):
+ assert (
+ row["session"] == "session2"
+ ), f"Expected 'session2', got '{row['session']}'"
assert row["uuid"] == uuid
- assert row["session"] == "session2"
process_1 = {
@@ -71,9 +83,10 @@ def test_get_with_search(self, auth_client: Client, mocker):
@pytest.mark.parametrize(
- "search,table,expected",
+ "search, column, table, expected",
[
pytest.param(
+ "",
"",
[process_1, process_2],
[process_1, process_2],
@@ -81,38 +94,43 @@ def test_get_with_search(self, auth_client: Client, mocker):
),
pytest.param(
"Process1",
+ "",
[process_1, process_2],
[process_1],
id="search all columns",
),
pytest.param(
- "name:Process1",
+ "Process1",
+ "name",
[process_1, process_2],
[process_1],
id="search specific column",
),
pytest.param(
- "nonexistent:Process1",
+ "Process1",
+ "nonexistent",
[process_1, process_2],
[process_1],
id="search non-existent column",
),
pytest.param(
"Process1",
+ "",
[],
[],
id="filter empty table",
),
pytest.param(
"process1",
+ "",
[process_1, process_2],
[process_1],
id="search case insensitive",
),
],
)
-def test_filter_table(search, table, expected):
+def test_filter_table(search, column, table, expected):
"""Test filter_table function."""
from process_manager.views.partials import filter_table
- assert filter_table(search, table) == expected
+ assert filter_table(search, column, table) == expected