Packages
livebook
0.3.2
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/output/table_dynamic_live.ex
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
~H"""
<div class="max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4">
<div class="h-4 bg-gray-200 rounded-lg w-3/4"></div>
<div class="h-4 bg-gray-200 rounded-lg"></div>
<div class="h-4 bg-gray-200 rounded-lg w-5/6"></div>
</div>
</div>
"""
end
def render(assigns) do
~H"""
<div class="mb-4 flex items-center space-x-3">
<h3 class="font-semibold text-gray-800">
<%= @name %>
</h3>
<div class="flex-grow"></div>
<!-- Actions -->
<div class="flex space-x-2">
<%= if :refetch in @features do %>
<span class="tooltip left" data-tooltip="Refetch">
<button class="icon-button" aria-label="refresh" phx-click="refetch">
<.remix_icon icon="refresh-line" class="text-xl" />
</button>
</span>
<% end %>
</div>
<!-- Pagination -->
<%= if :pagination in @features and @total_rows > 0 do %>
<div class="flex space-x-2">
<button class="flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300"
phx-click="prev"
disabled={@page == 1}>
<.remix_icon icon="arrow-left-s-line" class="text-xl" />
<span>Prev</span>
</button>
<div class="flex items-center px-3 py-1 rounded-lg border border-gray-300 font-medium text-sm text-gray-400">
<span><%= @page %> of <%= max_page(@total_rows, @limit) %></span>
</div>
<button class="flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300"
phx-click="next"
disabled={@page == max_page(@total_rows, @limit)}>
<span>Next</span>
<.remix_icon icon="arrow-right-s-line" class="text-xl" />
</button>
</div>
<% end %>
</div>
<%= if @columns == [] do %>
<!-- In case we don't have information about table structure yet -->
<p class="text-gray-700">
No data
</p>
<% else %>
<!-- Data table -->
<div class="shadow-xl-center rounded-lg max-w-full overflow-y-auto tiny-scrollbar">
<table class="w-full">
<thead class="text-left">
<tr class="border-b border-gray-200 whitespace-nowrap">
<%= for {column, idx} <- Enum.with_index(@columns) do %>
<th class={"py-3 px-6 text-gray-700 font-semibold #{if(:sorting in @features, do: "cursor-pointer", else: "pointer-events-none")}"}
phx-click="column_click"
phx-value-column_idx={idx}>
<div class="flex items-center space-x-1">
<span><%= column.label %></span>
<span class={unless(@order_by == column.key, do: "invisible")}>
<.remix_icon icon={order_icon(@order)} class="text-xl align-middle leading-none" />
</span>
</div>
</th>
<% end %>
</tr>
</thead>
<tbody class="text-gray-500">
<%= for row <- @rows do %>
<tr class="border-b border-gray-200 last:border-b-0 hover:bg-gray-50 whitespace-nowrap">
<%= for column <- @columns do %>
<td class="py-3 px-6">
<%= to_string(row.fields[column.key]) %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% 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