Skip to content

Commit 813083f

Browse files
authored
Update Admin Panel to work via iframes (#799)
* wrap admin panel in iframe * slightly better * Extract object info and query it with the meshdb API * Move script to its own file * Now if only I could get my admin map to behave * Aha, I have to listen for messages * Victory * Allow map to change location of admin panel * Refactor style sheets * Restore map.js * make it not crash * Update environment variables * Checkpoint: Subway refactor. This works!? * another checkpoint * another checkpoint: going to refactor iframe_panel to admin_panel_iframe * Capture back button requests * Delete dead code * getting the map to be resizable through css * computers are fucking dumb; * some cleanup * debugging weird flow control stuff * OK I think we have feature pairity * Pick up where we left off (protect against refreshes) * Pass URL through to the Admin Panel iFrame * Ope my code sucks * I have no idea what is going on * Fix admin panel using relative URLs * refactoring checkpoint * Refactoring checkpoint 2 * Strip 'panel' out of get_admin_url * Skip iframe in search tests * same for list * same for change * Lint * Delete a bunch of stuff * checkpoint: delete more things * fix layout issues * Swap /admin and /admin/panel * Revert "same for change" This reverts commit 5409788. * Revert "same for list" This reverts commit 75c25a3. * Revert "Skip iframe in search tests" This reverts commit 27b0919. * Add simple test * Add a slightly more complex test * Acutally, just add one test. See comment as to why * whoops * Add a way to get to /admin/panel and update map button logic * Swap /admin out from under users * Fix URL if user goes to /admin/panel, I should probs make the URL a variable * Refactor * andrew comments 1: Restore password reset, remove state override, fix login page * andrew comments 2: Path * lint * Redirect top to login on logout * Move iframe escape logic to onAdminPanelLoad * Make clicking an los work * Fixes, disable map if on mobile * Remove override that was breaking mobile * Update src/meshweb/static/admin/iframe_check.js * Update src/meshweb/static/admin/map.js * Fix variable and delete log lines
1 parent 75d3b7d commit 813083f

File tree

15 files changed

+493
-345
lines changed

15 files changed

+493
-345
lines changed

src/meshapi/admin/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
AdminPasswordResetDoneView,
88
AdminPasswordResetView,
99
)
10+
from meshdb.views import admin_iframe_view
1011

1112
urlpatterns = [
1213
path("password_reset/", AdminPasswordResetView.as_view(), name="admin_password_reset"),
1314
path("password_reset/done/", AdminPasswordResetDoneView.as_view(), name="password_reset_done"),
1415
path("password_reset/<uidb64>/<token>/", AdminPasswordResetConfirmView.as_view(), name="password_reset_confirm"),
1516
path("password_reset/done/", AdminPasswordResetCompleteView.as_view(), name="password_reset_complete"),
17+
path("iframe_wrapper/", admin_iframe_view),
1618
path("", admin.site.urls),
1719
]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import datetime
2+
3+
from bs4 import BeautifulSoup
4+
from django.contrib.auth.models import Group, User
5+
from django.test import Client, TestCase
6+
from rest_framework.authtoken.models import TokenProxy
7+
8+
from meshapi.models import LOS, AccessPoint, Building, Device, Install, Link, Member, Node, Sector
9+
from meshapi.tests.sample_data import sample_building, sample_device, sample_install, sample_member, sample_node
10+
from meshapi_hooks.hooks import CelerySerializerHook
11+
12+
13+
class TestAdminPanel(TestCase):
14+
c = Client()
15+
16+
def setUp(self) -> None:
17+
sample_install_copy = sample_install.copy()
18+
self.building_1 = Building(**sample_building)
19+
self.building_1.save()
20+
sample_install_copy["building"] = self.building_1
21+
22+
self.building_2 = Building(**sample_building)
23+
self.building_2.save()
24+
25+
self.los = LOS(
26+
from_building=self.building_1,
27+
to_building=self.building_2,
28+
analysis_date=datetime.date(2024, 1, 1),
29+
source=LOS.LOSSource.HUMAN_ANNOTATED,
30+
)
31+
self.los.save()
32+
33+
self.member = Member(**sample_member)
34+
self.member.save()
35+
sample_install_copy["member"] = self.member
36+
37+
self.install = Install(**sample_install_copy)
38+
self.install.save()
39+
40+
self.node1 = Node(**sample_node)
41+
self.node1.save()
42+
self.node2 = Node(**sample_node)
43+
self.node2.save()
44+
45+
self.device1 = Device(**sample_device)
46+
self.device1.node = self.node1
47+
self.device1.save()
48+
49+
self.device2 = Device(**sample_device)
50+
self.device2.node = self.node2
51+
self.device2.save()
52+
53+
self.sector = Sector(
54+
radius=1,
55+
azimuth=45,
56+
width=180,
57+
**sample_device,
58+
)
59+
self.sector.node = self.node2
60+
self.sector.save()
61+
62+
self.access_point = AccessPoint(
63+
**sample_device,
64+
latitude=0,
65+
longitude=0,
66+
)
67+
self.access_point.node = self.node2
68+
self.access_point.save()
69+
70+
self.link = Link(
71+
from_device=self.device1,
72+
to_device=self.device2,
73+
status=Link.LinkStatus.ACTIVE,
74+
)
75+
self.link.save()
76+
77+
self.admin_user = User.objects.create_superuser(
78+
username="admin", password="admin_password", email="admin@example.com"
79+
)
80+
self.c.login(username="admin", password="admin_password")
81+
82+
self.test_group = Group.objects.create(name="Test group")
83+
84+
self.test_auth_token = TokenProxy.objects.create(user=self.admin_user)
85+
86+
self.test_webhook = CelerySerializerHook.objects.create(
87+
user=self.admin_user, target="http://example.com", event="building.created", headers=""
88+
)
89+
90+
def test_iframe_loads(self):
91+
route = "/admin/iframe_wrapper/"
92+
code = 200
93+
response = self.c.get(route)
94+
self.assertEqual(code, response.status_code, f"Could not view {route} in the admin panel.")
95+
96+
decoded_panel = response.content.decode()
97+
soup = BeautifulSoup(decoded_panel, "html.parser")
98+
iframe = soup.find(id="admin_panel_iframe")
99+
iframe_src = iframe.attrs["src"]
100+
self.assertEqual("/admin/", iframe_src)
101+
iframe_response = self.c.get(iframe_src)
102+
self.assertEqual(code, iframe_response.status_code, f"Could not view {route} in the admin panel.")
103+
104+
# TODO (wdn): Add more tests checking if navigating to xyz page works
105+
# Unfortunately, because that is a lot of javascript, it's tricky to test.
106+
# It may be possible to run selenium integration tests or something to validate
107+
# that functionality

src/meshdb/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161

6262
USE_X_FORWARDED_HOST = True
6363

64+
X_FRAME_OPTIONS = "SAMEORIGIN"
65+
6466
SECURE_HSTS_SECONDS = 31536000
6567
SECURE_HSTS_PRELOAD = False
6668
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
@@ -170,6 +172,7 @@
170172

171173
CORS_ALLOWED_ORIGINS += [
172174
"http://127.0.0.1:3000",
175+
"http://127.0.0.1:3001",
173176
"http://localhost:3000",
174177
"http://127.0.0.1:80",
175178
"http://localhost:80",

src/meshdb/templates/admin/base.html

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
33
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" dir="{{ LANGUAGE_BIDI|yesno:'rtl,ltr,auto' }}">
44
<head>
5+
<script>const PANEL_URL = "/admin/iframe_wrapper/";</script>
6+
<script src="{% static '/admin/mobile_check.js' %}"></script>
7+
<script src="{% static 'admin/iframe_check.js' %}"></script>
58
<title>{% block title %}{% endblock %}</title>
69
<link rel="stylesheet" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
710
{% block dark-mode-vars %}
@@ -82,40 +85,35 @@
8285
{% endblock %}
8386
{% endif %}
8487

85-
<div id="map-wrapper">
86-
<div class="main flex" id="main">
87-
{% if not is_popup and is_nav_sidebar_enabled %}
88-
{% block nav-sidebar %}
89-
{% include "admin/nav_sidebar.html" %}
90-
{% endblock %}
88+
<div class="main flex" id="main">
89+
{% if not is_popup and is_nav_sidebar_enabled %}
90+
{% block nav-sidebar %}
91+
{% include "admin/nav_sidebar.html" %}
92+
{% endblock %}
93+
{% endif %}
94+
<div id="content-start" class="content" tabindex="-1">
95+
{% block messages %}
96+
{% if messages %}
97+
<ul class="messagelist">{% for message in messages %}
98+
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message|capfirst }}</li>
99+
{% endfor %}</ul>
91100
{% endif %}
92-
<div id="content-start" class="content" tabindex="-1">
93-
{% block messages %}
94-
{% if messages %}
95-
<ul class="messagelist">{% for message in messages %}
96-
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message|capfirst }}</li>
97-
{% endfor %}</ul>
98-
{% endif %}
99-
{% endblock messages %}
100-
<!-- Content -->
101-
<div id="content" class="{% block coltype %}colM{% endblock %}">
102-
{% block pretitle %}{% endblock %}
103-
{% block content_title %}{% if title %}<h1>{{ title }}</h1>{% endif %}{% endblock %}
104-
{% block content_subtitle %}{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}{% endblock %}
105-
{% block content %}
106-
{% block object-tools %}{% endblock %}
107-
{{ content }}
108-
{% endblock %}
109-
{% block sidebar %}{% endblock %}
110-
<br class="clear">
111-
</div>
112-
<!-- END Content -->
113-
{% block footer %}<div id="footer"></div>{% endblock %}
114-
</div>
101+
{% endblock messages %}
102+
<!-- Content -->
103+
<div id="content" class="{% block coltype %}colM{% endblock %}">
104+
{% block pretitle %}{% endblock %}
105+
{% block content_title %}{% if title %}<h1>{{ title }}</h1>{% endif %}{% endblock %}
106+
{% block content_subtitle %}{% if subtitle %}<h2>{{ subtitle }}</h2>{% endif %}{% endblock %}
107+
{% block content %}
108+
{% block object-tools %}{% endblock %}
109+
{{ content }}
110+
{% endblock %}
111+
{% block sidebar %}{% endblock %}
112+
<br class="clear">
115113
</div>
116-
{% block map_sidebar %}
117-
{% include "admin/map_sidebar.html" %}
118-
{% endblock %}
114+
<!-- END Content -->
115+
{% block footer %}<div id="footer"></div>{% endblock %}
116+
</div>
119117
</div>
120118

121119
</div>

src/meshdb/templates/admin/base_site.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ <h1 id="site-name"><a href="{% url 'admin:index' %}"><img src="{% static 'meshwe
1717

1818
{% block userlinks %}
1919
{{ block.super }}
20-
<a href="#" class="hidden" style="margin-left: 5px; vertical-align: middle; border: none;" id="show_map_button">
21-
<img src="{% static '/admin/map/img/map.png' %}" height="16px" title="Show Map">
22-
</a>
2320
{% endblock %}
2421

2522
{% block footer %}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{% load static %}
2+
3+
{% load env_extras %}
4+
5+
<!DOCTYPE html>
6+
<html>
7+
<head>
8+
<script>
9+
const MAP_BASE_URL = "{% get_env_var 'ADMIN_MAP_BASE_URL' %}";
10+
// PANEL_URL is necessary, but inherited from the admin panel
11+
</script>
12+
<!--We don't want people navigating to this URL, so we're gonna redirect them
13+
if they find themselves here-->
14+
<script src="{% static 'admin/panel_url_check.js' %}"></script>
15+
<script src="{% static '/admin/mobile_check.js' %}"></script>
16+
<!--Script that powers this iframed view and communicates between the admin panel
17+
and the map-->
18+
<script src="{% static '/admin/map.js' %}" defer></script>
19+
<title>{% block title %}{% endblock %}</title>
20+
<link rel="stylesheet" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
21+
{% block dark-mode-vars %}
22+
<link rel="stylesheet" href="{% static "admin/css/dark_mode.css" %}">
23+
<script src="{% static "admin/js/theme.js" %}" defer></script>
24+
{% endblock %}
25+
{% if not is_popup and is_nav_sidebar_enabled %}
26+
<link rel="stylesheet" href="{% static "admin/css/nav_sidebar.css" %}">
27+
<script src="{% static 'admin/js/nav_sidebar.js' %}" defer></script>
28+
{% endif %}
29+
{% block extrastyle %}{% endblock %}
30+
<link rel="stylesheet" href="{% static "admin/admin_ext.css" %}">
31+
{% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}">{% endif %}
32+
{% block extrahead %}{% endblock %}
33+
{% block responsive %}
34+
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
35+
<link rel="stylesheet" href="{% static "admin/css/responsive.css" %}">
36+
{% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{% static "admin/css/responsive_rtl.css" %}">{% endif %}
37+
{% endblock %}
38+
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE">{% endblock %}
39+
<link rel="stylesheet" href="{% static '/admin/iframed.css'%}"/>
40+
</head>
41+
<body>
42+
<div id="page_container">
43+
<div id="admin_panel_div" class="frameGrow">
44+
<iframe src="/admin/" id="admin_panel_iframe" class="frameGrow"></iframe>
45+
</div>
46+
47+
<div class="floating-button-above">
48+
<a href="#" class="button" style="display: inline-block" id="show_map_button">
49+
<img src="{% static '/admin/map/img/map.png' %}" height="16px" title="Show Map">
50+
</a>
51+
</div>
52+
53+
<div id="map_controls">
54+
<!-- This handle is always visible, unless you're resizing, in which case
55+
goes big and invisible to block the iframes from stealing focus -->
56+
<div class="handle" id="handle">
57+
<span class="vert-align-helper"></span>
58+
<img class="handlebar" id="handlebar" src="{% static '/admin/map/img/handlebar.svg' %}" height="60px"/>
59+
</div>
60+
<!-- Only shows up during resizes -->
61+
<div class="handle hidden" id="substituteHandle">
62+
<span class="vert-align-helper"></span>
63+
<img class="handlebar" id="substituteHandlebar" src="{% static '/admin/map/img/handlebar.svg' %}" height="60px"/>
64+
</div>
65+
<div class="floating-button">
66+
<a href="#" class="button" style="display: inline-block" id="map_hide_button">
67+
<img src="{% static '/admin/map/img/cross.png' %}" height="24px" title="Hide Map">
68+
</a>
69+
</div>
70+
<div class="floating-button-below">
71+
<a href="#" class="button" style="display: inline-block" id="map_recenter_button">
72+
<img src="{% static '/admin/map/img/recenter.png' %}" height="24px" title="Recenter map">
73+
</a>
74+
</div>
75+
</div>
76+
77+
<div id="map_panel_div">
78+
<iframe src="{% get_env_var 'ADMIN_MAP_BASE_URL' %}" id="map_panel"></iframe>
79+
</div>
80+
</div>
81+
</body>
82+
</html>

src/meshdb/templates/admin/login.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,3 @@ <h1 id="site-name"><a href="{% url 'admin:index' %}"><img src="{% static 'meshwe
99
{% endif %}
1010
{% endblock %}
1111

12-
13-
{% block map_sidebar %}{% endblock %}

src/meshdb/templates/admin/map_sidebar.html

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/meshdb/views.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.contrib.admin.views.decorators import staff_member_required
2+
from django.http import HttpRequest, HttpResponse
3+
from django.shortcuts import render
4+
5+
6+
@staff_member_required
7+
def admin_iframe_view(request: HttpRequest) -> HttpResponse:
8+
return render(request, "admin/iframed.html")

src/meshweb/static/admin/admin_ext.css

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,13 @@
5252
height: auto;
5353
}
5454

55-
.login #content {
56-
width: 37em !important;
57-
}
58-
5955
/* This is an override of the styles at admin/css/login.css in the admin site */
6056
.login #main {
6157
width: 28em;
6258
margin: auto;
6359
margin-top: 0px;
6460
}
6561

66-
.main.shifted {
67-
min-width: 975px !important;
68-
}
69-
7062
/* Fixes: delete button looks weird */
7163
#main a {
7264
box-sizing: content-box !important;
@@ -137,6 +129,23 @@
137129
margin-left: -4px; /* IDK why this is needed CSS is voodoo */
138130
}
139131

132+
.bigBar {
133+
position: fixed;
134+
top: 0;
135+
left: 0;
136+
width: 100vw;
137+
height: 100vh;
138+
background: transparent;
139+
}
140+
141+
.floating-button-above {
142+
z-index: 100;
143+
position: absolute;
144+
top: 15px;
145+
right: 10px;
146+
padding: 0;
147+
}
148+
140149
.floating-button {
141150
z-index: 100;
142151
position: absolute;

0 commit comments

Comments
 (0)