mirror of
https://github.com/simonw/datasette.git
synced 2026-05-27 20:36:17 +02:00
Improvements to Jump SQL columns
- Removed database_name and resource_name - url can now optionally return JSON to reuse datasette.urls. methods - description is now used as a truncated text description
This commit is contained in:
parent
0f7e4410c1
commit
21a79b34b8
10 changed files with 203 additions and 69 deletions
|
|
@ -2,16 +2,56 @@ from datasette import hookimpl
|
|||
from datasette.jump import JumpSQL
|
||||
|
||||
DEBUG_MENU_ITEMS = (
|
||||
("/-/databases", "Databases"),
|
||||
("/-/plugins", "Installed plugins"),
|
||||
("/-/versions", "Version info"),
|
||||
("/-/settings", "Settings"),
|
||||
("/-/permissions", "Debug permissions"),
|
||||
("/-/messages", "Debug messages"),
|
||||
("/-/allow-debug", "Debug allow rules"),
|
||||
("/-/threads", "Debug threads"),
|
||||
("/-/actor", "Debug actor"),
|
||||
("/-/patterns", "Pattern portfolio"),
|
||||
(
|
||||
"/-/databases",
|
||||
"Databases",
|
||||
"List of databases known to this Datasette instance.",
|
||||
),
|
||||
(
|
||||
"/-/plugins",
|
||||
"Installed plugins",
|
||||
"Review loaded plugins, their versions and their registered hooks.",
|
||||
),
|
||||
(
|
||||
"/-/versions",
|
||||
"Version info",
|
||||
"Check the Python, SQLite and dependency versions used by this server.",
|
||||
),
|
||||
(
|
||||
"/-/settings",
|
||||
"Settings",
|
||||
"Inspect the active Datasette settings and configuration values.",
|
||||
),
|
||||
(
|
||||
"/-/permissions",
|
||||
"Debug permissions",
|
||||
"Test permission checks for actors, actions and resources.",
|
||||
),
|
||||
(
|
||||
"/-/messages",
|
||||
"Debug messages",
|
||||
"Try out temporary flash messages shown to users.",
|
||||
),
|
||||
(
|
||||
"/-/allow-debug",
|
||||
"Debug allow rules",
|
||||
"Explore how allow blocks match actors against permission rules.",
|
||||
),
|
||||
(
|
||||
"/-/threads",
|
||||
"Debug threads",
|
||||
"Inspect worker threads and database tasks.",
|
||||
),
|
||||
(
|
||||
"/-/actor",
|
||||
"Debug actor",
|
||||
"View the actor object for the current signed-in user.",
|
||||
),
|
||||
(
|
||||
"/-/patterns",
|
||||
"Pattern portfolio",
|
||||
"Browse Datasette UI patterns.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -25,12 +65,13 @@ def jump_items_sql(datasette, actor, request):
|
|||
JumpSQL.menu_item(
|
||||
label=label,
|
||||
url=datasette.urls.path(path),
|
||||
description="Debug menu",
|
||||
description=description,
|
||||
source="datasette.default_debug_menu",
|
||||
sort_key=70 + index,
|
||||
search_text=f"debug {label} {description}",
|
||||
item_type="debug",
|
||||
)
|
||||
for index, (path, label) in enumerate(DEBUG_MENU_ITEMS)
|
||||
for index, (path, label, description) in enumerate(DEBUG_MENU_ITEMS)
|
||||
]
|
||||
|
||||
return inner
|
||||
|
|
|
|||
|
|
@ -23,10 +23,11 @@ def jump_items_sql(datasette, actor, request):
|
|||
SELECT
|
||||
'database' AS type,
|
||||
parent AS label,
|
||||
'Database' AS description,
|
||||
NULL AS url,
|
||||
parent AS database_name,
|
||||
NULL AS resource_name,
|
||||
NULL AS description,
|
||||
json_object(
|
||||
'method', 'database',
|
||||
'database', parent
|
||||
) AS url,
|
||||
parent AS search_text,
|
||||
10 AS sort_key,
|
||||
'datasette' AS source,
|
||||
|
|
@ -43,10 +44,12 @@ def jump_items_sql(datasette, actor, request):
|
|||
SELECT
|
||||
CASE WHEN catalog_views.view_name IS NULL THEN 'table' ELSE 'view' END AS type,
|
||||
allowed_tables.parent || ': ' || allowed_tables.child AS label,
|
||||
CASE WHEN catalog_views.view_name IS NULL THEN 'Table' ELSE 'View' END AS description,
|
||||
NULL AS url,
|
||||
allowed_tables.parent AS database_name,
|
||||
allowed_tables.child AS resource_name,
|
||||
NULL AS description,
|
||||
json_object(
|
||||
'method', 'table',
|
||||
'database', allowed_tables.parent,
|
||||
'table', allowed_tables.child
|
||||
) AS url,
|
||||
allowed_tables.parent || ' ' || allowed_tables.child AS search_text,
|
||||
CASE WHEN catalog_views.view_name IS NULL THEN 20 ELSE 25 END AS sort_key,
|
||||
'datasette' AS source,
|
||||
|
|
@ -66,10 +69,12 @@ def jump_items_sql(datasette, actor, request):
|
|||
SELECT
|
||||
'query' AS type,
|
||||
allowed_queries.parent || ': ' || allowed_queries.child AS label,
|
||||
'Canned query' AS description,
|
||||
NULL AS url,
|
||||
allowed_queries.parent AS database_name,
|
||||
allowed_queries.child AS resource_name,
|
||||
NULL AS description,
|
||||
json_object(
|
||||
'method', 'query',
|
||||
'database', allowed_queries.parent,
|
||||
'query', allowed_queries.child
|
||||
) AS url,
|
||||
allowed_queries.parent || ' ' || allowed_queries.child AS search_text,
|
||||
30 AS sort_key,
|
||||
'datasette' AS source,
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ class JumpSQL:
|
|||
:label AS label,
|
||||
:description AS description,
|
||||
:url AS url,
|
||||
NULL AS database_name,
|
||||
NULL AS resource_name,
|
||||
:search_text AS search_text,
|
||||
:sort_key AS sort_key,
|
||||
:source AS source,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,11 @@ class NavigationSearch extends HTMLElement {
|
|||
background-color: #dbeafe;
|
||||
}
|
||||
|
||||
.result-item > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.jump-start-content {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
margin-bottom: 0.5rem;
|
||||
|
|
@ -120,7 +125,7 @@ class NavigationSearch extends HTMLElement {
|
|||
color: #4b5563;
|
||||
}
|
||||
|
||||
.result-description {
|
||||
.result-type {
|
||||
color: #4b5563;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
|
|
@ -132,6 +137,17 @@ class NavigationSearch extends HTMLElement {
|
|||
color: #6b7280;
|
||||
}
|
||||
|
||||
.result-description {
|
||||
color: #374151;
|
||||
display: -webkit-box;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.35;
|
||||
margin-top: 0.35rem;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.results-heading {
|
||||
color: #4b5563;
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -500,6 +516,9 @@ class NavigationSearch extends HTMLElement {
|
|||
match.display_name && match.display_name !== match.name
|
||||
? `<div class="result-label">${this.escapeHtml(match.name)}</div>`
|
||||
: "";
|
||||
const type = match.type
|
||||
? `<div class="result-type">${this.escapeHtml(match.type)}</div>`
|
||||
: "";
|
||||
const description = match.description
|
||||
? `<div class="result-description">${this.escapeHtml(
|
||||
match.description,
|
||||
|
|
@ -513,10 +532,11 @@ class NavigationSearch extends HTMLElement {
|
|||
aria-selected="${index === this.selectedIndex}"
|
||||
>
|
||||
<div>
|
||||
${description}
|
||||
${type}
|
||||
<div class="result-name">${this.escapeHtml(displayName)}</div>
|
||||
${label}
|
||||
<div class="result-url">${this.escapeHtml(match.url)}</div>
|
||||
${description}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -944,16 +944,33 @@ class JumpView(BaseView):
|
|||
raise TypeError("jump_items_sql must return JumpSQL instances")
|
||||
return fragments
|
||||
|
||||
def _url_for_row(self, row):
|
||||
if row["url"]:
|
||||
return row["url"]
|
||||
if row["type"] == "database":
|
||||
return self.ds.urls.database(row["database_name"])
|
||||
if row["type"] in ("table", "view"):
|
||||
return self.ds.urls.table(row["database_name"], row["resource_name"])
|
||||
if row["type"] == "query":
|
||||
return self.ds.urls.query(row["database_name"], row["resource_name"])
|
||||
return ""
|
||||
def _resolve_url(self, url):
|
||||
if not url or url.startswith("/"):
|
||||
return url
|
||||
|
||||
descriptor = json.loads(url)
|
||||
if not isinstance(descriptor, dict):
|
||||
raise TypeError("jump item url JSON must be an object")
|
||||
method_name = descriptor.get("method")
|
||||
if not isinstance(method_name, str) or not method_name:
|
||||
raise TypeError("jump item url JSON must include a method")
|
||||
if method_name.startswith("_"):
|
||||
raise AttributeError(f"datasette.urls has no method named {method_name!r}")
|
||||
try:
|
||||
method = getattr(self.ds.urls, method_name)
|
||||
except AttributeError as ex:
|
||||
raise AttributeError(
|
||||
f"datasette.urls has no method named {method_name!r}"
|
||||
) from ex
|
||||
if not callable(method):
|
||||
raise TypeError(f"datasette.urls.{method_name} is not callable")
|
||||
kwargs = {key: value for key, value in descriptor.items() if key != "method"}
|
||||
try:
|
||||
return method(**kwargs)
|
||||
except TypeError as ex:
|
||||
raise TypeError(
|
||||
f"Invalid arguments for datasette.urls.{method_name}(): {ex}"
|
||||
) from ex
|
||||
|
||||
async def get(self, request):
|
||||
q = request.args.get("q", "").strip()
|
||||
|
|
@ -975,8 +992,6 @@ class JumpView(BaseView):
|
|||
label,
|
||||
description,
|
||||
url,
|
||||
database_name,
|
||||
resource_name,
|
||||
search_text,
|
||||
sort_key,
|
||||
source,
|
||||
|
|
@ -1016,7 +1031,7 @@ class JumpView(BaseView):
|
|||
for row in rows:
|
||||
match = {
|
||||
"name": row["label"],
|
||||
"url": self._url_for_row(row),
|
||||
"url": self._resolve_url(row["url"]),
|
||||
"type": row["type"],
|
||||
"description": row["description"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ Unreleased
|
|||
- Fixed a Safari bug with the table search mechanism triggered by pressing ``/``. (:issue:`2724`)
|
||||
- New "Jump to..." menu item, always visible, for triggering the previously undocumented ``/`` menu. (:issue:`2725`)
|
||||
- The ``/`` jump-to search interface now covers databases, views, canned queries and plugin-provided items in addition to tables. The endpoint backing it has been renamed from ``/-/tables`` to ``/-/jump``.
|
||||
- New :ref:`plugin_hook_jump_items_sql` plugin hook, allowing plugins to contribute additional items to the jump-to menu by returning SQL that queries the internal catalog.
|
||||
- New :ref:`plugin_hook_jump_items_sql` plugin hook, allowing plugins to contribute additional items to the jump-to menu by returning SQL that queries the internal catalog. Each row returned by this hook includes a ``url`` value, which can be a string starting with ``/`` or a JSON object describing a call to one of the :ref:`internals_datasette_urls` methods.
|
||||
- ``datasette.jump.JumpSQL.menu_item()`` is a shortcut for adding individual jump menu items that are not backed by resources in the internal catalog.
|
||||
- New :ref:`javascript_plugins_makeJumpSections` JavaScript plugin hook, allowing plugins to add custom blank-state sections to the jump-to menu before the user has typed a query.
|
||||
- Debug menu links now appear in the jump-to menu instead of the top-right app menu.
|
||||
- Jump menu results now show their ``type`` as a category label, and can show optional longer ``description`` text for individual results.
|
||||
- Debug menu links now appear in the jump-to menu instead of the top-right app menu, with descriptions for each debug item.
|
||||
- New documented :ref:`datasette.fixtures.populate_fixture_database(conn) <datasette_fixtures_populate_fixture_database>` helper for creating the fixture database tables used by Datasette's own tests, intended for plugin test suites.
|
||||
|
||||
.. _v1_0_a29:
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ Shows currently attached databases. `Databases example <https://latest.datasette
|
|||
|
||||
Returns a JSON list of items that the current actor has permission to view for Datasette's jump menu. By default this includes visible databases, tables, views and canned queries, and plugins can contribute additional items.
|
||||
|
||||
Each item includes a ``type`` string used as a category label in the menu. Items can also include an optional ``description`` with longer text describing that individual result.
|
||||
|
||||
The endpoint supports a ``?q=`` query parameter for filtering items by name.
|
||||
|
||||
`Jump example <https://latest.datasette.io/-/jump>`_:
|
||||
|
|
@ -163,19 +165,19 @@ The endpoint supports a ``?q=`` query parameter for filtering items by name.
|
|||
"name": "fixtures",
|
||||
"url": "/fixtures",
|
||||
"type": "database",
|
||||
"description": "Database"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"name": "fixtures: facetable",
|
||||
"url": "/fixtures/facetable",
|
||||
"type": "table",
|
||||
"description": "Table"
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"name": "fixtures: recent_releases",
|
||||
"url": "/fixtures/recent_releases",
|
||||
"type": "query",
|
||||
"description": "Canned query"
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"truncated": false
|
||||
|
|
@ -191,7 +193,7 @@ Search example with ``?q=facet`` returns only items matching ``.*facet.*``:
|
|||
"name": "fixtures: facetable",
|
||||
"url": "/fixtures/facetable",
|
||||
"type": "table",
|
||||
"description": "Table"
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"truncated": false
|
||||
|
|
|
|||
|
|
@ -1902,22 +1902,16 @@ Return a ``datasette.jump.JumpSQL`` object, or a list of ``JumpSQL`` objects. Ea
|
|||
The SQL query must return these columns:
|
||||
|
||||
``type``
|
||||
A short type string for the result, for example ``"app"`` or ``"dashboard"``.
|
||||
A short type string for the result, for example ``"app"`` or ``"dashboard"``. The jump menu displays this above the item as a category label.
|
||||
|
||||
``label``
|
||||
The stable name for the result. This is returned as ``name`` in the JSON API and is used for sorting.
|
||||
|
||||
``description``
|
||||
A short description shown above the item in the jump menu.
|
||||
Optional longer text describing this individual item, or ``NULL``. The jump menu displays this below the item's URL when it is present.
|
||||
|
||||
``url``
|
||||
The URL to navigate to when the item is selected.
|
||||
|
||||
``database_name``
|
||||
The database name for Datasette resources, or ``NULL`` for custom plugin results.
|
||||
|
||||
``resource_name``
|
||||
The table, view or query name for Datasette resources, or ``NULL`` for custom plugin results.
|
||||
The URL to navigate to when the item is selected. This can be either a string starting with ``/`` or a JSON object describing a call to one of the :ref:`internals_datasette_urls` methods. For example, ``json_object('method', 'table', 'database', 'fixtures', 'table', 'facetable')`` calls ``datasette.urls.table(database='fixtures', table='facetable')``. Unknown methods or invalid named arguments will result in an error.
|
||||
|
||||
``search_text``
|
||||
Text that should be searched by the ``?q=`` parameter.
|
||||
|
|
@ -1946,7 +1940,7 @@ This example adds a "Plugin dashboard" result for signed-in users:
|
|||
return JumpSQL.menu_item(
|
||||
item_type="dashboard",
|
||||
label="plugin-dashboard",
|
||||
description="Dashboard",
|
||||
description="Review plugin status and configuration.",
|
||||
url="/-/plugin-dashboard",
|
||||
search_text="plugin dashboard",
|
||||
sort_key=80,
|
||||
|
|
@ -1956,7 +1950,7 @@ This example adds a "Plugin dashboard" result for signed-in users:
|
|||
|
||||
Use ``params=`` to pass SQL parameters. Datasette will automatically namespace those parameters before combining SQL fragments from different plugins.
|
||||
|
||||
``JumpSQL.menu_item(...)`` is a shortcut for adding a single jump menu item that is not backed by a resource in Datasette's internal catalog tables. It returns ``NULL`` for ``database_name`` and ``resource_name`` and accepts the keyword arguments shown above.
|
||||
``JumpSQL.menu_item(...)`` is a shortcut for adding a single jump menu item from Python code. It accepts the keyword arguments shown above.
|
||||
|
||||
.. _plugin_actions:
|
||||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,15 @@ If you run ``datasette plugins --all`` it will include default plugins that ship
|
|||
"register_column_types"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "datasette.default_debug_menu",
|
||||
"static": false,
|
||||
"templates": false,
|
||||
"version": null,
|
||||
"hooks": [
|
||||
"jump_items_sql"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "datasette.default_jump_items",
|
||||
"static": false,
|
||||
|
|
@ -234,15 +243,6 @@ If you run ``datasette plugins --all`` it will include default plugins that ship
|
|||
"register_magic_parameters"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "datasette.default_debug_menu",
|
||||
"static": false,
|
||||
"templates": false,
|
||||
"version": null,
|
||||
"hooks": [
|
||||
"jump_items_sql"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "datasette.default_permissions",
|
||||
"static": false,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from datasette import hookimpl
|
|||
from datasette.app import Datasette
|
||||
from datasette.jump import JumpSQL
|
||||
from datasette.plugins import pm
|
||||
from datasette.views.special import JumpView
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
|
|
@ -113,7 +114,7 @@ async def test_jump_uses_canned_query_names_not_titles(ds_for_jump):
|
|||
"name": "content: release_notes",
|
||||
"url": "/content/release_notes",
|
||||
"type": "query",
|
||||
"description": "Canned query",
|
||||
"description": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -160,8 +161,6 @@ async def test_jump_sql_menu_item_helper(ds_for_jump):
|
|||
"label": "Plugin dashboard",
|
||||
"description": "Plugin tool",
|
||||
"url": "/-/plugin-dashboard",
|
||||
"database_name": None,
|
||||
"resource_name": None,
|
||||
"search_text": "dashboard plugin",
|
||||
"sort_key": 70,
|
||||
"source": "test-plugin",
|
||||
|
|
@ -196,7 +195,13 @@ async def test_debug_menu_items_are_in_jump_for_debug_menu_permission():
|
|||
"Debug actor": "/-/actor",
|
||||
"Pattern portfolio": "/-/patterns",
|
||||
}
|
||||
assert {match["description"] for match in debug_matches} == {"Debug menu"}
|
||||
descriptions_by_name = {
|
||||
match["name"]: match["description"] for match in debug_matches
|
||||
}
|
||||
assert all(descriptions_by_name.values())
|
||||
assert descriptions_by_name["Databases"] == (
|
||||
"Inspect the databases, tables, views and columns known to this Datasette instance."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -222,8 +227,6 @@ async def test_jump_uses_plugin_sql_with_namespaced_parameters(ds_for_jump):
|
|||
'plugin-dashboard: ' || :actor_id AS label,
|
||||
'Plugin supplied item' AS description,
|
||||
'/-/plugin-dashboard' AS url,
|
||||
NULL AS database_name,
|
||||
NULL AS resource_name,
|
||||
'plugin dashboard ' || :actor_id AS search_text,
|
||||
80 AS sort_key,
|
||||
'test-plugin' AS source,
|
||||
|
|
@ -256,6 +259,61 @@ async def test_jump_uses_plugin_sql_with_namespaced_parameters(ds_for_jump):
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_jump_resolves_url_descriptors_from_sql(ds_for_jump):
|
||||
class JumpPlugin:
|
||||
@hookimpl
|
||||
def jump_items_sql(self, datasette, actor, request):
|
||||
return JumpSQL(sql="""
|
||||
SELECT
|
||||
'plugin' AS type,
|
||||
'Table descriptor' AS label,
|
||||
NULL AS description,
|
||||
json_object(
|
||||
'method', 'table',
|
||||
'database', 'content',
|
||||
'table', 'comments'
|
||||
) AS url,
|
||||
'table descriptor comments' AS search_text,
|
||||
80 AS sort_key,
|
||||
'test-plugin' AS source,
|
||||
NULL AS display_name
|
||||
""")
|
||||
|
||||
plugin = JumpPlugin()
|
||||
pm.register(plugin, name="test-jump-url-descriptor-plugin")
|
||||
try:
|
||||
response = await ds_for_jump.client.get(
|
||||
"/-/jump.json?q=descriptor", actor={"id": "alice"}
|
||||
)
|
||||
finally:
|
||||
pm.unregister(name="test-jump-url-descriptor-plugin")
|
||||
|
||||
assert response.status_code == 200
|
||||
plugin_matches = [
|
||||
match for match in response.json()["matches"] if match["type"] == "plugin"
|
||||
]
|
||||
assert plugin_matches == [
|
||||
{
|
||||
"name": "Table descriptor",
|
||||
"url": "/content/comments",
|
||||
"type": "plugin",
|
||||
"description": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_jump_url_descriptor_errors(ds_for_jump):
|
||||
view = JumpView(ds_for_jump)
|
||||
with pytest.raises(AttributeError):
|
||||
view._resolve_url('{"method": "not_a_url_method"}')
|
||||
with pytest.raises(TypeError):
|
||||
view._resolve_url(
|
||||
'{"method": "table", "database_name": "content", "table_name": "comments"}'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tables_endpoint_removed(ds_for_jump):
|
||||
response = await ds_for_jump.client.get("/-/tables.json")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue