Add dynamic table output (#356)

* Add dynamic table output

* Support table name
This commit is contained in:
Jonatan Kłosko 2021-06-17 01:02:27 +02:00 committed by GitHub
parent 04c1b36eda
commit b18c1579bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 410 additions and 210 deletions

View file

@ -1,3 +1,4 @@
@layer components {
/* Buttons */ /* Buttons */
.button { .button {
@ -168,3 +169,4 @@
.error-box { .error-box {
@apply mb-3 rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium; @apply mb-3 rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium;
} }
}

View file

@ -1,3 +1,4 @@
@layer utilities {
/* A set of reusable classes */ /* A set of reusable classes */
.bg-editor { .bg-editor {
@ -12,3 +13,9 @@
font-family: "JetBrains Mono", "Droid Sans Mono", "monospace"; font-family: "JetBrains Mono", "Droid Sans Mono", "monospace";
font-size: 14px; font-size: 14px;
} }
.shadow-xl-center {
box-shadow: 0 0 25px -5px rgba(0, 0, 0, 0.1),
0 0 10px -5px rgba(0, 0, 0, 0.04);
}
}

View file

@ -31,6 +31,8 @@ defmodule Livebook.Notebook.Cell.Elixir do
| {:vega_lite_static, spec :: map()} | {:vega_lite_static, spec :: map()}
# Vega-Lite graphic with dynamic data # Vega-Lite graphic with dynamic data
| {:vega_lite_dynamic, widget_process :: pid()} | {:vega_lite_dynamic, widget_process :: pid()}
# Interactive data table
| {:table_dynamic, widget_process :: pid()}
# Internal output format for errors # Internal output format for errors
| {:error, message :: binary()} | {:error, message :: binary()}

View file

@ -0,0 +1,202 @@
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"""
<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
~L"""
<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">
<span class="tooltip left" aria-label="Refetch">
<%= tag :button, class: "icon-button",
phx_click: "refetch" %>
<%= remix_icon("refresh-line", class: "text-xl") %>
</button>
</span>
</div>
<!-- Pagination -->
<%= if :pagination in @features and @total_rows > 0 do %>
<div class="flex space-x-2">
<%= 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") %>
<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>
<%= 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" %>
<span>Next</span>
<%= remix_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">
<table class="w-full">
<thead class="text-left">
<tr class="border-b border-gray-200">
<%= for {column, idx} <- Enum.with_index(@columns) do %>
<th class="py-3 px-6 text-gray-700 font-smibold <%= 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>
<%= tag :span, class: unless(@order_by == column.key, do: "invisible") %>
<%= remix_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">
<%= for column <- @columns do %>
<td class="py-3 px-6">
<%= 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

View file

@ -66,7 +66,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
</div> </div>
<div class="markdown" data-element="markdown-container" id="markdown-container-<%= @cell_view.id %>" phx-update="ignore"> <div class="markdown" data-element="markdown-container" id="markdown-container-<%= @cell_view.id %>" phx-update="ignore">
<%= render_markdown_content_placeholder(empty: @cell_view.empty?) %> <%= render_content_placeholder("bg-gray-200", @cell_view.empty?) %>
</div> </div>
</div> </div>
</div> </div>
@ -217,7 +217,9 @@ defmodule LivebookWeb.SessionLive.CellComponent do
id="editor-container-<%= @cell_view.id %>" id="editor-container-<%= @cell_view.id %>"
data-element="editor-container" data-element="editor-container"
phx-update="ignore"> phx-update="ignore">
<%= render_editor_content_placeholder(empty: @cell_view.empty?) %> <div class="px-8">
<%= render_content_placeholder("bg-gray-500", @cell_view.empty?) %>
</div>
</div> </div>
<%= if @cell_view.type == :elixir do %> <%= if @cell_view.type == :elixir do %>
@ -243,7 +245,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
# There may be a tiny delay before the markdown is rendered # There may be a tiny delay before the markdown is rendered
# or editors are mounted, so show neat placeholders immediately. # or editors are mounted, so show neat placeholders immediately.
defp render_markdown_content_placeholder(empty: true) do defp render_content_placeholder(_bg_class, true = _empty) do
assigns = %{} assigns = %{}
~L""" ~L"""
@ -251,37 +253,15 @@ defmodule LivebookWeb.SessionLive.CellComponent do
""" """
end end
defp render_markdown_content_placeholder(empty: false) do defp render_content_placeholder(bg_class, false = _empty) do
assigns = %{} assigns = %{bg_class: bg_class}
~L""" ~L"""
<div class="max-w-2xl w-full animate-pulse"> <div class="max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4"> <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_class %> rounded-lg w-3/4"></div>
<div class="h-4 bg-gray-200 rounded-lg"></div> <div class="h-4 <%= @bg_class %> rounded-lg"></div>
<div class="h-4 bg-gray-200 rounded-lg w-5/6"></div> <div class="h-4 <%= @bg_class %> rounded-lg w-5/6"></div>
</div>
</div>
"""
end
defp render_editor_content_placeholder(empty: true) do
assigns = %{}
~L"""
<div class="h-4"></div>
"""
end
defp render_editor_content_placeholder(empty: false) do
assigns = %{}
~L"""
<div class="px-8 max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4 py-1">
<div class="h-4 bg-gray-500 rounded-lg w-3/4"></div>
<div class="h-4 bg-gray-500 rounded-lg"></div>
<div class="h-4 bg-gray-500 rounded-lg w-5/6"></div>
</div> </div>
</div> </div>
""" """
@ -289,7 +269,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
defp render_outputs(assigns, socket) do defp render_outputs(assigns, socket) do
~L""" ~L"""
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200 font-editor"> <div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200">
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %> <%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar"> <div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
<%= render_output(socket, output, "cell-#{@cell_view.id}-output#{index}") %> <%= render_output(socket, output, "cell-#{@cell_view.id}-output#{index}") %>
@ -322,6 +302,13 @@ defmodule LivebookWeb.SessionLive.CellComponent do
) )
end end
defp render_output(socket, {:table_dynamic, pid}, id) do
live_render(socket, LivebookWeb.Output.TableDynamicLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output(_socket, {:error, formatted}, _id) do defp render_output(_socket, {:error, formatted}, _id) do
render_error_message_output(formatted) render_error_message_output(formatted)
end end
@ -348,7 +335,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<div><%= line %></div> <div><%= line %></div>
<% end %> <% end %>
</div> </div>
<div data-content class="overflow-auto whitespace-pre text-gray-500 tiny-scrollbar" <div data-content class="overflow-auto whitespace-pre font-editor text-gray-500 tiny-scrollbar"
id="<%= @id %>-content" id="<%= @id %>-content"
phx-update="ignore"></div> phx-update="ignore"></div>
</div> </div>