Navigation menu plus menu_links() hook

Closes #1064, refs #690.
This commit is contained in:
Simon Willison 2020-10-29 20:45:15 -07:00 committed by GitHub
commit 18a64fbb29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 193 additions and 13 deletions

View file

@ -750,11 +750,22 @@ class Datasette:
)
extra_template_vars.update(extra_vars)
async def menu_links():
links = []
for hook in pm.hook.menu_links(
datasette=self, actor=request.actor if request else None
):
extra_links = await await_me_maybe(hook)
if extra_links:
links.extend(extra_links)
return links
template_context = {
**context,
**{
"urls": self.urls,
"actor": request.actor if request else None,
"menu_links": menu_links,
"display_actor": display_actor,
"show_logout": request is not None and "ds_actor" in request.cookies,
"app_css_hash": self.app_css_hash(),
@ -1161,6 +1172,7 @@ class DatasetteRouter:
info,
urls=self.ds.urls,
app_css_hash=self.ds.app_css_hash(),
menu_links=lambda: [],
)
),
status=status,

View file

@ -0,0 +1,40 @@
from datasette import hookimpl
@hookimpl
def menu_links(datasette, actor):
if actor and actor.get("id") == "root":
return [
{"href": datasette.urls.path("/-/databases"), "label": "Databases"},
{
"href": datasette.urls.path("/-/plugins"),
"label": "Installed plugins",
},
{
"href": datasette.urls.path("/-/versions"),
"label": "Version info",
},
{
"href": datasette.urls.path("/-/metadata"),
"label": "Metadata",
},
{
"href": datasette.urls.path("/-/config"),
"label": "Config",
},
{
"href": datasette.urls.path("/-/permissions"),
"label": "Debug permissions",
},
{
"href": datasette.urls.path("/-/messages"),
"label": "Debug messages",
},
{
"href": datasette.urls.path("/-/allow-debug"),
"label": "Debug allow rules",
},
{"href": datasette.urls.path("/-/threads"), "label": "Debug threads"},
{"href": datasette.urls.path("/-/actor"), "label": "Debug actor"},
{"href": datasette.urls.path("/-/patterns"), "label": "Pattern portfolio"},
]

View file

@ -97,3 +97,8 @@ def register_magic_parameters(datasette):
@hookspec
def forbidden(datasette, request, message):
"Custom response for a 403 forbidden error"
@hookspec
def menu_links(datasette, actor):
"Links for the navigation menu"

View file

@ -13,6 +13,7 @@ DEFAULT_PLUGINS = (
"datasette.default_permissions",
"datasette.default_magic_parameters",
"datasette.blob_renderer",
"datasette.default_menu_links",
)
pm = pluggy.PluginManager("datasette")

View file

@ -261,13 +261,13 @@ footer p {
header .crumbs {
float: left;
}
header .logout {
header .actor {
float: right;
text-align: right;
padding-left: 1rem;
}
header .logout form {
display: inline;
padding-right: 1rem;
position: relative;
top: -3px;
}
footer a:link,
@ -312,6 +312,29 @@ footer {
margin-top: 1rem;
}
/* Navigation menu */
details.nav-menu > summary {
list-style: none;
display: inline;
float: right;
position: relative;
}
details.nav-menu > summary::-webkit-details-marker {
display: none;
}
details .nav-menu-inner {
position: absolute;
top: 2rem;
right: 10px;
width: 180px;
background-color: #276890;
padding: 1rem;
z-index: 1000;
}
.nav-menu-inner a {
display: block;
}
/* Components ============================================================== */

View file

@ -13,15 +13,33 @@
{% block extra_head %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
<header><nav>{% block nav %}
{% set links = menu_links() %}{% if links or show_logout %}
<details class="nav-menu">
<summary><svg aria-labelledby="nav-menu-svg-title" role="img"
fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" width="16" height="16">
<title id="nav-menu-svg-title">Menu</title>
<path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path>
</svg></summary>
<div class="nav-menu-inner">
{% if links %}
<ul>
{% for link in links %}
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if show_logout %}
<form action="{{ urls.logout() }}" method="post">
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
<button class="button-as-link">Log out</button>
</form>{% endif %}
</div>
</details>{% endif %}
{% if actor %}
<div class="logout">
<strong>{{ display_actor(actor) }}</strong>{% if show_logout %} &middot;
<form action="{{ urls.logout() }}" method="post">
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
<button class="button-as-link">Log out</button>
</form>{% endif %}
<div class="actor">
<strong>{{ display_actor(actor) }}</strong>
</div>
{% endif %}
{% endblock %}</nav></header>
@ -41,6 +59,22 @@
<footer class="ft">{% block footer %}{% include "_footer.html" %}{% endblock %}</footer>
<script>
var menuDetails = document.querySelector('.nav-menu');
document.body.addEventListener('click', (ev) => {
/* was this click outside the menu? */
if (menuDetails.getAttribute('open') !== "") {
return;
}
var target = ev.target;
while (target && target != menuDetails) {
target = target.parentNode;
}
if (!target) {
menuDetails.removeAttribute('open');
}
});
</script>
{% for body_script in body_scripts %}
<script>{{ body_script }}</script>
{% endfor %}