kopia lustrzana https://github.com/simonw/datasette
rodzic
f99c2f5f8c
commit
6ec0081f5d
|
@ -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"""
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -46,6 +46,7 @@ EXPECTED_PLUGINS = [
|
|||
"permission_allowed",
|
||||
"prepare_connection",
|
||||
"prepare_jinja2_environment",
|
||||
"query_actions",
|
||||
"register_facet_classes",
|
||||
"register_magic_parameters",
|
||||
"register_permissions",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
Ładowanie…
Reference in New Issue