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},
]