mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-19 18:06:16 +08:00
Add dynamic table output (#356)
* Add dynamic table output * Support table name
This commit is contained in:
parent
04c1b36eda
commit
b18c1579bd
5 changed files with 410 additions and 210 deletions
|
@ -1,170 +1,172 @@
|
|||
/* Buttons */
|
||||
@layer components {
|
||||
/* Buttons */
|
||||
|
||||
.button {
|
||||
@apply px-5 py-2 rounded-lg border border-transparent font-medium text-sm;
|
||||
}
|
||||
|
||||
.button-blue {
|
||||
@apply border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:bg-blue-700;
|
||||
}
|
||||
|
||||
.button-red {
|
||||
@apply border-transparent bg-red-600 text-white hover:bg-red-700 focus:bg-red-700;
|
||||
}
|
||||
|
||||
.button-gray {
|
||||
@apply border-gray-200 bg-gray-100 text-gray-600 hover:bg-gray-200 focus:bg-gray-200;
|
||||
}
|
||||
|
||||
.button-outlined-blue {
|
||||
@apply bg-blue-50 border-blue-600 text-blue-600 hover:bg-blue-100 focus:bg-blue-100;
|
||||
}
|
||||
|
||||
.button-outlined-red {
|
||||
@apply bg-red-50 border-red-600 text-red-600 hover:bg-red-100 focus:bg-red-100;
|
||||
}
|
||||
|
||||
.button-outlined-gray {
|
||||
@apply bg-white border-gray-300 text-gray-600 hover:bg-gray-100 focus:bg-gray-100;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
@apply cursor-default pointer-events-none border-transparent bg-gray-100 text-gray-400;
|
||||
}
|
||||
|
||||
.button-small {
|
||||
@apply px-2 py-1 bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100 focus:bg-gray-100;
|
||||
}
|
||||
|
||||
.button-square-icon {
|
||||
@apply p-2 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.button-square-icon i {
|
||||
@apply text-xl leading-none;
|
||||
}
|
||||
|
||||
.choice-button {
|
||||
@apply px-5 py-2 rounded-lg border text-gray-700 bg-white border-gray-200;
|
||||
}
|
||||
|
||||
.choice-button.active {
|
||||
@apply bg-blue-100 border-blue-600;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
@apply p-1 flex items-center justify-center text-gray-400 hover:text-gray-800;
|
||||
}
|
||||
|
||||
.icon-button:focus {
|
||||
@apply rounded-full bg-gray-100;
|
||||
}
|
||||
|
||||
.icon-button i {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-outlined-button {
|
||||
@apply rounded-full border-2;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
|
||||
.input {
|
||||
@apply w-full px-3 py-2 bg-gray-50 text-sm border border-gray-200 rounded-lg placeholder-gray-400 text-gray-600;
|
||||
}
|
||||
|
||||
.input--error {
|
||||
@apply bg-red-50 border-red-600 text-red-600;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
@apply mb-0.5 text-sm text-gray-800 font-medium;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
@apply mt-2 text-red-600 italic text-xs;
|
||||
}
|
||||
|
||||
.switch-button {
|
||||
@apply relative inline-block w-14 h-7 mr-2 select-none transition;
|
||||
}
|
||||
|
||||
.switch-button__checkbox {
|
||||
@apply appearance-none absolute block w-7 h-7 rounded-full bg-gray-400 border-[5px] border-gray-100 cursor-pointer transition-all duration-300;
|
||||
}
|
||||
|
||||
.switch-button__bg {
|
||||
@apply block h-full w-full rounded-full bg-gray-100 cursor-pointer transition-all duration-300;
|
||||
}
|
||||
|
||||
.switch-button__checkbox:checked {
|
||||
@apply bg-white border-blue-600;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.switch-button__checkbox:checked + .switch-button__bg {
|
||||
@apply bg-blue-600;
|
||||
}
|
||||
|
||||
.radio-base {
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='10' cy='10' r='9.5' stroke='%23CAD5E0' fill='white' /%3e%3c/svg%3e");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.radio-base:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='10' cy='10' r='9.5' stroke='%233E64FF' fill='white' /%3e%3ccircle cx='10' cy='10' r='6' fill='%233E64FF' /%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* Custom scrollbars */
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.25rem;
|
||||
@apply bg-gray-400;
|
||||
}
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar-track {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
|
||||
.tabs {
|
||||
@apply w-full flex;
|
||||
}
|
||||
|
||||
.tabs .tab {
|
||||
@apply flex items-center space-x-2 px-3 py-2 border-b-2 text-gray-400 border-gray-100;
|
||||
}
|
||||
|
||||
.tabs .tab.active {
|
||||
@apply text-blue-600 border-blue-600;
|
||||
}
|
||||
|
||||
/* Toggleable menu */
|
||||
|
||||
.menu {
|
||||
@apply absolute right-0 z-20 rounded-lg bg-white flex flex-col py-2;
|
||||
box-shadow: 0px 15px 99px rgba(13, 24, 41, 0.15);
|
||||
}
|
||||
|
||||
.menu__item {
|
||||
@apply flex space-x-3 px-5 py-2 items-center hover:bg-gray-50 whitespace-nowrap;
|
||||
}
|
||||
|
||||
/* Boxes */
|
||||
|
||||
.error-box {
|
||||
@apply mb-3 rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium;
|
||||
.button {
|
||||
@apply px-5 py-2 rounded-lg border border-transparent font-medium text-sm;
|
||||
}
|
||||
|
||||
.button-blue {
|
||||
@apply border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:bg-blue-700;
|
||||
}
|
||||
|
||||
.button-red {
|
||||
@apply border-transparent bg-red-600 text-white hover:bg-red-700 focus:bg-red-700;
|
||||
}
|
||||
|
||||
.button-gray {
|
||||
@apply border-gray-200 bg-gray-100 text-gray-600 hover:bg-gray-200 focus:bg-gray-200;
|
||||
}
|
||||
|
||||
.button-outlined-blue {
|
||||
@apply bg-blue-50 border-blue-600 text-blue-600 hover:bg-blue-100 focus:bg-blue-100;
|
||||
}
|
||||
|
||||
.button-outlined-red {
|
||||
@apply bg-red-50 border-red-600 text-red-600 hover:bg-red-100 focus:bg-red-100;
|
||||
}
|
||||
|
||||
.button-outlined-gray {
|
||||
@apply bg-white border-gray-300 text-gray-600 hover:bg-gray-100 focus:bg-gray-100;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
@apply cursor-default pointer-events-none border-transparent bg-gray-100 text-gray-400;
|
||||
}
|
||||
|
||||
.button-small {
|
||||
@apply px-2 py-1 bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100 focus:bg-gray-100;
|
||||
}
|
||||
|
||||
.button-square-icon {
|
||||
@apply p-2 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.button-square-icon i {
|
||||
@apply text-xl leading-none;
|
||||
}
|
||||
|
||||
.choice-button {
|
||||
@apply px-5 py-2 rounded-lg border text-gray-700 bg-white border-gray-200;
|
||||
}
|
||||
|
||||
.choice-button.active {
|
||||
@apply bg-blue-100 border-blue-600;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
@apply p-1 flex items-center justify-center text-gray-400 hover:text-gray-800;
|
||||
}
|
||||
|
||||
.icon-button:focus {
|
||||
@apply rounded-full bg-gray-100;
|
||||
}
|
||||
|
||||
.icon-button i {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-outlined-button {
|
||||
@apply rounded-full border-2;
|
||||
}
|
||||
|
||||
/* Form fields */
|
||||
|
||||
.input {
|
||||
@apply w-full px-3 py-2 bg-gray-50 text-sm border border-gray-200 rounded-lg placeholder-gray-400 text-gray-600;
|
||||
}
|
||||
|
||||
.input--error {
|
||||
@apply bg-red-50 border-red-600 text-red-600;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
@apply mb-0.5 text-sm text-gray-800 font-medium;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
@apply mt-2 text-red-600 italic text-xs;
|
||||
}
|
||||
|
||||
.switch-button {
|
||||
@apply relative inline-block w-14 h-7 mr-2 select-none transition;
|
||||
}
|
||||
|
||||
.switch-button__checkbox {
|
||||
@apply appearance-none absolute block w-7 h-7 rounded-full bg-gray-400 border-[5px] border-gray-100 cursor-pointer transition-all duration-300;
|
||||
}
|
||||
|
||||
.switch-button__bg {
|
||||
@apply block h-full w-full rounded-full bg-gray-100 cursor-pointer transition-all duration-300;
|
||||
}
|
||||
|
||||
.switch-button__checkbox:checked {
|
||||
@apply bg-white border-blue-600;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.switch-button__checkbox:checked + .switch-button__bg {
|
||||
@apply bg-blue-600;
|
||||
}
|
||||
|
||||
.radio-base {
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='10' cy='10' r='9.5' stroke='%23CAD5E0' fill='white' /%3e%3c/svg%3e");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.radio-base:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='10' cy='10' r='9.5' stroke='%233E64FF' fill='white' /%3e%3ccircle cx='10' cy='10' r='6' fill='%233E64FF' /%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* Custom scrollbars */
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.25rem;
|
||||
@apply bg-gray-400;
|
||||
}
|
||||
|
||||
.tiny-scrollbar::-webkit-scrollbar-track {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
|
||||
.tabs {
|
||||
@apply w-full flex;
|
||||
}
|
||||
|
||||
.tabs .tab {
|
||||
@apply flex items-center space-x-2 px-3 py-2 border-b-2 text-gray-400 border-gray-100;
|
||||
}
|
||||
|
||||
.tabs .tab.active {
|
||||
@apply text-blue-600 border-blue-600;
|
||||
}
|
||||
|
||||
/* Toggleable menu */
|
||||
|
||||
.menu {
|
||||
@apply absolute right-0 z-20 rounded-lg bg-white flex flex-col py-2;
|
||||
box-shadow: 0px 15px 99px rgba(13, 24, 41, 0.15);
|
||||
}
|
||||
|
||||
.menu__item {
|
||||
@apply flex space-x-3 px-5 py-2 items-center hover:bg-gray-50 whitespace-nowrap;
|
||||
}
|
||||
|
||||
/* Boxes */
|
||||
|
||||
.error-box {
|
||||
@apply mb-3 rounded-lg px-4 py-2 bg-red-100 text-red-400 font-medium;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
/* A set of reusable classes */
|
||||
@layer utilities {
|
||||
/* A set of reusable classes */
|
||||
|
||||
.bg-editor {
|
||||
background-color: #282c34;
|
||||
}
|
||||
.bg-editor {
|
||||
background-color: #282c34;
|
||||
}
|
||||
|
||||
.text-editor {
|
||||
color: #abb2bf;
|
||||
}
|
||||
.text-editor {
|
||||
color: #abb2bf;
|
||||
}
|
||||
|
||||
.font-editor {
|
||||
font-family: "JetBrains Mono", "Droid Sans Mono", "monospace";
|
||||
font-size: 14px;
|
||||
.font-editor {
|
||||
font-family: "JetBrains Mono", "Droid Sans Mono", "monospace";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ defmodule Livebook.Notebook.Cell.Elixir do
|
|||
| {:vega_lite_static, spec :: map()}
|
||||
# Vega-Lite graphic with dynamic data
|
||||
| {:vega_lite_dynamic, widget_process :: pid()}
|
||||
# Interactive data table
|
||||
| {:table_dynamic, widget_process :: pid()}
|
||||
# Internal output format for errors
|
||||
| {:error, message :: binary()}
|
||||
|
||||
|
|
202
lib/livebook_web/live/output/table_dynamic_live.ex
Normal file
202
lib/livebook_web/live/output/table_dynamic_live.ex
Normal 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
|
|
@ -66,7 +66,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
</div>
|
||||
|
||||
<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>
|
||||
|
@ -217,7 +217,9 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
id="editor-container-<%= @cell_view.id %>"
|
||||
data-element="editor-container"
|
||||
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>
|
||||
|
||||
<%= 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
|
||||
# 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 = %{}
|
||||
|
||||
~L"""
|
||||
|
@ -251,37 +253,15 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp render_markdown_content_placeholder(empty: false) do
|
||||
assigns = %{}
|
||||
defp render_content_placeholder(bg_class, false = _empty) do
|
||||
assigns = %{bg_class: bg_class}
|
||||
|
||||
~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
|
||||
|
||||
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 class="h-4 <%= @bg_class %> rounded-lg w-3/4"></div>
|
||||
<div class="h-4 <%= @bg_class %> rounded-lg"></div>
|
||||
<div class="h-4 <%= @bg_class %> rounded-lg w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
@ -289,7 +269,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
defp render_outputs(assigns, socket) do
|
||||
~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 %>
|
||||
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
|
||||
<%= render_output(socket, output, "cell-#{@cell_view.id}-output#{index}") %>
|
||||
|
@ -322,6 +302,13 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
)
|
||||
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
|
||||
render_error_message_output(formatted)
|
||||
end
|
||||
|
@ -348,7 +335,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<div><%= line %></div>
|
||||
<% end %>
|
||||
</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"
|
||||
phx-update="ignore"></div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue