Packages
livebook
0.3.0
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/session_live/cell_component.ex
defmodule LivebookWeb.SessionLive.CellComponent do
use LivebookWeb, :live_component
def render(assigns) do
~H"""
<div class="flex flex-col relative"
data-element="cell"
id={"cell-#{@cell_view.id}"}
phx-hook="Cell"
data-cell-id={@cell_view.id}
data-type={@cell_view.type}
data-session-path={Routes.session_path(@socket, :page, @session_id)}>
<%= render_cell(assigns) %>
</div>
"""
end
def render_cell(%{cell_view: %{type: :markdown}} = assigns) do
~H"""
<div class="mb-1 flex items-center justify-end">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
<.cell_link_button cell_id={@cell_view.id} />
<span class="tooltip top" aria-label="Edit content" data-element="enable-insert-mode-button">
<button class="icon-button">
<.remix_icon icon="pencil-line" class="text-xl" />
</button>
</span>
<span class="tooltip top" aria-label="Insert image" data-element="insert-image-button">
<%= live_patch to: Routes.session_path(@socket, :cell_upload, @session_id, @cell_view.id),
class: "icon-button" do %>
<.remix_icon icon="image-add-line" class="text-xl" />
<% end %>
</span>
<.move_cell_up_button cell_id={@cell_view.id} />
<.move_cell_down_button cell_id={@cell_view.id} />
<.delete_cell_button cell_id={@cell_view.id} />
</div>
</div>
<.cell_body>
<div class="pb-4" data-element="editor-box">
<.editor cell_view={@cell_view} />
</div>
<div class="markdown"
data-element="markdown-container"
id={"markdown-container-#{@cell_view.id}"}
phx-update="ignore">
<.content_placeholder bg_class="bg-gray-200" empty={@cell_view.empty?} />
</div>
</.cell_body>
"""
end
def render_cell(%{cell_view: %{type: :elixir}} = assigns) do
~H"""
<div class="mb-1 flex items-center justify-between">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions" data-primary>
<%= if @cell_view.evaluation_status == :ready do %>
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="queue_cell_evaluation"
phx-value-cell_id={@cell_view.id}>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
<%= if(@cell_view.validity_status == :evaluated, do: "Reevaluate", else: "Evaluate") %>
</span>
</button>
<% else %>
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="cancel_cell_evaluation"
phx-value-cell_id={@cell_view.id}>
<.remix_icon icon="stop-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
Stop
</span>
</button>
<% end %>
</div>
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
<.cell_link_button cell_id={@cell_view.id} />
<.cell_settings_button cell_id={@cell_view.id} socket={@socket} session_id={@session_id} />
<.move_cell_up_button cell_id={@cell_view.id} />
<.move_cell_down_button cell_id={@cell_view.id} />
<.delete_cell_button cell_id={@cell_view.id} />
</div>
</div>
<.cell_body>
<.editor cell_view={@cell_view} />
<%= if @cell_view.outputs != [] do %>
<div class="mt-2">
<.outputs cell_view={@cell_view} socket={@socket} />
</div>
<% end %>
</.cell_body>
"""
end
def render_cell(%{cell_view: %{type: :input}} = assigns) do
~H"""
<div class="mb-1 flex items-center justify-end">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
<.cell_link_button cell_id={@cell_view.id} />
<.cell_settings_button cell_id={@cell_view.id} socket={@socket} session_id={@session_id} />
<.move_cell_up_button cell_id={@cell_view.id} />
<.move_cell_down_button cell_id={@cell_view.id} />
<.delete_cell_button cell_id={@cell_view.id} />
</div>
</div>
<.cell_body>
<form phx-change="set_cell_value" phx-submit="queue_bound_cells_evaluation">
<input type="hidden" name="cell_id" value={@cell_view.id} />
<div class="input-label">
<%= @cell_view.name %>
</div>
<.cell_input cell_view={@cell_view} />
<%= if @cell_view.error do %>
<div class="input-error">
<%= String.capitalize(@cell_view.error) %>
</div>
<% end %>
</form>
</.cell_body>
"""
end
defp cell_input(%{cell_view: %{input_type: :textarea}} = assigns) do
~H"""
<textarea
data-element="input"
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
name="value"
spellcheck="false"
tabindex="-1"><%= [?\n, @cell_view.value] %></textarea>
"""
end
defp cell_input(%{cell_view: %{input_type: :range}} = assigns) do
~H"""
<div class="flex items-center space-x-2">
<div><%= @cell_view.props.min %></div>
<input type="range"
data-element="input"
class="input-range"
name="value"
value={@cell_view.value}
phx-debounce="300"
spellcheck="false"
autocomplete="off"
tabindex="-1"
min={@cell_view.props.min}
max={@cell_view.props.max}
step={@cell_view.props.step} />
<div><%= @cell_view.props.max %></div>
</div>
"""
end
defp cell_input(%{cell_view: %{input_type: :select}} = assigns) do
~H"""
<div class="flex items-center space-x-2">
<select
data-element="input"
spellcheck="false"
phx-debounce="300"
class="input input-select"
tabindex="-1"
name="value">
<%= for option <- @cell_view.props.options do %>
<option value={option} selected={option == @cell_view.value}>
<%= option %>
</option>
<% end %>
</select>
</div>
"""
end
defp cell_input(%{cell_view: %{input_type: :checkbox}} = assigns) do
~H"""
<div class="mt-1">
<.switch_checkbox
data-element="input"
name="value"
checked={@cell_view.value == "true"} />
</div>
"""
end
defp cell_input(assigns) do
~H"""
<input type={html_input_type(@cell_view.input_type)}
data-element="input"
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
name="value"
value={@cell_view.value}
phx-debounce="300"
spellcheck="false"
autocomplete="off"
tabindex="-1" />
"""
end
defp html_input_type(:password), do: "password"
defp html_input_type(:number), do: "number"
defp html_input_type(:color), do: "color"
defp html_input_type(:range), do: "range"
defp html_input_type(:select), do: "select"
defp html_input_type(_), do: "text"
defp cell_body(assigns) do
~H"""
<div class="flex relative">
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
</div>
<div class="w-full">
<%= render_block(@inner_block) %>
</div>
</div>
"""
end
defp cell_link_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Link">
<a href={"#cell-#{@cell_id}"} class="icon-button">
<.remix_icon icon="link" class="text-xl" />
</a>
</span>
"""
end
defp cell_settings_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Cell settings">
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_id), class: "icon-button" do %>
<.remix_icon icon="settings-3-line" class="text-xl" />
<% end %>
</span>
"""
end
defp move_cell_up_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id={@cell_id}
phx-value-offset="-1">
<.remix_icon icon="arrow-up-s-line" class="text-xl" />
</button>
</span>
"""
end
defp move_cell_down_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id={@cell_id}
phx-value-offset="1">
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
</button>
</span>
"""
end
defp delete_cell_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Delete">
<button class="icon-button"
phx-click="delete_cell"
phx-value-cell_id={@cell_id}>
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
</button>
</span>
"""
end
defp editor(assigns) do
~H"""
<div class="py-3 rounded-lg bg-editor relative">
<div
id={"editor-container-#{@cell_view.id}"}
data-element="editor-container"
phx-update="ignore">
<div class="px-8">
<.content_placeholder bg_class="bg-gray-500" empty={@cell_view.empty?} />
</div>
</div>
<%= if @cell_view.type == :elixir do %>
<div class="absolute bottom-2 right-2">
<.cell_status cell_view={@cell_view} />
</div>
<% end %>
</div>
"""
end
# The whole page has to load and then hooks are mounted.
# There may be a tiny delay before the markdown is rendered
# or editors are mounted, so show neat placeholders immediately.
defp content_placeholder(assigns) do
~H"""
<%= if @empty do %>
<div class="h-4"></div>
<% else %>
<div class="max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4">
<div class={"#{@bg_class} h-4 rounded-lg w-3/4"}></div>
<div class={"#{@bg_class} h-4 rounded-lg"}></div>
<div class={"#{@bg_class} h-4 rounded-lg w-5/6"}></div>
</div>
</div>
<% end %>
"""
end
defp cell_status(%{cell_view: %{evaluation_status: :evaluating}} = assigns) do
~H"""
<.status_indicator circle_class="bg-blue-500" animated_circle_class="bg-blue-400" change_indicator={true}>
<span class="font-mono"
id={"cell-timer-#{@cell_view.id}-evaluation-#{@cell_view.number_of_evaluations}"}
phx-hook="Timer"
phx-update="ignore">
</span>
</.status_indicator>
"""
end
defp cell_status(%{cell_view: %{evaluation_status: :queued}} = assigns) do
~H"""
<.status_indicator circle_class="bg-gray-500" animated_circle_class="bg-gray-400">
Queued
</.status_indicator>
"""
end
defp cell_status(%{cell_view: %{validity_status: :evaluated}} = assigns) do
~H"""
<.status_indicator
circle_class="bg-green-400"
change_indicator={true}
tooltip={evaluated_label(@cell_view.evaluation_time_ms)}>
Evaluated
</.status_indicator>
"""
end
defp cell_status(%{cell_view: %{validity_status: :stale}} = assigns) do
~H"""
<.status_indicator circle_class="bg-yellow-200" change_indicator={true}>
Stale
</.status_indicator>
"""
end
defp cell_status(%{cell_view: %{validity_status: :aborted}} = assigns) do
~H"""
<.status_indicator circle_class="bg-red-400">
Aborted
</.status_indicator>
"""
end
defp cell_status(assigns), do: ~H""
defp status_indicator(assigns) do
assigns =
assigns
|> assign_new(:animated_circle_class, fn -> nil end)
|> assign_new(:change_indicator, fn -> false end)
|> assign_new(:tooltip, fn -> nil end)
~H"""
<div class={"#{if(@tooltip, do: "tooltip")} bottom distant-medium"} aria-label={@tooltip}>
<div class="flex items-center space-x-1">
<div class="flex text-xs text-gray-400">
<%= render_block(@inner_block) %>
<%= if @change_indicator do %>
<span data-element="change-indicator">*</span>
<% end %>
</div>
<span class="flex relative h-3 w-3">
<%= if @animated_circle_class do %>
<span class={"#{@animated_circle_class} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"}></span>
<% end %>
<span class={"#{@circle_class} relative inline-flex rounded-full h-3 w-3"}></span>
</span>
</div>
</div>
"""
end
defp evaluated_label(time_ms) when is_integer(time_ms) do
evaluation_time =
if time_ms > 100 do
seconds = time_ms |> Kernel./(1000) |> Float.floor(1)
"#{seconds}s"
else
"#{time_ms}ms"
end
"Took " <> evaluation_time
end
defp evaluated_label(_time_ms), do: nil
# Outputs
defp outputs(assigns) do
~H"""
<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(output, %{id: "cell-#{@cell_view.id}-evaluation#{@cell_view.number_of_evaluations}-output#{index}", socket: @socket}) %>
</div>
<% end %>
</div>
"""
end
defp render_output(text, %{id: id}) when is_binary(text) do
# Captured output usually has a trailing newline that we can ignore,
# because each line is itself an HTML block anyway.
text = String.replace_suffix(text, "\n", "")
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: true)
end
defp render_output({:text, text}, %{id: id}) do
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false)
end
defp render_output({:markdown, markdown}, %{id: id}) do
live_component(LivebookWeb.Output.MarkdownComponent, id: id, content: markdown)
end
defp render_output({:image, content, mime_type}, %{id: id}) do
live_component(LivebookWeb.Output.ImageComponent,
id: id,
content: content,
mime_type: mime_type
)
end
defp render_output({:vega_lite_static, spec}, %{id: id}) do
live_component(LivebookWeb.Output.VegaLiteStaticComponent, id: id, spec: spec)
end
defp render_output({:vega_lite_dynamic, pid}, %{id: id, socket: socket}) do
live_render(socket, LivebookWeb.Output.VegaLiteDynamicLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output({:table_dynamic, pid}, %{id: id, socket: socket}) do
live_render(socket, LivebookWeb.Output.TableDynamicLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output({:error, formatted, :runtime_restart_required}, %{}) do
assigns = %{formatted: formatted}
~H"""
<div class="flex flex-col space-y-4">
<%= render_error_message_output(@formatted) %>
<div>
<button class="button button-gray" phx-click="restart_runtime">
Restart runtime
</button>
</div>
</div>
"""
end
defp render_output({:error, formatted, _type}, %{}) do
render_error_message_output(formatted)
end
defp render_output(output, %{}) do
render_error_message_output("""
Unknown output format: #{inspect(output)}. If you're using Kino,
you may want to update Kino and Livebook to the latest version.
""")
end
defp render_error_message_output(message) do
assigns = %{message: message}
~H"""
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
"""
end
end