diff --git a/datasette/static/app.css b/datasette/static/app.css index 5453a3d4de..22e3637060 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -840,6 +840,13 @@ svg.dropdown-menu-icon { .dropdown-menu a:hover { background-color: #eee; } +.dropdown-menu .dropdown-description { + margin: 0; + color: #666; + font-size: 0.8em; + max-width: 80vw; + white-space: normal; +} .dropdown-menu .hook { display: block; position: absolute; diff --git a/datasette/templates/database.html b/datasette/templates/database.html index 6c0cebcdbd..02e6fb3d24 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -31,7 +31,11 @@

{{ metadata.title or database }}{% if private %} 🔒{% endif %}

{% if links %} {% endif %} diff --git a/datasette/templates/query.html b/datasette/templates/query.html index b599177294..09a29118b9 100644 --- a/datasette/templates/query.html +++ b/datasette/templates/query.html @@ -47,7 +47,11 @@

{{ {% if links %} {% endif %} diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 0c2be67250..1d328366dc 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -42,7 +42,11 @@

{{ metadata.get("title") or table }}{% if is_view %} (view){% endif %}{% if {% if links %} {% endif %} diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index ecc8f0580d..4f77d75c5c 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -1493,7 +1493,7 @@ table_actions(datasette, actor, database, table, request) ``request`` - :ref:`internals_request` or None The current HTTP request. This can be ``None`` if the request object is not available. -This hook allows table actions to be displayed in a menu accessed via an action icon at the top of the table page. It should return a list of ``{"href": "...", "label": "..."}`` menu items. +This hook allows table actions to be displayed in a menu accessed via an action icon at the top of the table page. It should return a list of ``{"href": "...", "label": "..."}`` menu items, with optional ``"description": "..."`` keys describing each action in more detail. It can alternatively return an ``async def`` awaitable function which returns a list of menu items. @@ -1515,6 +1515,7 @@ This example adds a new table action if the signed in user is ``"root"``: ) ), "label": "Edit schema for this table", + "description": "Add, remove, rename or alter columns for this table.", } ] @@ -1571,6 +1572,7 @@ This example adds a new query action linking to a page for explaining a query: } ), "label": "Explain this query", + "description": "Get a summary of how SQLite executes the query", }, ] diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index 0132421313..5f0005373e 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -406,6 +406,7 @@ def query_actions(datasette, database, query_name, sql): } ), "label": "Explain this query", + "description": "Runs a SQLite explain", }, ] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d1da16fae8..9f69e4fad5 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -925,26 +925,36 @@ def get_menu_links(html): @pytest.mark.asyncio @pytest.mark.parametrize("table_or_view", ["facetable", "simple_view"]) async def test_hook_table_actions(ds_client, table_or_view): - 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(f"/fixtures/{table_or_view}") - assert get_table_actions_links(response.text) == [] + assert get_actions_links(response.text) == [] response_2 = await ds_client.get(f"/fixtures/{table_or_view}?_bot=1&_hello=BOB") assert sorted( - get_table_actions_links(response_2.text), key=lambda link: link["label"] + get_actions_links(response_2.text), key=lambda link: link["label"] ) == [ - {"label": "Database: fixtures", "href": "/"}, - {"label": "From async BOB", "href": "/"}, - {"label": f"Table: {table_or_view}", "href": "/"}, + {"label": "Database: fixtures", "href": "/", "description": None}, + {"label": "From async BOB", "href": "/", "description": None}, + {"label": f"Table: {table_or_view}", "href": "/", "description": None}, ] +def get_actions_links(html): + soup = Soup(html, "html.parser") + details = soup.find("details", {"class": "actions-menu-links"}) + if details is None: + return [] + links = [] + for a_el in details.select("a"): + description = None + if a_el.find("p") is not None: + description = a_el.find("p").text.strip() + a_el.find("p").extract() + label = a_el.text.strip() + href = a_el["href"] + links.append({"label": label, "href": href, "description": description}) + return links + + @pytest.mark.asyncio @pytest.mark.parametrize( "path,expected_url", @@ -959,37 +969,29 @@ def get_table_actions_links(html): ), ) 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) + links = get_actions_links(response.text) if expected_url is None: assert links == [] else: - assert links == [{"label": "Explain this query", "href": expected_url}] + assert links == [ + { + "label": "Explain this query", + "href": expected_url, + "description": "Runs a SQLite explain", + } + ] @pytest.mark.asyncio async def test_hook_database_actions(ds_client): - 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("/fixtures") - assert get_table_actions_links(response.text) == [] + assert get_actions_links(response.text) == [] response_2 = await ds_client.get("/fixtures?_bot=1&_hello=BOB") - assert get_table_actions_links(response_2.text) == [ - {"label": "Database: fixtures - BOB", "href": "/"}, + assert get_actions_links(response_2.text) == [ + {"label": "Database: fixtures - BOB", "href": "/", "description": None}, ]