Skip to content

Commit

Permalink
query_actions plugin hook
Browse files Browse the repository at this point in the history
* New query_actions plugin hook, closes #2283
  • Loading branch information
simonw authored Feb 28, 2024
1 parent f99c2f5 commit 6ec0081
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 0 deletions.
5 changes: 5 additions & 0 deletions datasette/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
27 changes: 27 additions & 0 deletions datasette/templates/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down
23 changes: 23 additions & 0 deletions datasette/views/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import sqlite_utils
import textwrap
from typing import List

from datasette.events import AlterTableEvent, CreateTableEvent, InsertRowsEvent
from datasette.database import QueryInterrupted
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -694,6 +700,22 @@ async def fetch_data_for_csv(request, _next=None):
)
)

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,
Expand Down Expand Up @@ -749,6 +771,7 @@ async def fetch_data_for_csv(request, _next=None):
database=database,
query_name=canned_query["name"] if canned_query else None,
),
query_actions=query_actions,
),
request=request,
view_name="database",
Expand Down
52 changes: 52 additions & 0 deletions docs/plugin_hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"permission_allowed",
"prepare_connection",
"prepare_jinja2_environment",
"query_actions",
"register_facet_classes",
"register_magic_parameters",
"register_permissions",
Expand Down
18 changes: 18 additions & 0 deletions tests/plugins/my_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import base64
import pint
import json
import urllib

ureg = pint.UnitRegistry()

Expand Down Expand Up @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,31 @@ def get_table_actions_links(html):
]


@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):
Expand Down

0 comments on commit 6ec0081

Please sign in to comment.