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 %} +
+ Clear Sorting +
+ {% if table.show_header %} + + + {% for column in table.columns %} + + {% if column.orderable %} + + {{ column.header }} + + + {% else %} + {{ column.header }} + {% 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