diff --git a/analytics/tests/test_support_views.py b/analytics/tests/test_support_views.py
index 5557fde51e..ea2f62bf74 100644
--- a/analytics/tests/test_support_views.py
+++ b/analytics/tests/test_support_views.py
@@ -25,6 +25,54 @@ from zerver.models import (
if TYPE_CHECKING:
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
+import uuid
+
+from zilencer.models import RemoteZulipServer
+
+
+class TestRemoteServerSupportEndpoint(ZulipTestCase):
+ def setUp(self) -> None:
+ super().setUp()
+
+ # Set up some initial example data.
+ for i in range(20):
+ hostname = f"zulip-{i}.example.com"
+ RemoteZulipServer.objects.create(
+ hostname=hostname, contact_email=f"admin@{hostname}", plan_type=1, uuid=uuid.uuid4()
+ )
+
+ def test_search(self) -> None:
+ self.login("cordelia")
+
+ result = self.client_get("/activity/remote/support")
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(result["Location"], "/login/")
+
+ # Iago is the user with the appropriate permissions to access this page.
+ self.login("iago")
+ assert self.example_user("iago").is_staff
+
+ result = self.client_get("/activity/remote/support")
+ self.assert_in_success_response(
+ [
+ 'input type="text" name="q" class="input-xxlarge search-query" placeholder="hostname or contact email"'
+ ],
+ result,
+ )
+
+ result = self.client_get("/activity/remote/support", {"q": "zulip-1.example.com"})
+ self.assert_in_success_response(["
zulip-1.example.com
"], result)
+ self.assert_not_in_success_response(["zulip-2.example.com
"], result)
+
+ result = self.client_get("/activity/remote/support", {"q": "example.com"})
+ for i in range(20):
+ self.assert_in_success_response([f"zulip-{i}.example.com
"], result)
+
+ result = self.client_get("/activity/remote/support", {"q": "admin@zulip-2.example.com"})
+ self.assert_in_success_response(["zulip-2.example.com
"], result)
+ self.assert_in_success_response(["Contact email: admin@zulip-2.example.com"], result)
+ self.assert_not_in_success_response(["zulip-1.example.com
"], result)
+
class TestSupportEndpoint(ZulipTestCase):
def test_search(self) -> None:
diff --git a/analytics/urls.py b/analytics/urls.py
index 8d7380a906..7d7556c2db 100644
--- a/analytics/urls.py
+++ b/analytics/urls.py
@@ -21,7 +21,7 @@ from analytics.views.stats import (
stats_for_remote_installation,
stats_for_remote_realm,
)
-from analytics.views.support import support
+from analytics.views.support import remote_servers_support, support
from analytics.views.user_activity import get_user_activity
from zerver.lib.rest import rest_path
@@ -30,6 +30,7 @@ i18n_urlpatterns: List[Union[URLPattern, URLResolver]] = [
path("activity", get_installation_activity),
path("activity/remote", get_remote_server_activity),
path("activity/support", support, name="support"),
+ path("activity/remote/support", remote_servers_support, name="remote_servers_support"),
path("realm_activity//", get_realm_activity),
path("user_activity//", get_user_activity),
path("stats/realm//", stats_for_realm),
diff --git a/analytics/views/support.py b/analytics/views/support.py
index c487e498f9..a8a6089750 100644
--- a/analytics/views/support.py
+++ b/analytics/views/support.py
@@ -17,6 +17,7 @@ from django.utils.timesince import timesince
from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
+from analytics.views.activity_common import remote_installation_stats_link
from confirmation.models import Confirmation, confirmation_url
from confirmation.settings import STATUS_USED
from zerver.actions.create_realm import do_change_realm_subdomain
@@ -48,6 +49,9 @@ from zerver.models import (
)
from zerver.views.invite import get_invitee_emails_set
+if settings.ZILENCER_ENABLED:
+ from zilencer.models import RemoteZulipServer
+
if settings.BILLING_ENABLED:
from corporate.lib.stripe import approve_sponsorship as do_approve_sponsorship
from corporate.lib.stripe import (
@@ -406,3 +410,44 @@ def support(
)
return render(request, "analytics/support.html", context=context)
+
+
+def get_remote_servers_for_support(
+ email_to_search: Optional[str], hostname_to_search: Optional[str]
+) -> List["RemoteZulipServer"]:
+ if not email_to_search and not hostname_to_search:
+ return []
+
+ remote_servers_query = RemoteZulipServer.objects.order_by("id")
+ if email_to_search:
+ remote_servers_query = remote_servers_query.filter(contact_email__iexact=email_to_search)
+ elif hostname_to_search:
+ remote_servers_query = remote_servers_query.filter(hostname__icontains=hostname_to_search)
+
+ return list(remote_servers_query)
+
+
+@require_server_admin
+@has_request_variables
+def remote_servers_support(
+ request: HttpRequest, query: Optional[str] = REQ("q", default=None)
+) -> HttpResponse:
+ email_to_search = None
+ hostname_to_search = None
+ if query:
+ if "@" in query:
+ email_to_search = query
+ else:
+ hostname_to_search = query
+
+ remote_servers = get_remote_servers_for_support(
+ email_to_search=email_to_search, hostname_to_search=hostname_to_search
+ )
+ return render(
+ request,
+ "analytics/remote_server_support.html",
+ context=dict(
+ remote_servers=remote_servers,
+ remote_installation_stats_link=remote_installation_stats_link,
+ ),
+ )
diff --git a/templates/analytics/remote_server_support.html b/templates/analytics/remote_server_support.html
new file mode 100644
index 0000000000..f70d393f6c
--- /dev/null
+++ b/templates/analytics/remote_server_support.html
@@ -0,0 +1,33 @@
+{% extends "zerver/base.html" %}
+{% set entrypoint = "support" %}
+
+{# Remote servers. #}
+
+{% block title %}
+Remote servers
+{% endblock %}
+
+
+{% block content %}
+
+
+
+
+
+ {% for remote_server in remote_servers %}
+
+ remote server
+
{{ remote_server.hostname }}
+ Contact email: {{ remote_server.contact_email }}
+ Last updated: {{ remote_server.last_updated|timesince }} ago
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py
index 466ae52592..094192835c 100644
--- a/tools/linter_lib/custom_check.py
+++ b/tools/linter_lib/custom_check.py
@@ -548,6 +548,7 @@ html_rules: List["Rule"] = [
},
"exclude": {
"templates/analytics/support.html",
+ "templates/analytics/remote_server_support.html",
# We have URL template and Pygments language name as placeholders
# in the below template which we don't want to be translatable.
"web/templates/settings/playground_settings_admin.hbs",