Commit 24ca7d32 authored by Romain Clement's avatar Romain Clement
Browse files

Basic dashboard filters from URL

parent 0f0a5589
import re
import urllib
from datasette import hookimpl
from datasette.utils.asgi import Forbidden, NotFound, Response
sql_opt_pattern = re.compile(r"(?P<opt>\[\[[^\]]*\]\])")
sql_var_pattern = re.compile(r"\:(?P<var>[a-zA-Z0-9_]+)")
async def check_permission_instance(request, datasette):
if (
await datasette.permission_allowed(
......@@ -26,6 +31,36 @@ async def check_permission_execute_sql(request, datasette, database):
raise Forbidden("execute-sql denied")
def get_dashboard_filters_keys(request, dashboard):
filters_keys = (dashboard.get("filters") or {}).keys()
return set(filters_keys) & set(request.args.keys())
def generate_dashboard_filters_qs(request, opts_keys):
return urllib.parse.urlencode({key: request.args[key] for key in opts_keys})
def fill_chart_query_options(chart, options_keys):
query = chart.get("query")
if query is None:
return
to_replace = []
for opt_match in re.finditer(sql_opt_pattern, query):
opt_group = opt_match.group("opt")
var_match = re.search(sql_var_pattern, opt_group)
var_group = var_match.group("var")
to_replace.append({"opt": opt_group, "keep": var_group in options_keys})
for r in to_replace:
if r["keep"]:
query = query.replace(r["opt"], r["opt"].strip("[[]]"))
else:
query = query.replace(r["opt"], "")
chart["query"] = query
async def dashboard_list(request, datasette):
await check_permission_instance(request, datasette)
config = datasette.plugin_config("datasette-dashboards") or {}
......@@ -55,10 +90,20 @@ async def dashboard_view(request, datasette):
raise NotFound(f"Database does not exist: {db}")
await check_permission_execute_sql(request, datasette, database)
options_keys = get_dashboard_filters_keys(request, dashboard)
query_string = generate_dashboard_filters_qs(request, options_keys)
for chart in dashboard["charts"].values():
fill_chart_query_options(chart, options_keys)
return Response.html(
await datasette.render_template(
"dashboard_view.html",
{"slug": slug, "dashboard": dashboard},
{
"slug": slug,
"query_string": query_string,
"dashboard": dashboard,
},
)
)
......@@ -85,11 +130,16 @@ async def dashboard_chart(request, datasette):
database = datasette.get_database(db)
await check_permission_execute_sql(request, datasette, database)
options_keys = get_dashboard_filters_keys(request, dashboard)
query_string = generate_dashboard_filters_qs(request, options_keys)
fill_chart_query_options(chart, options_keys)
return Response.html(
await datasette.render_template(
"dashboard_chart.html",
{
"slug": slug,
"query_string": query_string,
"dashboard": dashboard,
"chart": chart,
},
......
function renderVegaChart(el, chart, height_style = undefined) {
function renderVegaChart(el, chart, query_string, height_style = undefined) {
const query = encodeURIComponent(chart.query)
const spec = {
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
......@@ -16,7 +16,7 @@ function renderVegaChart(el, chart, height_style = undefined) {
}
},
data: {
url: `/${chart.db}.csv?sql=${query}`,
url: `/${chart.db}.csv?sql=${query}&${query_string}`,
format: {'type': 'csv'}
},
...chart.display
......@@ -25,9 +25,9 @@ function renderVegaChart(el, chart, height_style = undefined) {
vegaEmbed(el, spec);
}
async function renderMetricChart(el, chart) {
async function renderMetricChart(el, chart, query_string) {
const query = encodeURIComponent(chart.query)
const results = await fetch(`/${chart.db}.json?sql=${query}&_shape=array`)
const results = await fetch(`/${chart.db}.json?sql=${query}&${query_string}&_shape=array`)
const data = await results.json()
const metric = data[0][chart.display.field]
......
......@@ -29,7 +29,11 @@
</div>
{% if chart.db and chart.query %}
<p><a class="not-underlined" title="{{ chart.query }}" href="{{ urls.database(chart.db) }}?{{ {'sql': chart.query}|urlencode|safe }}">&#x270e; <span class="underlined">View and edit SQL</span></a></p>
<p>
<a class="not-underlined" title="{{ chart.query }}" href="{{ urls.database(chart.db) }}?{{ {'sql': chart.query}|urlencode|safe }}{% if query_string|length > 0 %}&{{ query_string|safe }}{% endif %}">
&#x270e; <span class="underlined">View and edit SQL</span>
</a>
</p>
{% endif %}
<div class="dashboard-card">
......@@ -46,9 +50,9 @@
<script src="{{ urls.static_plugins('datasette_dashboards', 'dashboards.js') }}"></script>
<script type="text/javascript">
{% if chart.library == 'vega' %}
renderVegaChart('#chart', JSON.parse('{{ chart|tojson }}'), 'container')
renderVegaChart('#chart', JSON.parse('{{ chart|tojson }}'), '{{ query_string|safe }}', 'container')
{% elif chart.library == 'metric' %}
renderMetricChart('#chart', JSON.parse('{{ chart|tojson }}'))
renderMetricChart('#chart', JSON.parse('{{ chart|tojson }}'), '{{ query_string|safe }}')
{% endif %}
</script>
{% endblock %}
......@@ -49,7 +49,7 @@
<div id="card-{{ chart_slug }}" class="dashboard-card">
{% if chart.library != 'markdown' %}
<div class="dashboard-card-title">
<p><a href="{{ urls.path('-/dashboards') }}/{{ slug }}/{{ chart_slug }}">{{ chart.title }}</a></p>
<p><a href="{{ urls.path('-/dashboards') }}/{{ slug }}/{{ chart_slug }}{% if query_string|length > 0 %}?{{ query_string|safe }}{% endif %}">{{ chart.title }}</a></p>
</div>
{% endif %}
......@@ -69,9 +69,9 @@
<script type="text/javascript">
{% for chart_slug, chart in dashboard.charts.items() %}
{% if chart.library == 'vega' %}
renderVegaChart('#chart-{{ chart_slug }}', JSON.parse('{{ chart|tojson }}'))
renderVegaChart('#chart-{{ chart_slug }}', JSON.parse('{{ chart|tojson }}'), '{{ query_string|safe }}')
{% elif chart.library == 'metric' %}
renderMetricChart('#chart-{{ chart_slug }}', JSON.parse('{{ chart|tojson }}'))
renderMetricChart('#chart-{{ chart_slug }}', JSON.parse('{{ chart|tojson }}'), '{{ query_string|safe }}')
{% endif %}
{% endfor %}
</script>
......
......@@ -3,6 +3,7 @@ about: rclement/datasette-dashboards
about_url: https://github.com/rclement/datasette-dashboards
description_html: |-
Try it out at <a href="/-/dashboards">/-/dashboards</a>
plugins:
datasette-dashboards:
job-offers-stats:
......@@ -11,6 +12,33 @@ plugins:
layout:
- [analysis-note, offers-day, offers-day, offers-count]
- [analysis-note, offers-source, offers-day-source, offers-region]
filters:
title:
name: Title
type: search
source:
name: Source
type: category
date_start:
name: Date Start
type: date
default: 2021-01-01
date_end:
name: Date End
type: date
default: 2021-12-31
value:
name: Some Value
type: number
min: 0
max: 10
default: 5
threshold:
name: Threshold
type: range
min: 1
max: 1000
step: 10
charts:
analysis-note:
library: markdown
......@@ -48,7 +76,7 @@ plugins:
offers-count:
title: Total number of offers
db: jobs
query: SELECT count(*) as count FROM offers_view;
query: SELECT count(*) as count FROM offers_view WHERE TRUE [[ AND date >= date(:date_start) ]] [[ AND date <= date(:date_end) ]];
library: metric
display:
field: count
......@@ -58,7 +86,7 @@ plugins:
offers-day:
title: Number of offers by day
db: jobs
query: SELECT date(date) as day, count(*) as count FROM offers_view GROUP BY day ORDER BY day
query: SELECT date(date) as day, count(*) as count FROM offers_view WHERE TRUE [[ AND date >= date(:date_start) ]] [[ AND date <= date(:date_end) ]] GROUP BY day ORDER BY day
library: vega
display:
mark: { type: line, tooltip: true }
......@@ -69,7 +97,7 @@ plugins:
offers-source:
title: Number of offers by source
db: jobs
query: SELECT source, count(*) as count FROM offers_view GROUP BY source ORDER BY count DESC
query: SELECT source, count(*) as count FROM offers_view WHERE TRUE [[ AND date >= date(:date_start) ]] [[ AND date <= date(:date_end) ]] GROUP BY source ORDER BY count DESC
library: vega
display:
mark: { type: arc, tooltip: true }
......@@ -87,7 +115,7 @@ plugins:
offers-day-source:
title: Offers by day by source
db: jobs
query: SELECT date(date) as day, source, count(*) as count FROM offers_view GROUP BY day, source ORDER BY day
query: SELECT date(date) as day, source, count(*) as count FROM offers_view WHERE TRUE [[ AND date >= date(:date_start) ]] [[ AND date <= date(:date_end) ]] GROUP BY day, source ORDER BY day
library: vega
display:
mark: { type: bar, tooltip: true }
......@@ -106,7 +134,7 @@ plugins:
offers-region:
title: Offers by region
db: jobs
query: SELECT region, count(*) as count FROM offers_view GROUP BY region ORDER BY count DESC
query: SELECT region, count(*) as count FROM offers_view WHERE TRUE [[ AND date >= date(:date_start) ]] [[ AND date <= date(:date_end) ]] GROUP BY region ORDER BY count DESC
library: vega
display:
mark: geoshape
......
......@@ -35,6 +35,18 @@ def datasette_metadata():
"job-dashboard": {
"title": "Job dashboard",
"description": "Gathering metrics about jobs",
"filters": {
"date_start": {
"name": "Date Start",
"type": "date",
"default": "2021-01-01",
},
"date_end": {
"name": "Date End",
"type": "date",
"default": "2021-12-31",
},
},
"charts": {
"analysis-note": {
"library": "markdown",
......@@ -84,7 +96,7 @@ def datasette_metadata():
"offers-day": {
"title": "Number of offers by day",
"db": "test",
"query": "SELECT date(date) as day, count(*) as count FROM jobs GROUP BY day ORDER BY day",
"query": "SELECT date(date) as day, count(*) as count FROM offers_view WHERE TRUE [[ AND date >= date(:date_start) ]] [[ AND date <= date(:date_end) ]] GROUP BY day ORDER BY day",
"library": "vega",
"display": {
"mark": {
......
......@@ -60,6 +60,18 @@ async def test_dashboard_view_layout(datasette):
datasette._metadata = original_metadata
@pytest.mark.asyncio
async def test_dashboard_view_parameters(datasette):
response = await datasette.client.get(
"/-/dashboards/job-dashboard?date_start=2021-01-01"
)
assert response.status_code == 200
assert (
"SELECT date(date) as day, count(*) as count FROM offers_view WHERE TRUE AND date \\u003e= date(:date_start) GROUP BY day ORDER BY day"
in response.text
)
@pytest.mark.asyncio
async def test_dashboard_view_unknown(datasette):
response = await datasette.client.get("/-/dashboards/unknown-dashboard")
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment