Commit bf87331e authored by 20after4's avatar 20after4
Browse files

Add custom_dashboard_chart plugin and dynamic filters

* Proof of concept: Adds a new render_custom_dashboard_chart hook and a new 'custom' rendering engine type.
  The new hook can be used by other datasette plugins to render custom charts.
* Also added a "select" type dashboard filter that populates it's list of options from a query
supplied in the plugin config metadata.

Example `render_custom_dashboard_chart` hook:

```python
@hookimpl
async def render_custom_dashboard_chart(chart_display):
    return "<h3>test <b>1</b> 2 3</h3>"
```

Example query filter config:

```yaml
plugins:
  datasette-dashboards:
    project-metrics:
      title: Data³ - workflow metrics
      description: Metrics about projects, tasks and workflows
      layout:
      - [ custom-chart-display, custom-chart-display]
      - [project-events, column-metrics]
      - [project-tasks-state, project-tasks-state]
      - [task-states,task-states ]
    filters:
        project:
          name: Project
          type: select
          query: select phid as key, name as label from Project
```
parent 3973d709
from pprint import pprint
import re import re
import sys
from typing import Coroutine
import urllib import urllib
from datasette import hookimpl from datasette import hookimpl
from datasette.plugins import pm
from datasette.hookspecs import hookspec
from datasette.utils.asgi import Forbidden, NotFound, Response from datasette.utils.asgi import Forbidden, NotFound, Response
from jinja2 import Markup # type: ignore
sql_opt_pattern = re.compile(r"(?P<opt>\[\[[^\]]*\]\])") sql_opt_pattern = re.compile(r"(?P<opt>\[\[[^\]]*\]\])")
sql_var_pattern = re.compile(r"\:(?P<var>[a-zA-Z0-9_]+)") sql_var_pattern = re.compile(r"\:(?P<var>[a-zA-Z0-9_]+)")
@hookspec(firstresult=True)
async def render_custom_dashboard_chart(chart_display):
""" Render a custom dashboard chart display. """
pm.add_hookspecs(sys.modules[__name__])
@hookimpl
def extra_template_vars(datasette, database):
async def render_custom_dashboard_chart(chart_display):
result = pm.hook.render_custom_dashboard_chart(chart_display=chart_display) # type: ignore
if not result:
result = chart_display
if isinstance(result, Coroutine):
result = await result
return Markup(result)
return {"render_custom_dashboard_chart": render_custom_dashboard_chart }
async def check_permission_instance(request, datasette): async def check_permission_instance(request, datasette):
if ( if (
await datasette.permission_allowed( await datasette.permission_allowed(
...@@ -36,6 +60,14 @@ def get_dashboard_filters_keys(request, dashboard): ...@@ -36,6 +60,14 @@ def get_dashboard_filters_keys(request, dashboard):
return set(filters_keys) & set(request.args.keys()) return set(filters_keys) & set(request.args.keys())
async def populate_dashboard_filter_queries(database, dashboard):
filters = dashboard.get("filters") or {}
for key,filter in filters.items():
if 'query' in filter:
res = await database.execute(filter["query"])
filter['options'] = {row['key']:row['label'] for row in res}
def get_dashboard_filters(request, opts_keys): def get_dashboard_filters(request, opts_keys):
return {key: request.args[key] for key in opts_keys} return {key: request.args[key] for key in opts_keys}
...@@ -93,7 +125,7 @@ async def dashboard_view(request, datasette): ...@@ -93,7 +125,7 @@ async def dashboard_view(request, datasette):
except KeyError: except KeyError:
raise NotFound(f"Database does not exist: {db}") raise NotFound(f"Database does not exist: {db}")
await check_permission_execute_sql(request, datasette, database) await check_permission_execute_sql(request, datasette, database)
await populate_dashboard_filter_queries(database, dashboard)
options_keys = get_dashboard_filters_keys(request, dashboard) options_keys = get_dashboard_filters_keys(request, dashboard)
query_parameters = get_dashboard_filters(request, options_keys) query_parameters = get_dashboard_filters(request, options_keys)
query_string = generate_dashboard_filters_qs(request, options_keys) query_string = generate_dashboard_filters_qs(request, options_keys)
......
...@@ -22,7 +22,9 @@ function renderVegaChart(el, chart, query_string, height_style = undefined) { ...@@ -22,7 +22,9 @@ function renderVegaChart(el, chart, query_string, height_style = undefined) {
...chart.display ...chart.display
}; };
vegaEmbed(el, spec); vegaEmbed(el, spec).then(function(result) {
console.log('Vega-embed view:',result.view);
});
} }
async function renderMetricChart(el, chart, query_string) { async function renderMetricChart(el, chart, query_string) {
......
...@@ -40,6 +40,8 @@ ...@@ -40,6 +40,8 @@
<div id="chart" class="chart-container"> <div id="chart" class="chart-container">
{% if chart.library == 'markdown' %} {% if chart.library == 'markdown' %}
{{ render_markdown(chart.display) }} {{ render_markdown(chart.display) }}
{% elif chart.library == 'custom' %}
{{ render_custom_dashboard_chart(chart.display) }}
{% endif %} {% endif %}
</div> </div>
</div> </div>
......
...@@ -50,11 +50,20 @@ ...@@ -50,11 +50,20 @@
<form method="GET"> <form method="GET">
{% for key, dfilter in dashboard.filters.items() %} {% for key, dfilter in dashboard.filters.items() %}
{% set dfilter_type = dfilter.type if dfilter.type in ['text', 'date', 'number'] else 'text' %}
<p> <p>
<label for="{{ key }}">{{ dfilter.name }}</label> <label for="{{ key }}">{{ dfilter.name }}</label>
<input id="{{ key }}" name="{{ key }}" type="{{ dfilter_type }}"{% if dfilter.min is defined %} min="{{ dfilter.min }}"{% endif %}{% if dfilter.max is defined %} max="{{ dfilter.max }}"{% endif %}{% if dfilter.step is defined %} step="{{ dfilter.step }}"{% endif %} value="{% if key in query_parameters.keys() %}{{ query_parameters[key] }}{% else %}{{ dfilter.default }}{% endif %}"> {% if dfilter.type == 'select' %}
{% set dfilter_select_val = query_parameters[key] if key in query_parameters.keys() else dfilter.default %}
<select name="{{ key }}" id="{{ key }}">
{% for option, val in dfilter.options.items() %}
<option {% if dfilter_select_val == option %}selected {%endif%} value="{{ option }}">{{ val }}</option>
{% endfor %}
</select>
{% else %}
{% set dfilter_type = dfilter.type if dfilter.type in ['text', 'date', 'number'] else 'text' %}
<input id="{{ key }}" name="{{ key }}" type="{{ dfilter_type }}"{% if dfilter.min is defined %} min="{{ dfilter.min }}"{% endif %}{% if dfilter.max is defined %} max="{{ dfilter.max }}"{% endif %}{% if dfilter.step is defined %} step="{{ dfilter.step }}"{% endif %} value="{% if key in query_parameters.keys() %}{{ query_parameters[key] }}{% else %}{{ dfilter.default }}{% endif %}">
</p> </p>
{%endif%}
{% endfor %} {% endfor %}
<input type="submit" value="Apply"> <input type="submit" value="Apply">
</form> </form>
...@@ -73,6 +82,8 @@ ...@@ -73,6 +82,8 @@
<div id="chart-{{ chart_slug }}" class="dashboard-card-chart"> <div id="chart-{{ chart_slug }}" class="dashboard-card-chart">
{% if chart.library == 'markdown' %} {% if chart.library == 'markdown' %}
{{ render_markdown(chart.display) }} {{ render_markdown(chart.display) }}
{% elif chart.library == 'custom' %}
{{ render_custom_dashboard_chart(chart.display) }}
{% endif %} {% endif %}
</div> </div>
</div> </div>
......
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