Static Dashboards
First published: November 4, 2024
Last updated: November 26, 2024
I’ve been building dashboards (essentially just pretty tables and some plots) that display data from different public data sources in a novel ways.
Most of these dashboards are ephemeral and experimental, so I’d rather not host a separate public application + database before I’m sure they’ll be around for a while. The simplest and most secure way to do this is to setup a static website and setup some kind of auto-update mechanism to build and re-deploy1.
Table Rendering with Hugo ¶
The trick with tables is that they have to flexible enough for me to quickly iterate and change the layout and styling.
Often I’m displaying a lot of information in a single cell, and I’ll change the column order and cell styling a lot before I’m happy with it.
This hugo partial takes an array of column names keys
and an array of objects and renders that into a table.
<div class="border-2 rounded-md border-gray-300">
{{if .search}}
<noscript><div class="mx-auto"><b>Enable JS for search</b></div></noscript>
{{ end }}
<div id="{{.tableId}}-outer" class="max-h-96 overscroll-auto overflow-x-auto overflow-y-auto">
<table id="{{.tableId}}" class="m-0 min-w-full table-auto border-collapse">
<thead class="bg-gray-100">
<tr>
{{- range .keys -}}
<th class="border border-gray-300 px-4 py-2 text-left text-gray-700">{{- . -}}</th>
{{- end -}}
</tr>
</thead>
<tbody>
{{ $keys := .keys }}
{{- range .values -}}
<tr class="odd:bg-white even:bg-gray-50">
{{- $row := . -}}
{{- range $keys -}}
<td class="border border-gray-300 px-4 py-2 text-gray-700">
{{- index $row . | safeHTML -}} </td>
{{- end -}}
</tr>
{{- end -}}
</tbody>
</table>
</div>
</div>
You could just iterate through the k,v
pairs in array of objects, but I’d like a deterministic ordering of columns, so I settled on a separate array with a determined order.
I’ll handle that offline using either python’s OrderedDict
or a Go struct
.
The only other thing of note is that the values are raw HTML strings I generate, so I pass them through safeHTML
for rendering.
Input Data and Keeping Updated ¶
The input data is typically a JSON document generated from an ETL job or one of my API’s:
{
"data" : [{"col1":"val1", "col2":"val2"...}, ...],
"keys" : ["col1","col2", ...],
"lastUpdated": <timestamp>
}
My display page will consist of my partial wrapped in a shortcode, which will {{ .Get }}
and parse the JSON document to pass to the partial. The JSON doc is stored as a page resource.
Scheduling Updates ¶
Now I can update the dashboard by re-generating the JSON document with fresh data. There are three ways I’ve done this:
- Manually regenerate and push back to gitlab to trigger a new build
- Store the document in a bucket and have the CI build pull the new document
- Have hugo make an API call as part of the build
I always have a lastUpdated
field so I can display it appropriately, and set up a pipeline schedule on gitlab.
While you could generate the entire table instead of a json document that hugo builds, I want my general table structure to look uniform, so it is better for me to have one place for the table skeleton, then fill it in with custom data.
Simon Willison refers to a this as the “baked data” pattern. https://simonwillison.net/2021/Jul/28/baked-data/ ↩︎