mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-02-24 06:48:07 +08:00
* Update phoenix deps
* Update reference to LiveDashboard encode_pid
* Fix form input id references
* Move to HEEx
* Update back to filesystem LV npm package
* Further HEEx rewrites
* Refactor icons into function components
* .html.leex -> .html.heex
* Further refactoring
* Move render helpers into function components
* Add doctype back
* Further refactoring
* Refactor cell component
* Further refactoring
* Compose sidebar using function components
* Rewrite notebook card component as function component
* Fruther refactoring
* Fix race condition in runtime tests
* Rewrite tooltips into function component
* Update Tailwind purge rules
* Revert "Rewrite tooltips into function component"
This reverts commit bd6ca8f0b5
.
* Refactor conditional tooltip
201 lines
6.4 KiB
Elixir
201 lines
6.4 KiB
Elixir
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" aria-label="Refetch">
|
|
<button class="icon-button" 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
|