Current section

Files

Jump to
oban_web lib oban web components core.ex
Raw

lib/oban/web/components/core.ex

defmodule Oban.Web.Components.Core do
use Oban.Web, :html
attr :click, :string, required: true
attr :danger, :boolean, default: false
attr :disabled, :boolean, default: false
attr :label, :string, required: true
attr :target, :any
slot :icon
slot :title
def action_button(assigns) do
class =
cond do
assigns.disabled ->
"text-gray-400"
assigns.danger ->
"text-red-500 group-hover:text-red-600 hover:bg-gray-100 dark:hover:bg-gray-950"
true ->
"text-gray-500 group-hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-950"
end
assigns = assign(assigns, :class, class)
~H"""
<button
class={["flex items-center space-x-2 px-3 py-1.5 rounded-md text-sm", @class]}
data-title={render_slot(@title)}
disabled={@disabled}
id={@click}
phx-click={@click}
phx-hook="Tippy"
phx-target={@target}
type="button"
>
{render_slot(@icon)}
<span class="block">{@label}</span>
</button>
"""
end
slot :inner_block, required: true
attr :name, :string, required: true
attr :options, :list, required: true
attr :selected, :any, required: true
attr :title, :string, required: true
attr :disabled, :boolean, default: false
attr :target, :any, default: "myself"
def dropdown_button(assigns) do
~H"""
<div class="relative" id={"#{@name}-selector"}>
<button
aria-expanded="true"
aria-haspopup="listbox"
class="cursor-pointer text-gray-500 dark:text-gray-400 disabled:text-gray-400 disabled:dark:text-gray-500 focus:outline-none hover:text-gray-800 dark:hover:text-gray-200 hidden md:block"
data-title={@title}
disabled={@disabled}
id={"#{@name}-menu-toggle"}
phx-hook="Tippy"
phx-click={JS.toggle(to: "##{@name}-menu")}
type="button"
>
{render_slot(@inner_block)}
</button>
<ul
class="hidden absolute z-50 top-full right-0 mt-2 w-32 overflow-hidden border border-gray-100 dark:border-gray-800 rounded-md shadow-md text-sm font-semibold bg-white dark:bg-gray-800 focus:outline-none"
id={"#{@name}-menu"}
role="listbox"
tabindex="-1"
>
<%= for option <- @options do %>
<.dropdown_option name={@name} value={option} selected={@selected} target={@target} />
<% end %>
</ul>
</div>
"""
end
attr :name, :any, required: true
attr :value, :any, required: true
attr :selected, :any, required: true
attr :target, :any
defp dropdown_option(assigns) do
class =
if assigns.selected == assigns.value do
"text-blue-500 dark:text-blue-400"
else
"text-gray-500 dark:text-gray-400 "
end
assigns = assign(assigns, :class, class)
~H"""
<li
class={"block w-full py-1 px-2 flex items-center cursor-pointer select-none space-x-2 hover:bg-gray-50 hover:dark:bg-gray-600/30 #{@class}"}
id={"select-#{@name}-#{@value}"}
role="option"
phx-click={"select-#{@name}"}
phx-click-away={JS.hide(to: "##{@name}-menu")}
phx-target={@target}
phx-value-choice={@value}
>
<%= if to_string(@value) == to_string(@selected) do %>
<Icons.check class="w-5 h-5" />
<% else %>
<span class="block w-5 h-5"></span>
<% end %>
<span class="capitalize text-gray-800 dark:text-gray-200">
{@value |> to_string() |> String.replace("_", " ")}
</span>
</li>
"""
end
attr :checked, :boolean, default: false
attr :click, :string, required: true
attr :myself, :any, required: true
attr :value, :string, required: true
def row_checkbox(assigns) do
style =
if assigns.checked do
"border-blue-500 bg-blue-500"
else
"border-gray-400 dark:border-gray-600 group-hover:bg-gray-400 dark:group-hover:bg-gray-600"
end
assigns = assign(assigns, :style, style)
~H"""
<button
class="p-6 group"
phx-click={@click}
phx-target={@myself}
phx-value-id={@value}
rel="check"
>
<div class={["w-4 h-4 flex items-center justify-center rounded border", @style]}>
<Icons.check class="w-3 h-3 text-white dark:text-gray-900" />
</div>
</button>
"""
end
attr :checked, :atom, required: true
attr :click, :string, required: true
attr :myself, :any, required: true
def all_checkbox(assigns) do
style =
if assigns.checked in [:all, :some] do
"border-blue-500 bg-blue-500"
else
"border-gray-400 dark:border-gray-600 group-hover:bg-gray-400 dark:group-hover:bg-gray-600"
end
assigns = assign(assigns, :style, style)
~H"""
<button
class="p-6 group"
data-title="Select All"
id="toggle-select"
phx-click={@click}
phx-hook="Tippy"
phx-target={@myself}
type="button"
>
<div class={["w-4 h-4 flex items-center justify-center rounded border", @style]}>
<%= if @checked == :some do %>
<Icons.indeterminate class="w-3 h-3 text-white dark:text-gray-900" />
<% else %>
<Icons.check class="w-3 h-3 text-white dark:text-gray-900" />
<% end %>
</div>
</button>
"""
end
@doc """
A status badge with icon that expands to show label on hover.
"""
attr :icon, :string, required: true
attr :id, :string, default: nil
attr :label, :string, required: true
def status_badge(assigns) do
~H"""
<div id={@id} class="group flex items-center cursor-default select-none">
<span class="inline-flex items-center justify-center h-9 pl-2.5 pr-2.5 group-hover:pr-4 rounded-full text-sm font-medium bg-violet-100 text-violet-700 dark:bg-violet-700/70 dark:text-violet-200 transition-all duration-200">
<.badge_icon name={@icon} />
<span class="max-w-0 overflow-hidden group-hover:max-w-24 group-hover:ml-1.5 transition-all duration-200 whitespace-nowrap">
{@label}
</span>
</span>
</div>
"""
end
defp badge_icon(%{name: "camera"} = assigns), do: ~H[<Icons.camera class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "life_buoy"} = assigns),
do: ~H[<Icons.life_buoy class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "lock_closed"} = assigns),
do: ~H[<Icons.lock_closed class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "sparkles"} = assigns),
do: ~H[<Icons.sparkles class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "table_cells"} = assigns),
do: ~H[<Icons.table_cells class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "square_2x2"} = assigns),
do: ~H[<Icons.square_2x2 class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "rectangle_group"} = assigns),
do: ~H[<Icons.rectangle_group class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "user_group"} = assigns),
do: ~H[<Icons.user_group class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "link"} = assigns),
do: ~H[<Icons.link class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "power"} = assigns),
do: ~H[<Icons.power class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "pause_circle"} = assigns),
do: ~H[<Icons.pause_circle class="h-4 w-4 shrink-0" />]
defp badge_icon(%{name: "play_pause_circle"} = assigns),
do: ~H[<Icons.play_pause_circle class="h-4 w-4 shrink-0" />]
@doc """
An icon-only button that expands to show label on hover. Supports disabled state.
"""
attr :id, :string, required: true
attr :icon, :string, required: true
attr :label, :string, required: true
attr :color, :string, required: true
attr :disabled, :boolean, default: false
attr :confirm, :string, default: nil
attr :tooltip, :string, default: nil
attr :rest, :global
def icon_button(assigns) do
color_classes =
case {assigns.color, assigns.disabled} do
{_, true} ->
"text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 cursor-not-allowed"
{"yellow", false} ->
"text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 cursor-pointer hover:text-yellow-600 hover:border-yellow-600"
{"blue", false} ->
"text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 cursor-pointer hover:text-blue-500 hover:border-blue-600"
{"red", false} ->
"text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 cursor-pointer hover:text-red-500 hover:border-red-600"
{"violet", false} ->
"text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 cursor-pointer hover:text-violet-500 hover:border-violet-600"
end
icon_color =
if assigns.disabled do
"text-gray-400 dark:text-gray-500"
else
case assigns.color do
"yellow" -> "text-gray-500 group-hover:text-yellow-500"
"blue" -> "text-gray-500 group-hover:text-blue-500"
"red" -> "text-gray-500 group-hover:text-red-500"
"violet" -> "text-gray-500 group-hover:text-violet-500"
end
end
assigns = assign(assigns, color_classes: color_classes, icon_color: icon_color)
~H"""
<button
id={@id}
type="button"
disabled={@disabled}
data-confirm={@confirm}
data-title={@tooltip}
phx-hook={if @tooltip, do: "Tippy"}
class={[
"group inline-flex items-center justify-center h-9 pl-2.5 pr-2.5 rounded-md border text-sm font-medium transition-all duration-200",
unless(@disabled, do: "group-hover:pr-3"),
@color_classes
]}
{@rest}
>
<.button_icon name={@icon} class={["h-5 w-5 shrink-0", @icon_color]} />
<span class={[
"overflow-hidden transition-all duration-200 whitespace-nowrap",
if(@disabled, do: "max-w-0", else: "max-w-0 group-hover:max-w-24 group-hover:ml-1.5")
]}>
{@label}
</span>
</button>
"""
end
defp button_icon(%{name: "arrow_path"} = assigns), do: ~H[<Icons.arrow_path class={@class} />]
defp button_icon(%{name: "pause_circle"} = assigns),
do: ~H[<Icons.pause_circle class={@class} />]
defp button_icon(%{name: "pencil_square"} = assigns),
do: ~H[<Icons.pencil_square class={@class} />]
defp button_icon(%{name: "play_circle"} = assigns), do: ~H[<Icons.play_circle class={@class} />]
defp button_icon(%{name: "trash"} = assigns), do: ~H[<Icons.trash class={@class} />]
defp button_icon(%{name: "x_circle"} = assigns), do: ~H[<Icons.x_circle class={@class} />]
# Sparkline
attr :id, :string, required: true
attr :history, :map, required: true
attr :max_value, :integer, default: nil
attr :bar_width, :integer, default: 4
attr :count, :integer, default: 60
attr :gap, :integer, default: 1
attr :height, :integer, default: 16
attr :class, :string, default: nil
def sparkline(assigns) do
history = assigns.history
count = assigns.count
bar_width = assigns.bar_width
gap = assigns.gap
height = assigns.height
max_index = count - 1
max_value =
if assigns.max_value do
max(assigns.max_value, 1)
else
history
|> Map.values()
|> Enum.reduce(1, fn %{count: c}, acc -> max(c, acc) end)
end
now = System.system_time(:millisecond)
{bars, tooltip_data} =
for slot <- 0..max_index, reduce: {[], []} do
{bars_acc, tool_acc} ->
index = max_index - slot
timestamp = now - index * 5 * 1000
x = slot * (bar_width + gap)
case Map.get(history, index) do
%{count: c} ->
bar_height = min(c / max_value, 1.0) * height
bar = %{x: x, height: max(bar_height, 0)}
tooltip = %{timestamp: timestamp, count: c}
{[bar | bars_acc], [tooltip | tool_acc]}
nil ->
tooltip = %{timestamp: timestamp, count: 0}
{bars_acc, [tooltip | tool_acc]}
end
end
bars = Enum.reverse(bars)
tooltip_data = Enum.reverse(tooltip_data)
placeholders =
for slot <- 0..max_index do
%{x: slot * (bar_width + gap)}
end
width = count * (bar_width + gap)
assigns =
assigns
|> assign(bars: bars, placeholders: placeholders, width: width)
|> assign(tooltip_data: tooltip_data)
~H"""
<svg
id={@id}
width={@width}
height={@height}
viewBox={"0 0 #{@width} #{@height}"}
class={["flex-shrink-0", @class]}
phx-hook="QueueSparkline"
data-tooltip={Oban.JSON.encode!(@tooltip_data)}
data-bar-width={@bar_width}
>
<rect
:for={placeholder <- @placeholders}
x={placeholder.x}
y={@height - 2}
width={@bar_width}
height="2"
fill="#e5e7eb"
class="dark:fill-gray-700"
rx="0.5"
/>
<rect
:for={bar <- @bars}
x={bar.x}
y={@height - bar.height}
width={@bar_width}
height={bar.height}
fill="#22d3ee"
rx="1"
/>
</svg>
"""
end
end