diff --git a/datasette/app.py b/datasette/app.py index 3016043a..fb5c34a4 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -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, diff --git a/datasette/default_menu_links.py b/datasette/default_menu_links.py new file mode 100644 index 00000000..11374fb5 --- /dev/null +++ b/datasette/default_menu_links.py @@ -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"}, + ] diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index f7e90e4e..7bad262a 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -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" diff --git a/datasette/plugins.py b/datasette/plugins.py index 1c2f392f..50791988 100644 --- a/datasette/plugins.py +++ b/datasette/plugins.py @@ -13,6 +13,7 @@ DEFAULT_PLUGINS = ( "datasette.default_permissions", "datasette.default_magic_parameters", "datasette.blob_renderer", + "datasette.default_menu_links", ) pm = pluggy.PluginManager("datasette") diff --git a/datasette/static/app.css b/datasette/static/app.css index 8b462b35..2fd5371b 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -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 ============================================================== */ diff --git a/datasette/templates/base.html b/datasette/templates/base.html index 03de2115..ec1fd00e 100644 --- a/datasette/templates/base.html +++ b/datasette/templates/base.html @@ -13,15 +13,33 @@ {% block extra_head %}{% endblock %}
-