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-deploy.
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.