From 24cb0565b97746ea0bf79366d3b8558933debae9 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 28 Feb 2021 09:05:23 +0000 Subject: [PATCH] add pagination to agent table --- .../migrations/0012_user_agents_per_page.py | 18 ++ api/tacticalrmm/accounts/models.py | 1 + api/tacticalrmm/accounts/tests.py | 1 + api/tacticalrmm/accounts/views.py | 4 + api/tacticalrmm/agents/tests.py | 189 ++++++++++++------ api/tacticalrmm/agents/urls.py | 2 - api/tacticalrmm/agents/views.py | 146 ++++++-------- api/tacticalrmm/core/views.py | 1 + api/tacticalrmm/natsapi/views.py | 2 +- web/src/components/AgentTable.vue | 29 ++- .../modals/coresettings/UserPreferences.vue | 10 +- web/src/store/index.js | 3 - web/src/views/Dashboard.vue | 88 +++++--- 13 files changed, 301 insertions(+), 193 deletions(-) create mode 100644 api/tacticalrmm/accounts/migrations/0012_user_agents_per_page.py diff --git a/api/tacticalrmm/accounts/migrations/0012_user_agents_per_page.py b/api/tacticalrmm/accounts/migrations/0012_user_agents_per_page.py new file mode 100644 index 00000000..a2a83823 --- /dev/null +++ b/api/tacticalrmm/accounts/migrations/0012_user_agents_per_page.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-02-28 06:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0011_user_default_agent_tbl_tab'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='agents_per_page', + field=models.PositiveIntegerField(default=50), + ), + ] diff --git a/api/tacticalrmm/accounts/models.py b/api/tacticalrmm/accounts/models.py index dba7642b..7ab03728 100644 --- a/api/tacticalrmm/accounts/models.py +++ b/api/tacticalrmm/accounts/models.py @@ -27,6 +27,7 @@ class User(AbstractUser, BaseAuditModel): default_agent_tbl_tab = models.CharField( max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server" ) + agents_per_page = models.PositiveIntegerField(default=50) agent = models.OneToOneField( "agents.Agent", diff --git a/api/tacticalrmm/accounts/tests.py b/api/tacticalrmm/accounts/tests.py index 93af76bc..10c6d4bc 100644 --- a/api/tacticalrmm/accounts/tests.py +++ b/api/tacticalrmm/accounts/tests.py @@ -283,6 +283,7 @@ class TestUserAction(TacticalTestCase): "userui": True, "agent_dblclick_action": "editagent", "default_agent_tbl_tab": "mixed", + "agents_per_page": 1000, } r = self.client.patch(url, data, format="json") self.assertEqual(r.status_code, 200) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index 7168388d..db60d576 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -199,4 +199,8 @@ class UserUI(APIView): user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"] user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"]) + if "agents_per_page" in request.data.keys(): + user.agents_per_page = request.data["agents_per_page"] + user.save(update_fields=["agents_per_page"]) + return Response("ok") diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index f3489eee..ad6a8354 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -17,6 +17,107 @@ from .serializers import AgentSerializer from .tasks import auto_self_agent_update_task +class TestAgentsList(TacticalTestCase): + def setUp(self): + self.authenticate() + self.setup_coresettings() + + def test_agents_list(self): + url = "/agents/listagents/" + + # 36 total agents + company1 = baker.make("clients.Client") + company2 = baker.make("clients.Client") + site1 = baker.make("clients.Site", client=company1) + site2 = baker.make("clients.Site", client=company1) + site3 = baker.make("clients.Site", client=company2) + + baker.make_recipe( + "agents.online_agent", site=site1, monitoring_type="server", _quantity=15 + ) + baker.make_recipe( + "agents.online_agent", + site=site2, + monitoring_type="workstation", + _quantity=10, + ) + baker.make_recipe( + "agents.online_agent", + site=site3, + monitoring_type="server", + _quantity=4, + ) + baker.make_recipe( + "agents.online_agent", + site=site3, + monitoring_type="workstation", + _quantity=7, + ) + + data = { + "pagination": { + "rowsPerPage": 50, + "rowsNumber": None, + "sortBy": "hostname", + "descending": False, + "page": 1, + }, + "monType": "mixed", + } + + # test mixed + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data["total"], 36) # type: ignore + self.assertEqual(len(r.data["agents"]), 36) # type: ignore + + # test servers + data["monType"] = "server" + data["pagination"]["rowsPerPage"] = 6 + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data["total"], 19) # type: ignore + self.assertEqual(len(r.data["agents"]), 6) # type: ignore + + # test workstations + data["monType"] = "server" + data["pagination"]["rowsPerPage"] = 6 + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data["total"], 19) # type: ignore + self.assertEqual(len(r.data["agents"]), 6) # type: ignore + + # test client1 mixed + data = { + "pagination": { + "rowsPerPage": 3, + "rowsNumber": None, + "sortBy": "hostname", + "descending": False, + "page": 1, + }, + "monType": "mixed", + "clientPK": company1.pk, # type: ignore + } + + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data["total"], 25) # type: ignore + self.assertEqual(len(r.data["agents"]), 3) # type: ignore + + # test site3 workstations + del data["clientPK"] + data["monType"] = "workstation" + data["sitePK"] = site3.pk # type: ignore + + r = self.client.patch(url, data, format="json") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data["total"], 7) # type: ignore + self.assertEqual(len(r.data["agents"]), 3) # type: ignore + + self.check_not_authenticated("patch", url) + + class TestAgentViews(TacticalTestCase): def setUp(self): self.authenticate() @@ -256,7 +357,7 @@ class TestAgentViews(TacticalTestCase): mock_ret.return_value = "nt authority\system" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) - self.assertIsInstance(r.data, str) + self.assertIsInstance(r.data, str) # type: ignore mock_ret.return_value = "timeout" r = self.client.post(url, data, format="json") @@ -276,15 +377,15 @@ class TestAgentViews(TacticalTestCase): nats_cmd.return_value = "ok" r = self.client.patch(url, data, format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") - self.assertEqual(r.data["agent"], self.agent.hostname) + self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") # type: ignore + self.assertEqual(r.data["agent"], self.agent.hostname) # type: ignore nats_data = { "func": "schedtask", "schedtaskpayload": { "type": "schedreboot", "trigger": "once", - "name": r.data["task_name"], + "name": r.data["task_name"], # type: ignore "year": 2025, "month": "August", "day": 29, @@ -305,7 +406,7 @@ class TestAgentViews(TacticalTestCase): r = self.client.patch(url, data_invalid, format="json") self.assertEqual(r.status_code, 400) - self.assertEqual(r.data, "Invalid date") + self.assertEqual(r.data, "Invalid date") # type: ignore self.check_not_authenticated("patch", url) @@ -316,8 +417,8 @@ class TestAgentViews(TacticalTestCase): site = baker.make("clients.Site") data = { - "client": site.client.id, - "site": site.id, + "client": site.client.id, # type: ignore + "site": site.id, # type: ignore "arch": "64", "expires": 23, "installMethod": "exe", @@ -401,14 +502,6 @@ class TestAgentViews(TacticalTestCase): self.check_not_authenticated("post", url) - def test_agents_list(self): - url = "/agents/listagents/" - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - - self.check_not_authenticated("get", url) - def test_agents_agent_detail(self): url = f"/agents/{self.agent.pk}/agentdetail/" @@ -425,7 +518,7 @@ class TestAgentViews(TacticalTestCase): edit = { "id": self.agent.pk, - "site": site.id, + "site": site.id, # type: ignore "monitoring_type": "workstation", "description": "asjdk234andasd", "offline_time": 4, @@ -456,7 +549,7 @@ class TestAgentViews(TacticalTestCase): agent = Agent.objects.get(pk=self.agent.pk) data = AgentSerializer(agent).data - self.assertEqual(data["site"], site.id) + self.assertEqual(data["site"], site.id) # type: ignore policy = WinUpdatePolicy.objects.get(agent=self.agent) data = WinUpdatePolicySerializer(policy).data @@ -474,21 +567,21 @@ class TestAgentViews(TacticalTestCase): # TODO # decode the cookie - self.assertIn("&viewmode=13", r.data["file"]) - self.assertIn("&viewmode=12", r.data["terminal"]) - self.assertIn("&viewmode=11", r.data["control"]) + self.assertIn("&viewmode=13", r.data["file"]) # type: ignore + self.assertIn("&viewmode=12", r.data["terminal"]) # type: ignore + self.assertIn("&viewmode=11", r.data["control"]) # type: ignore - self.assertIn("&gotonode=", r.data["file"]) - self.assertIn("&gotonode=", r.data["terminal"]) - self.assertIn("&gotonode=", r.data["control"]) + self.assertIn("&gotonode=", r.data["file"]) # type: ignore + self.assertIn("&gotonode=", r.data["terminal"]) # type: ignore + self.assertIn("&gotonode=", r.data["control"]) # type: ignore - self.assertIn("?login=", r.data["file"]) - self.assertIn("?login=", r.data["terminal"]) - self.assertIn("?login=", r.data["control"]) + self.assertIn("?login=", r.data["file"]) # type: ignore + self.assertIn("?login=", r.data["terminal"]) # type: ignore + self.assertIn("?login=", r.data["control"]) # type: ignore - self.assertEqual(self.agent.hostname, r.data["hostname"]) - self.assertEqual(self.agent.client.name, r.data["client"]) - self.assertEqual(self.agent.site.name, r.data["site"]) + self.assertEqual(self.agent.hostname, r.data["hostname"]) # type: ignore + self.assertEqual(self.agent.client.name, r.data["client"]) # type: ignore + self.assertEqual(self.agent.site.name, r.data["site"]) # type: ignore self.assertEqual(r.status_code, 200) @@ -498,32 +591,6 @@ class TestAgentViews(TacticalTestCase): self.check_not_authenticated("get", url) - def test_by_client(self): - url = f"/agents/byclient/{self.agent.client.id}/" - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertTrue(r.data) - - url = f"/agents/byclient/500/" - r = self.client.get(url) - self.assertFalse(r.data) # returns empty list - - self.check_not_authenticated("get", url) - - def test_by_site(self): - url = f"/agents/bysite/{self.agent.site.id}/" - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertTrue(r.data) - - url = f"/agents/bysite/500/" - r = self.client.get(url) - self.assertEqual(r.data, []) - - self.check_not_authenticated("get", url) - def test_overdue_action(self): url = "/agents/overdueaction/" @@ -532,14 +599,14 @@ class TestAgentViews(TacticalTestCase): self.assertEqual(r.status_code, 200) agent = Agent.objects.get(pk=self.agent.pk) self.assertTrue(agent.overdue_email_alert) - self.assertEqual(self.agent.hostname, r.data) + self.assertEqual(self.agent.hostname, r.data) # type: ignore payload = {"pk": self.agent.pk, "overdue_text_alert": False} r = self.client.post(url, payload, format="json") self.assertEqual(r.status_code, 200) agent = Agent.objects.get(pk=self.agent.pk) self.assertFalse(agent.overdue_text_alert) - self.assertEqual(self.agent.hostname, r.data) + self.assertEqual(self.agent.hostname, r.data) # type: ignore self.check_not_authenticated("post", url) @@ -683,7 +750,7 @@ class TestAgentViews(TacticalTestCase): nats_cmd.return_value = "ok" r = self.client.get(url) self.assertEqual(r.status_code, 200) - self.assertIn(self.agent.hostname, r.data) + self.assertIn(self.agent.hostname, r.data) # type: ignore nats_cmd.assert_called_with( {"func": "recover", "payload": {"mode": "mesh"}}, timeout=45 ) @@ -800,7 +867,7 @@ class TestAgentViewsNew(TacticalTestCase): r = self.client.post(url, format="json") self.assertEqual(r.status_code, 200) - self.assertEqual(r.data, data) + self.assertEqual(r.data, data) # type: ignore self.check_not_authenticated("post", url) @@ -812,14 +879,14 @@ class TestAgentViewsNew(TacticalTestCase): agent = baker.make_recipe("agents.agent", site=site) # Test client toggle maintenance mode - data = {"type": "Client", "id": site.client.id, "action": True} + data = {"type": "Client", "id": site.client.id, "action": True} # type: ignore r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode) # Test site toggle maintenance mode - data = {"type": "Site", "id": site.id, "action": False} + data = {"type": "Site", "id": site.id, "action": False} # type: ignore r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) diff --git a/api/tacticalrmm/agents/urls.py b/api/tacticalrmm/agents/urls.py index 515199ac..d4d488ff 100644 --- a/api/tacticalrmm/agents/urls.py +++ b/api/tacticalrmm/agents/urls.py @@ -6,8 +6,6 @@ urlpatterns = [ path("listagents/", views.AgentsTableList.as_view()), path("listagentsnodetail/", views.list_agents_no_detail), path("/agenteditdetails/", views.agent_edit_details), - path("byclient//", views.by_client), - path("bysite//", views.by_site), path("overdueaction/", views.overdue_action), path("sendrawcmd/", views.send_raw_cmd), path("/agentdetail/", views.agent_detail), diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index da001dc4..8c7487a9 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -5,11 +5,13 @@ import random import string from django.conf import settings +from django.core.paginator import Paginator +from django.db.models import Q from django.http import HttpResponse from django.shortcuts import get_object_or_404 from loguru import logger from packaging import version as pyver -from rest_framework import generics, status +from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.views import APIView @@ -224,37 +226,61 @@ def send_raw_cmd(request): return Response(r) -class AgentsTableList(generics.ListAPIView): - queryset = ( - Agent.objects.select_related("site") - .prefetch_related("agentchecks") - .only( - "pk", - "hostname", - "agent_id", - "site", - "monitoring_type", - "description", - "needs_reboot", - "overdue_text_alert", - "overdue_email_alert", - "overdue_time", - "offline_time", - "last_seen", - "boot_time", - "logged_in_username", - "last_logged_in_user", - "time_zone", - "maintenance_mode", - ) - ) - serializer_class = AgentTableSerializer +class AgentsTableList(APIView): + def patch(self, request): + pagination = request.data["pagination"] + monType = request.data["monType"] + client = Q() + site = Q() + mon_type = Q() + + if monType == "server": + mon_type = Q(monitoring_type="server") + elif monType == "workstation": + mon_type = Q(monitoring_type="workstation") + + if "clientPK" in request.data: + client = Q(site__client_id=request.data["clientPK"]) + + if "sitePK" in request.data: + site = Q(site_id=request.data["sitePK"]) + + queryset = ( + Agent.objects.select_related("site") + .prefetch_related("agentchecks") + .filter(mon_type) + .filter(client) + .filter(site) + .only( + "pk", + "hostname", + "agent_id", + "site", + "monitoring_type", + "description", + "needs_reboot", + "overdue_text_alert", + "overdue_email_alert", + "overdue_time", + "offline_time", + "last_seen", + "boot_time", + "logged_in_username", + "last_logged_in_user", + "time_zone", + "maintenance_mode", + ) + .order_by(pagination["sortBy"]) + ) + paginator = Paginator(queryset, pagination["rowsPerPage"]) - def list(self, request): - queryset = self.get_queryset() ctx = {"default_tz": get_default_timezone()} - serializer = AgentTableSerializer(queryset, many=True, context=ctx) - return Response(serializer.data) + serializer = AgentTableSerializer( + paginator.get_page(pagination["page"]), many=True, context=ctx + ) + + ret = {"agents": serializer.data, "total": paginator.count} + return Response(ret) @api_view() @@ -269,66 +295,6 @@ def agent_edit_details(request, pk): return Response(AgentEditSerializer(agent).data) -@api_view() -def by_client(request, clientpk): - agents = ( - Agent.objects.select_related("site") - .filter(site__client_id=clientpk) - .prefetch_related("agentchecks") - .only( - "pk", - "hostname", - "agent_id", - "site", - "monitoring_type", - "description", - "needs_reboot", - "overdue_text_alert", - "overdue_email_alert", - "overdue_time", - "offline_time", - "last_seen", - "boot_time", - "logged_in_username", - "last_logged_in_user", - "time_zone", - "maintenance_mode", - ) - ) - ctx = {"default_tz": get_default_timezone()} - return Response(AgentTableSerializer(agents, many=True, context=ctx).data) - - -@api_view() -def by_site(request, sitepk): - agents = ( - Agent.objects.filter(site_id=sitepk) - .select_related("site") - .prefetch_related("agentchecks") - .only( - "pk", - "hostname", - "agent_id", - "site", - "monitoring_type", - "description", - "needs_reboot", - "overdue_text_alert", - "overdue_email_alert", - "overdue_time", - "offline_time", - "last_seen", - "boot_time", - "logged_in_username", - "last_logged_in_user", - "time_zone", - "maintenance_mode", - ) - ) - ctx = {"default_tz": get_default_timezone()} - return Response(AgentTableSerializer(agents, many=True, context=ctx).data) - - @api_view(["POST"]) def overdue_action(request): agent = get_object_or_404(Agent, pk=request.data["pk"]) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index 6f17ffa1..3c939cb2 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -63,6 +63,7 @@ def dashboard_info(request): "show_community_scripts": request.user.show_community_scripts, "dbl_click_action": request.user.agent_dblclick_action, "default_agent_tbl_tab": request.user.default_agent_tbl_tab, + "agents_per_page": request.user.agents_per_page, } ) diff --git a/api/tacticalrmm/natsapi/views.py b/api/tacticalrmm/natsapi/views.py index 4bac5ac8..ee2ddd51 100644 --- a/api/tacticalrmm/natsapi/views.py +++ b/api/tacticalrmm/natsapi/views.py @@ -9,9 +9,9 @@ from rest_framework.decorators import ( ) from rest_framework.response import Response from rest_framework.views import APIView -from tacticalrmm.utils import notify_error from agents.models import Agent +from tacticalrmm.utils import notify_error logger.configure(**settings.LOG_CONFIG) diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index ed3eb68d..9a6a89d4 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -13,9 +13,11 @@ row-key="id" binary-state-sort virtual-scroll - :pagination.sync="pagination" - :rows-per-page-options="[0]" + :pagination.sync="agentPagination" + :rows-per-page-options="[25, 50, 100, 200, 300, 500, 1000]" no-data-label="No Agents" + rows-per-page-label="Agents per page:" + @request="onRequest" >