defmodule LivebookWeb.Output.TableDynamicLive do
use LivebookWeb, :live_view
@limit 10
@loading_delay_ms 100
@impl true
def mount(_params, %{"pid" => pid, "id" => id}, socket) do
send(pid, {:connect, self()})
{:ok,
assign(socket,
id: id,
pid: pid,
loading: true,
show_loading_timer: nil,
# Data specification
page: 1,
limit: @limit,
order_by: nil,
order: :asc,
# Fetched data
name: "Table",
features: [],
columns: [],
rows: [],
total_rows: 0
)}
end
@impl true
def render(%{loading: true} = assigns) do
~L"""
"""
end
def render(assigns) do
~L"""
<%= @name %>
<%= if :refetch in @features do %>
<%= tag :button, class: "icon-button",
phx_click: "refetch" %>
<%= remix_icon("refresh-line", class: "text-xl") %>
<% end %>
<%= if :pagination in @features and @total_rows > 0 do %>
<%= tag :button,
class: "flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300",
disabled: @page == 1,
phx_click: "prev" %>
<%= remix_icon("arrow-left-s-line", class: "text-xl") %>
Prev
<%= @page %> of <%= max_page(@total_rows, @limit) %>
<%= tag :button,
class: "flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300",
disabled: @page == max_page(@total_rows, @limit),
phx_click: "next" %>
Next
<%= remix_icon("arrow-right-s-line", class: "text-xl") %>
<% end %>
<%= if @columns == [] do %>
No data
<% else %>
<%= for {column, idx} <- Enum.with_index(@columns) do %>
| "
phx-click="column_click"
phx-value-column_idx="<%= idx %>">
<%= column.label %>
<%= tag :span, class: unless(@order_by == column.key, do: "invisible") %>
<%= remix_icon(order_icon(@order), class: "text-xl align-middle leading-none") %>
|
<% end %>
<%= for row <- @rows do %>
<%= for column <- @columns do %>
|
<%= to_string(row.fields[column.key]) %>
|
<% end %>
<% end %>
<% end %>
"""
end
defp order_icon(:asc), do: "arrow-up-s-line"
defp order_icon(:desc), do: "arrow-down-s-line"
defp max_page(total_rows, limit) do
ceil(total_rows / limit)
end
@impl true
def handle_event("refetch", %{}, socket) do
{:noreply, request_rows(socket)}
end
def handle_event("prev", %{}, socket) do
{:noreply, assign(socket, :page, socket.assigns.page - 1) |> request_rows()}
end
def handle_event("next", %{}, socket) do
{:noreply, assign(socket, :page, socket.assigns.page + 1) |> request_rows()}
end
def handle_event("column_click", %{"column_idx" => idx}, socket) do
idx = String.to_integer(idx)
%{key: key} = Enum.at(socket.assigns.columns, idx)
{order_by, order} =
case {socket.assigns.order_by, socket.assigns.order} do
{^key, :asc} -> {key, :desc}
{^key, :desc} -> {nil, :asc}
_ -> {key, :asc}
end
{:noreply, assign(socket, order_by: order_by, order: order) |> request_rows()}
end
@impl true
def handle_info({:connect_reply, %{name: name, columns: columns, features: features}}, socket) do
{:noreply, assign(socket, name: name, columns: columns, features: features) |> request_rows()}
end
def handle_info({:rows, %{rows: rows, total_rows: total_rows, columns: columns}}, socket) do
columns =
case columns do
:initial -> socket.assigns.columns
columns when is_list(columns) -> columns
end
if socket.assigns.show_loading_timer do
Process.cancel_timer(socket.assigns.show_loading_timer)
end
{:noreply,
assign(socket,
loading: false,
show_loading_timer: nil,
columns: columns,
rows: rows,
total_rows: total_rows
)}
end
def handle_info(:show_loading, socket) do
{:noreply, assign(socket, loading: true, show_loading_timer: nil)}
end
defp request_rows(socket) do
rows_spec = %{
offset: (socket.assigns.page - 1) * socket.assigns.limit,
limit: socket.assigns.limit,
order_by: socket.assigns.order_by,
order: socket.assigns.order
}
send(socket.assigns.pid, {:get_rows, self(), rows_spec})
show_loading_timer = Process.send_after(self(), :show_loading, @loading_delay_ms)
assign(socket, show_loading_timer: show_loading_timer)
end
end