empowered.house ↗

status/health-check.ex

Health check

A list of services with a status indicator each (operational, degraded, down) and their response times.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.status_health_check animated />
</div>
default
isometric
all operational · custom copy
one down · hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "System status"
services list [%{name: "ERP connector", status: "operational", time: "84 ms"}, %{name: "Order intake agent", ...}, ...] Up to seven `%{name, status, time}`. Status is `operational`, `degraded` or `down`. Names up to 22 characters, `time` is the response time as text.
summary any nil The pill in the header, up to 18 characters. `nil` writes it from the statuses.
checked string "Checked 30 seconds ago"
Frame props, shared by every composition
Prop Type Default What it does
animated boolean false Turns motion on. Without it the composition renders in its final, still state.
trigger "mount" | "in-view" | "in-view-repeat" "in-view" When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view.
hover boolean false Enables the hover micro-interactions the composition supports.
glow boolean true The warm incandescent halo behind the composition.
fade_out boolean false Fades the bottom edge into the surface underneath.
isometric boolean false Tilts the composition into the isometric plane of the platform stack.
theme "dark" | "light" "dark" `dark` for the site canvas, `light` for the cream reversed surface.
class string nil Extra classes for the frame.
rest global — Any other HTML attribute, set on the frame.

Source

Two files. Copy them into your app if you would rather own the code than depend on the library.

lib/eh_visuals/status/health-check.ex
health-check.ex
defmodule EHVisuals.Status.HealthCheck do
  @moduledoc """
  A list of services, each with a status indicator and its response time, and
  a summary of the whole list in the header.
  """

  use EHVisuals.Composition,
    name: "status/health-check",
    title: "Health check",
    description:
      "A list of services with a status indicator each (operational, degraded, down) and their response times."

  @services [
    %{name: "ERP connector", status: "operational", time: "84 ms"},
    %{name: "Order intake agent", status: "operational", time: "120 ms"},
    %{name: "Mail gateway", status: "operational", time: "96 ms"},
    %{name: "Bank feed", status: "degraded", time: "640 ms"},
    %{name: "Warehouse sync", status: "operational", time: "152 ms"}
  ]

  @first 58
  @pitch 34

  attr(:title, :string, default: "System status")

  attr(:services, :list,
    default: @services,
    doc:
      "Up to seven `%{name, status, time}`. Status is `operational`, `degraded` or `down`. Names up to 22 characters, `time` is the response time as text."
  )

  attr(:summary, :any,
    default: nil,
    doc: "The pill in the header, up to 18 characters. `nil` writes it from the statuses."
  )

  attr(:checked, :string, default: "Checked 30 seconds ago")

  attr(:label, :string,
    default: "Five services with their response times, four operational and one degraded"
  )

  frame_attrs()

  @spec status_health_check(map()) :: Phoenix.LiveView.Rendered.t()
  def status_health_check(assigns) do
    rows = assigns.services |> Enum.take(7) |> Enum.with_index()
    statuses = Enum.map(rows, fn {row, _index} -> row.status end)
    summary = assigns.summary || summary(statuses)
    foot = @first + length(rows) * @pitch

    assigns =
      assigns
      |> assign(:rows, rows)
      |> assign(:tone, tone(statuses))
      |> assign(:pill, summary)
      |> assign(:pill_w, Float.round(28 + String.length(summary) * 5.9, 1))
      |> assign(:foot, foot)
      |> assign(:first, @first)
      |> assign(:pitch, @pitch)
      |> assign(:height, foot + 34)

    ~H"""
    <.frame name="status/health-check" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 340 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height={@height - 1} rx="12" />
          <text class="ehv-t-strong ehv-a-fade ehv-i-1" x="20" y="35" font-size="14">{@title}</text>
          <g class="ehv-a-fade ehv-i-2">
            <rect
              class={"ehv-status-health-check__pill--#{@tone}"}
              x={320 - @pill_w}
              y="20"
              width={@pill_w}
              height="20"
              rx="6"
            />
            <circle
              class={["ehv-a-pulse", "ehv-status-health-check__lamp--#{@tone}"]}
              cx={320 - @pill_w + 11}
              cy="30"
              r="2.5"
            />
            <text
              class={["ehv-mono", "ehv-status-health-check__word--#{@tone}"]}
              x="311"
              y="33.5"
              font-size="8.5"
              text-anchor="end"
            >
              {String.upcase(@pill)}
            </text>
          </g>
          <line class="ehv-line-faint" x1="20" x2="320" y1="52.5" y2="52.5" />
          <g :for={{row, index} <- @rows} class={"ehv-a-rise ehv-i-#{min(index + 3, 24)}"}>
            <line
              :if={index > 0}
              class="ehv-line-faint"
              x1="20"
              x2="320"
              y1={@first + index * @pitch + 0.5}
              y2={@first + index * @pitch + 0.5}
            />
            <g :if={row.status == "operational"}>
              <circle
                class={"ehv-fill-lime ehv-a-ping ehv-i-#{min(index * 3, 24)}"}
                cx="28"
                cy={@first + index * @pitch + 17}
                r="4"
              />
              <circle class="ehv-fill-lime" cx="28" cy={@first + index * @pitch + 17} r="4" />
            </g>
            <g :if={row.status == "degraded"}>
              <circle
                class="ehv-status-health-check__ring"
                cx="28"
                cy={@first + index * @pitch + 17}
                r="4"
              />
              <circle class="ehv-fill-coral" cx="28" cy={@first + index * @pitch + 17} r="1.5" />
            </g>
            <g :if={row.status not in ["operational", "degraded"]}>
              <circle class="ehv-fill-coral-soft" cx="28" cy={@first + index * @pitch + 17} r="7" />
              <circle class="ehv-fill-coral" cx="28" cy={@first + index * @pitch + 17} r="4" />
            </g>
            <text x="46" y={@first + index * @pitch + 21.5} font-size="12.5">{row.name}</text>
            <text
              class={["ehv-mono", if(row.status == "operational", do: "ehv-t-2", else: "ehv-t-coral")]}
              x="234"
              y={@first + index * @pitch + 20.5}
              font-size="8.5"
              text-anchor="end"
            >
              {String.upcase(row.time)}
            </text>
            <text
              class={["ehv-mono", row.status != "operational" && "ehv-t-coral"]}
              x="320"
              y={@first + index * @pitch + 20.5}
              font-size="8"
              text-anchor="end"
            >
              {String.upcase(row.status)}
            </text>
          </g>
          <g class="ehv-a-fade ehv-i-9">
            <line class="ehv-line-faint" x1="20" x2="320" y1={@foot + 0.5} y2={@foot + 0.5} />
            <text class="ehv-mono ehv-t-4" x="20" y={@foot + 20} font-size="8">
              {String.upcase(@checked)}
            </text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end

  defp tone(statuses) do
    cond do
      Enum.any?(statuses, &(&1 not in ["operational", "degraded"])) -> "down"
      "degraded" in statuses -> "degraded"
      true -> "ok"
    end
  end

  defp summary(statuses) do
    case tone(statuses) do
      "ok" -> "All operational"
      "degraded" -> "#{Enum.count(statuses, &(&1 == "degraded"))} degraded"
      "down" -> "#{Enum.count(statuses, &(&1 not in ["operational", "degraded"]))} down"
    end
  end
end
assets/css/status/health-check.css
health-check.css
/* status/health-check */
.ehv-status-health-check { --ehv-max-w: 360px; }

.ehv-status-health-check__ring { fill: none; stroke: var(--ehv-coral); stroke-width: 1.5; }

.ehv-status-health-check__pill--ok { fill: var(--ehv-lime-soft); }
.ehv-status-health-check__pill--degraded { fill: var(--ehv-coral-soft); }
.ehv-status-health-check__pill--down { fill: var(--ehv-coral-soft); stroke: var(--ehv-coral); stroke-width: 1; }

.ehv-status-health-check__lamp--ok { fill: var(--ehv-lime); }
.ehv-status-health-check__lamp--degraded,
.ehv-status-health-check__lamp--down { fill: var(--ehv-coral); }

.ehv svg .ehv-mono.ehv-status-health-check__word--ok { fill: var(--ehv-lime); }
.ehv svg .ehv-mono.ehv-status-health-check__word--degraded,
.ehv svg .ehv-mono.ehv-status-health-check__word--down { fill: var(--ehv-coral); }