status/health-check.ex
Health check
A list of services with a status indicator each (operational, degraded, down) and their response times.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.status_health_check animated />
</div>
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
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
/* 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); }