mirror of
https://github.com/simonw/datasette.git
synced 2026-06-01 06:37:02 +02:00
Big visual improvement to /-/queries pages
Including /db/-/queries Refs https://github.com/simonw/datasette/issues/2735#issuecomment-4536860239
This commit is contained in:
parent
4208ded249
commit
8ab8999ba9
3 changed files with 229 additions and 34 deletions
|
|
@ -2,6 +2,155 @@
|
|||
|
||||
{% block title %}{% if database %}{{ database }}: {% endif %}queries{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{- super() -}}
|
||||
<style>
|
||||
.query-list-page {
|
||||
max-width: 64rem;
|
||||
}
|
||||
.query-list-filters {
|
||||
margin: 0.5rem 0 1rem;
|
||||
}
|
||||
.query-list-search {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
.query-list-search label {
|
||||
width: auto;
|
||||
}
|
||||
.query-list-search input[type=search] {
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 18rem;
|
||||
max-width: 24rem;
|
||||
}
|
||||
.query-list-search button[type=submit] {
|
||||
font-size: 0.78rem;
|
||||
height: 2rem;
|
||||
line-height: 1.1;
|
||||
padding: 0.35rem 0.65rem;
|
||||
}
|
||||
.query-list-filter-groups {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem 1.4rem;
|
||||
}
|
||||
.query-list-filter-group {
|
||||
border: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.query-list-filter-group legend {
|
||||
font-weight: 700;
|
||||
margin: 0 0.45rem 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
.query-list-filter-group label {
|
||||
align-items: center;
|
||||
border: 1px solid #c8d1dc;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-size: 0.82rem;
|
||||
gap: 0.3rem;
|
||||
line-height: 1.1;
|
||||
padding: 0.35rem 0.55rem;
|
||||
}
|
||||
.query-list-filter-group input {
|
||||
margin: 0;
|
||||
}
|
||||
.query-list-filter-group input:checked + span {
|
||||
font-weight: 700;
|
||||
}
|
||||
.query-list-results {
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
margin: 0.25rem 0 1rem;
|
||||
min-width: 36rem;
|
||||
width: 100%;
|
||||
}
|
||||
.query-list-results th,
|
||||
.query-list-results td {
|
||||
border-bottom: 1px solid #d7dde5;
|
||||
padding: 0.45rem 0.7rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.query-list-results th {
|
||||
background-color: #edf6fb;
|
||||
border-top: 1px solid #d7dde5;
|
||||
color: #39445a;
|
||||
font-weight: 700;
|
||||
}
|
||||
.query-list-results tbody tr:nth-child(even) {
|
||||
background-color: rgba(39, 104, 144, 0.05);
|
||||
}
|
||||
.query-list-results a.query-list-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
.query-list-description {
|
||||
color: #4f5b6d;
|
||||
font-size: 0.78rem;
|
||||
margin: 0.15rem 0 0;
|
||||
}
|
||||
.query-list-pill {
|
||||
background-color: #eef1f5;
|
||||
border: 1px solid #d7dde5;
|
||||
border-radius: 0.25rem;
|
||||
color: #39445a;
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 0.25rem 0.4rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.query-list-pill-write {
|
||||
background-color: #fff4db;
|
||||
border-color: #e2b64e;
|
||||
}
|
||||
.query-list-pill-published {
|
||||
background-color: #e7f5ec;
|
||||
border-color: #9ecfab;
|
||||
color: #267a3e;
|
||||
}
|
||||
.query-list-pill-unpublished {
|
||||
background-color: #f7edf0;
|
||||
border-color: #dbb8c1;
|
||||
}
|
||||
.query-list-pagination a {
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 0.25rem;
|
||||
display: inline-block;
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
.query-list-pagination-bottom {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.query-list-search input[type=search] {
|
||||
max-width: none;
|
||||
}
|
||||
.query-list-filter-group {
|
||||
display: block;
|
||||
}
|
||||
.query-list-filter-group legend {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.query-list-filter-group label {
|
||||
margin: 0 0.25rem 0.35rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}query-list{% if database %} db-{{ database|to_css_class }}{% endif %}{% endblock %}
|
||||
|
||||
{% block crumbs %}
|
||||
|
|
@ -10,49 +159,66 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<h1>Queries</h1>
|
||||
<div class="query-list-page">
|
||||
|
||||
<form action="{{ query_list_path }}" method="get">
|
||||
<p>
|
||||
<h1 style="padding-left: 10px; border-left: 10px solid #{% if database_color %}{{ database_color }}{% else %}666{% endif %}">Queries</h1>
|
||||
|
||||
<form class="query-list-filters core" action="{{ query_list_path }}" method="get">
|
||||
<p class="query-list-search">
|
||||
<label for="query-search">Search</label>
|
||||
<input id="query-search" type="search" name="q" value="{{ filters.q }}">
|
||||
<button type="submit">Search</button>
|
||||
</p>
|
||||
<p>
|
||||
<label for="query-is-write">Mode</label>
|
||||
<select id="query-is-write" name="is_write">
|
||||
<option value=""{% if not filters.is_write %} selected{% endif %}>Any</option>
|
||||
<option value="0"{% if filters.is_write == "0" %} selected{% endif %}>Read-only</option>
|
||||
<option value="1"{% if filters.is_write == "1" %} selected{% endif %}>Writable</option>
|
||||
</select>
|
||||
<label for="query-is-published">Publication</label>
|
||||
<select id="query-is-published" name="is_published">
|
||||
<option value=""{% if not filters.is_published %} selected{% endif %}>Any</option>
|
||||
<option value="1"{% if filters.is_published == "1" %} selected{% endif %}>Published</option>
|
||||
<option value="0"{% if filters.is_published == "0" %} selected{% endif %}>Unpublished</option>
|
||||
</select>
|
||||
</p>
|
||||
<div class="query-list-filter-groups">
|
||||
<fieldset class="query-list-filter-group">
|
||||
<legend>Mode</legend>
|
||||
<label><input type="radio" name="is_write" value=""{% if not filters.is_write %} checked{% endif %}> <span>Any</span></label>
|
||||
<label><input type="radio" name="is_write" value="0"{% if filters.is_write == "0" %} checked{% endif %}> <span>Read-only</span></label>
|
||||
<label><input type="radio" name="is_write" value="1"{% if filters.is_write == "1" %} checked{% endif %}> <span>Writable</span></label>
|
||||
</fieldset>
|
||||
<fieldset class="query-list-filter-group">
|
||||
<legend>Publication</legend>
|
||||
<label><input type="radio" name="is_published" value=""{% if not filters.is_published %} checked{% endif %}> <span>Any</span></label>
|
||||
<label><input type="radio" name="is_published" value="1"{% if filters.is_published == "1" %} checked{% endif %}> <span>Published</span></label>
|
||||
<label><input type="radio" name="is_published" value="0"{% if filters.is_published == "0" %} checked{% endif %}> <span>Unpublished</span></label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if queries %}
|
||||
<ul class="bullets">
|
||||
{% for query in queries %}
|
||||
<li>
|
||||
{% if show_database %}
|
||||
<a href="{{ urls.database(query.database) }}">{{ query.database }}</a>:
|
||||
{% endif %}
|
||||
<a href="{{ urls.query(query.database, query.name) }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a>{% if query.private %} 🔒{% endif %}
|
||||
{% if query.is_write %}<span>Writable</span>{% endif %}
|
||||
{% if query.is_published %}<span>Published</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="table-wrapper"><table class="query-list-results">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if show_database %}<th scope="col">Database</th>{% endif %}
|
||||
<th scope="col">Query</th>
|
||||
<th scope="col">Mode</th>
|
||||
<th scope="col">Publication</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for query in queries %}
|
||||
<tr>
|
||||
{% if show_database %}
|
||||
<td><a class="query-list-database" href="{{ urls.database(query.database) }}">{{ query.database }}</a></td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<a class="query-list-title" href="{{ urls.query(query.database, query.name) }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a>{% if query.private %} 🔒{% endif %}
|
||||
{% if query.description %}<p class="query-list-description">{{ query.description }}</p>{% endif %}
|
||||
</td>
|
||||
<td>{% if query.is_write %}<span class="query-list-pill query-list-pill-write">Writable</span>{% else %}<span class="query-list-pill">Read-only</span>{% endif %}</td>
|
||||
<td>{% if query.is_published %}<span class="query-list-pill query-list-pill-published">Published</span>{% else %}<span class="query-list-pill query-list-pill-unpublished">Unpublished</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}
|
||||
<p>No queries found.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if next_url %}
|
||||
<p><a href="{{ next_url }}">Next page</a></p>
|
||||
<nav class="query-list-pagination query-list-pagination-bottom" aria-label="Query pagination"><a href="{{ next_url }}">Next page</a></nav>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -487,9 +487,9 @@ def _as_optional_bool(value, name):
|
|||
raise QueryValidationError("{} must be 0 or 1".format(name))
|
||||
|
||||
|
||||
def _query_list_limit(value):
|
||||
def _query_list_limit(value, default=50):
|
||||
if value in (None, ""):
|
||||
return 50
|
||||
return default
|
||||
try:
|
||||
return min(max(1, int(value)), 1000)
|
||||
except ValueError as ex:
|
||||
|
|
@ -1136,7 +1136,10 @@ class QueryListView(BaseView):
|
|||
database = await self.database_name(request)
|
||||
format_ = request.url_vars.get("format") or "html"
|
||||
try:
|
||||
limit = _query_list_limit(request.args.get("_size"))
|
||||
limit = _query_list_limit(
|
||||
request.args.get("_size"),
|
||||
default=20 if format_ == "html" else 50,
|
||||
)
|
||||
is_write = _as_optional_bool(request.args.get("is_write"), "is_write")
|
||||
is_published = _as_optional_bool(
|
||||
request.args.get("is_published"), "is_published"
|
||||
|
|
@ -1175,6 +1178,9 @@ class QueryListView(BaseView):
|
|||
data = {
|
||||
"ok": True,
|
||||
"database": database,
|
||||
"database_color": (
|
||||
self.ds.get_database(database).color if database is not None else None
|
||||
),
|
||||
"queries": page["queries"],
|
||||
"next": page["next"],
|
||||
"next_url": next_url,
|
||||
|
|
|
|||
|
|
@ -451,12 +451,34 @@ async def test_query_list_search_filter_and_html():
|
|||
assert html_response.status_code == 200
|
||||
assert "Demo query 02" in html_response.text
|
||||
assert "Demo query 01" not in html_response.text
|
||||
assert 'class="query-list-results"' in html_response.text
|
||||
assert "<legend>Mode</legend>" in html_response.text
|
||||
assert 'type="radio" name="is_published" value="1"' in html_response.text
|
||||
assert json_response.json()["queries"][0]["name"] == "demo_query_02"
|
||||
assert [query["name"] for query in filtered_response.json()["queries"]] == [
|
||||
"private_query"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_list_html_defaults_to_twenty_and_shows_pagination():
|
||||
ds = Datasette(memory=True)
|
||||
ds.root_enabled = True
|
||||
ds.add_memory_database("query_list_html_pagination", name="data")
|
||||
await ds.invoke_startup()
|
||||
await add_numbered_queries(ds, "data", 25)
|
||||
|
||||
response = await ds.client.get("/data/-/queries", actor={"id": "root"})
|
||||
json_response = await ds.client.get("/data/-/queries.json", actor={"id": "root"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text.count('aria-label="Query pagination"') == 1
|
||||
assert "Demo query 20" in response.text
|
||||
assert "Demo query 21" not in response.text
|
||||
assert 'href="/data/-/queries?_next=' in response.text
|
||||
assert len(json_response.json()["queries"]) == 25
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_global_query_list_api_and_html():
|
||||
ds = Datasette(memory=True)
|
||||
|
|
@ -519,7 +541,8 @@ async def test_global_query_list_api_and_html():
|
|||
("beta", "beta_first"),
|
||||
]
|
||||
assert html_response.status_code == 200
|
||||
assert 'href="/beta">beta</a>:' in html_response.text
|
||||
assert '<th scope="col">Database</th>' in html_response.text
|
||||
assert 'class="query-list-database" href="/beta">beta</a>' in html_response.text
|
||||
assert "Beta first" in html_response.text
|
||||
assert "Alpha first" not in html_response.text
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue