empowered.house ↗

api/request.ex

Request

A request arcing from a client to a server and the response arcing back, with method, path, status and latency.

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

<div class="h-96 rounded-xl border border-border">
  <.api_request animated />
</div>
default
isometric
a read · custom copy
rejected · hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
client string "Orders app" Up to 14 characters.
server string "EHOS API" Up to 14 characters.
method string "POST"
path string "/v1/orders" Up to 24 characters.
status string "201 Created" A status that starts with 4 or 5 turns the response coral. Up to 22 characters.
latency string "38 ms"
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/api/request.ex
request.ex
defmodule EHVisuals.Api.Request do
  @moduledoc """
  One API call: the request arcs from the client to the server, the response
  arcs back, and the round trip time sits between them.
  """

  use EHVisuals.Composition,
    name: "api/request",
    title: "Request",
    description:
      "A request arcing from a client to a server and the response arcing back, with method, path, status and latency."

  @request_path "M76 78 C76 14 364 14 364 70"
  @response_path "M364 158 C364 222 76 222 76 166"

  attr(:client, :string, default: "Orders app", doc: "Up to 14 characters.")
  attr(:server, :string, default: "EHOS API", doc: "Up to 14 characters.")
  attr(:method, :string, default: "POST")
  attr(:path, :string, default: "/v1/orders", doc: "Up to 24 characters.")

  attr(:status, :string,
    default: "201 Created",
    doc: "A status that starts with 4 or 5 turns the response coral. Up to 22 characters."
  )

  attr(:latency, :string, default: "38 ms")

  attr(:label, :string,
    default: "A POST request from a client to a server, answered with 201 Created in 38 ms"
  )

  frame_attrs()

  @spec api_request(map()) :: Phoenix.LiveView.Rendered.t()
  def api_request(assigns) do
    method_w = 14 + String.length(assigns.method) * 5.6
    path_w = String.length(assigns.path) * 6.0
    request_w = method_w + 8 + path_w
    status_w = 18 + String.length(assigns.status) * 5.6

    assigns =
      assigns
      |> assign(:failed, String.starts_with?(assigns.status, ["4", "5"]))
      |> assign(:method_w, Float.round(method_w, 1))
      |> assign(:request_x, Float.round(220 - request_w / 2, 1))
      |> assign(:request_w, Float.round(request_w, 1))
      |> assign(:status_w, Float.round(status_w, 1))
      |> assign(:request_path, @request_path)
      |> assign(:response_path, @response_path)

    ~H"""
    <.frame name="api/request" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 440 236">
        <g class="ehv-h-lift">
          <path
            class="ehv-line-ink ehv-a-draw ehv-i-2"
            d={@request_path}
            pathLength="1"
          />
          <path class="ehv-fill-2 ehv-a-fade ehv-i-6" d="M359.5 69 h9 l-4.5 8 z" />
          <path
            class={[
              if(@failed, do: "ehv-line-coral", else: "ehv-line-lime"),
              "ehv-a-draw ehv-i-7"
            ]}
            d={@response_path}
            pathLength="1"
          />
          <path
            class={[
              if(@failed, do: "ehv-fill-coral", else: "ehv-api-request__tip"),
              "ehv-a-fade ehv-i-11"
            ]}
            d="M71.5 167 h9 l-4.5 -8 z"
          />

          <g :if={@animated} class="ehv-smil ehv-a-fade ehv-i-12">
            <circle class="ehv-fill-ink" r="3" opacity="0">
              <animateMotion
                path={@request_path}
                dur="3.6s"
                keyPoints="0;1;1"
                keyTimes="0;0.45;1"
                calcMode="linear"
                repeatCount="indefinite"
              />
              <animate
                attributeName="opacity"
                values="0;1;1;0;0"
                keyTimes="0;0.05;0.4;0.45;1"
                dur="3.6s"
                repeatCount="indefinite"
              />
            </circle>
            <circle class={if @failed, do: "ehv-fill-coral", else: "ehv-fill-lime"} r="3" opacity="0">
              <animateMotion
                path={@response_path}
                dur="3.6s"
                keyPoints="0;0;1;1"
                keyTimes="0;0.5;0.95;1"
                calcMode="linear"
                repeatCount="indefinite"
              />
              <animate
                attributeName="opacity"
                values="0;0;1;1;0"
                keyTimes="0;0.5;0.55;0.9;1"
                dur="3.6s"
                repeatCount="indefinite"
              />
            </circle>
          </g>

          <g class="ehv-a-rise ehv-i-0">
            <rect class="ehv-card" x="16.5" y="78.5" width="119" height="79" rx="10" />
            <rect class="ehv-node-soft" x="30.5" y="92.5" width="20" height="15" rx="3.5" />
            <line class="ehv-line" x1="31" x2="50" y1="97.5" y2="97.5" />
            <text class="ehv-t-strong" x="30" y="131" font-size="12.5">{@client}</text>
            <text class="ehv-mono" x="30" y="145" font-size="7.5">CLIENT</text>
          </g>
          <g class="ehv-a-rise ehv-i-1">
            <rect class="ehv-card" x="304.5" y="78.5" width="119" height="79" rx="10" />
            <rect class="ehv-node-soft" x="318.5" y="91.5" width="20" height="7" rx="2.5" />
            <rect class="ehv-node-soft" x="318.5" y="101.5" width="20" height="7" rx="2.5" />
            <circle class="ehv-fill-lime ehv-a-pulse" cx="333.5" cy="95" r="1.5" />
            <circle class="ehv-fill-3" cx="333.5" cy="105" r="1.5" />
            <text class="ehv-t-strong" x="318" y="131" font-size="12.5">{@server}</text>
            <text class="ehv-mono" x="318" y="145" font-size="7.5">SERVER</text>
          </g>

          <g class="ehv-a-fade ehv-i-4">
            <rect
              class="ehv-fill-canvas"
              x={@request_x - 8}
              y="18"
              width={@request_w + 16}
              height="24"
              rx="6"
            />
            <rect class="ehv-card-3" x={@request_x} y="20.5" width={@method_w} height="19" rx="5" />
            <text
              class="ehv-mono ehv-t-ink"
              x={@request_x + @method_w / 2}
              y="33.5"
              font-size="8"
              text-anchor="middle"
            >
              {String.upcase(@method)}
            </text>
            <text class="ehv-code ehv-t-2" x={@request_x + @method_w + 8} y="33.5" font-size="10">
              {@path}
            </text>
          </g>

          <g class="ehv-a-fade ehv-i-9">
            <text class="ehv-t-display" x="220" y="124" font-size="26" text-anchor="middle">
              {@latency}
            </text>
            <text class="ehv-mono" x="220" y="142" font-size="7.5" text-anchor="middle">
              ROUND TRIP
            </text>
          </g>

          <g class="ehv-a-pop ehv-i-10">
            <rect
              class="ehv-fill-canvas"
              x={220 - @status_w / 2 - 6}
              y="194"
              width={@status_w + 12}
              height="24"
              rx="6"
            />
            <rect
              class={if @failed, do: "ehv-fill-coral-soft", else: "ehv-fill-lime-soft"}
              x={220 - @status_w / 2}
              y="196"
              width={@status_w}
              height="20"
              rx="6"
            />
            <text
              class={["ehv-mono", if(@failed, do: "ehv-t-coral", else: "ehv-t-lime")]}
              x="220"
              y="209.5"
              font-size="8.5"
              text-anchor="middle"
            >
              {String.upcase(@status)}
            </text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/api/request.css
request.css
/* api/request */
.ehv-api-request { --ehv-max-w: 500px; --ehv-iso-scale: 0.82; --ehv-step: 110ms; }

/* The arrowhead takes the line colour, so it stays readable on the light surface. */
.ehv-api-request__tip { fill: var(--ehv-lime); }