`query_actions` plugin hook

* New query_actions plugin hook, closes #2283
pull/2295/head
Simon Willison 2024-02-27 21:55:16 -08:00 zatwierdzone przez GitHub
rodzic f99c2f5f8c
commit 6ec0081f5d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
7 zmienionych plików z 151 dodań i 0 usunięć

Wyświetl plik

@ -145,6 +145,11 @@ def table_actions(datasette, actor, database, table, request):
"""Links for the table actions menu"""
@hookspec
def query_actions(datasette, actor, database, query_name, request, sql, params):
"""Links for the query and canned query actions menu"""
@hookspec
def database_actions(datasette, actor, database, request):
"""Links for the database actions menu"""

Wyświetl plik

@ -29,6 +29,33 @@
{% endif %}
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ metadata.title or database }}{% if canned_query and not metadata.title %}: {{ canned_query }}{% endif %}{% if private %} 🔒{% endif %}</h1>
{% set links = query_actions() %}{% if links %}
<div class="page-action-menu">
<details class="actions-menu-links details-menu">
<summary>
<div class="icon-text">
<svg class="icon" aria-labelledby="actions-menu-links-title" role="img" style="color: #fff" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<title id="actions-menu-links-title">Query actions</title>
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<span>Query actions</span>
</div>
</summary>
<div class="dropdown-menu">
<div class="hook"></div>
{% if links %}
<ul>
{% for link in links %}
<li><a href="{{ link.href }}">{{ link.label }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
</details>
</div>
{% endif %}
{% if canned_query %}{{ top_canned_query() }}{% else %}{{ top_query() }}{% endif %}

Wyświetl plik

@ -9,6 +9,7 @@ import os
import re
import sqlite_utils
import textwrap
from typing import List
from datasette.events import AlterTableEvent, CreateTableEvent, InsertRowsEvent
from datasette.database import QueryInterrupted
@ -256,6 +257,11 @@ class QueryContext:
top_canned_query: callable = field(
metadata={"help": "Callable to render the top_canned_query slot"}
)
query_actions: callable = field(
metadata={
"help": "Callable returning a list of links for the query action menu"
}
)
async def get_tables(datasette, request, db):
@ -694,6 +700,22 @@ class QueryView(View):
)
)
async def query_actions():
query_actions = []
for hook in pm.hook.query_actions(
datasette=datasette,
actor=request.actor,
database=database,
query_name=canned_query["name"] if canned_query else None,
request=request,
sql=sql,
params=params,
):
extra_links = await await_me_maybe(hook)
if extra_links:
query_actions.extend(extra_links)
return query_actions
r = Response.html(
await datasette.render_template(
template,
@ -749,6 +771,7 @@ class QueryView(View):
database=database,
query_name=canned_query["name"] if canned_query else None,
),
query_actions=query_actions,
),
request=request,
view_name="database",

Wyświetl plik

@ -1520,6 +1520,58 @@ This example adds a new table action if the signed in user is ``"root"``:
Example: `datasette-graphql <https://datasette.io/plugins/datasette-graphql>`_
.. _plugin_hook_query_actions:
query_actions(datasette, actor, database, query_name, request, sql, params)
---------------------------------------------------------------------------
``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries.
``actor`` - dictionary or None
The currently authenticated :ref:`actor <authentication_actor>`.
``database`` - string
The name of the database.
``query_name`` - string or None
The name of the canned query, or ``None`` if this is an arbitrary SQL query.
``request`` - :ref:`internals_request`
The current HTTP request.
``sql`` - string
The SQL query being executed
``params`` - dictionary
The parameters passed to the SQL query, if any.
This hook is similar to :ref:`plugin_hook_table_actions` but populates an actions menu on the canned query and arbitrary SQL query pages.
This example adds a new query action linking to a page for explaining a query:
.. code-block:: python
from datasette import hookimpl
import urllib
@hookimpl
def query_actions(datasette, database, sql):
return [
{
"href": datasette.urls.database(database)
+ "/-/explain?"
+ urllib.parse.urlencode(
{
"sql": sql,
}
),
"label": "Explain this query",
},
]
.. _plugin_hook_database_actions:
database_actions(datasette, actor, database, request)

Wyświetl plik

@ -46,6 +46,7 @@ EXPECTED_PLUGINS = [
"permission_allowed",
"prepare_connection",
"prepare_jinja2_environment",
"query_actions",
"register_facet_classes",
"register_magic_parameters",
"register_permissions",

Wyświetl plik

@ -7,6 +7,7 @@ from datasette.utils.asgi import asgi_send_json, Response
import base64
import pint
import json
import urllib
ureg = pint.UnitRegistry()
@ -390,6 +391,23 @@ def table_actions(datasette, database, table, actor):
]
@hookimpl
def query_actions(datasette, database, query_name, sql):
args = {
"sql": sql,
}
if query_name:
args["query_name"] = query_name
return [
{
"href": datasette.urls.database(database)
+ "/-/explain?"
+ urllib.parse.urlencode(args),
"label": "Explain this query",
},
]
@hookimpl
def database_actions(datasette, database, actor, request):
if actor:

Wyświetl plik

@ -945,6 +945,31 @@ async def test_hook_table_actions(ds_client, table_or_view):
]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"path,expected_url",
(
("/fixtures?sql=select+1", "/fixtures/-/explain?sql=select+1"),
(
"/fixtures/pragma_cache_size",
"/fixtures/-/explain?sql=PRAGMA+cache_size%3B&query_name=pragma_cache_size",
),
),
)
async def test_hook_query_actions(ds_client, path, expected_url):
def get_table_actions_links(html):
soup = Soup(html, "html.parser")
details = soup.find("details", {"class": "actions-menu-links"})
if details is None:
return []
return [{"label": a.text, "href": a["href"]} for a in details.select("a")]
response = await ds_client.get(path)
assert response.status_code == 200
links = get_table_actions_links(response.text)
assert links == [{"label": "Explain this query", "href": expected_url}]
@pytest.mark.asyncio
async def test_hook_database_actions(ds_client):
def get_table_actions_links(html):