Nick George
all/

Static Dashboards

First published: November 4, 2024
Last updated: November 5, 2024
image demonstrating a CI/CD build updating a static website table

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:

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.