Packages
livebook
0.15.3
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
import LivebookWeb.NotebookComponents
@impl true
def mount(socket) do
{:ok, stream(socket, :outputs, [])}
end
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
socket =
case assigns.cell_view do
%{eval: %{outputs: outputs}} ->
stream_items =
for {idx, output} <- Enum.reverse(outputs) do
%{id: Integer.to_string(idx), idx: idx, output: output}
end
stream(socket, :outputs, stream_items)
%{} ->
socket
end
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div
class="flex flex-col relative scroll-mt-[50px] sm:scroll-mt-0"
data-el-cell
id={"cell-#{@cell_view.id}"}
data-type={@cell_view.type}
data-focusable-id={@cell_view.id}
data-js-empty={@cell_view.empty}
data-eval-validity={get_in(@cell_view, [:eval, :validity])}
data-eval-errored={get_in(@cell_view, [:eval, :errored])}
phx-hook="Cell"
data-p-cell-id={hook_prop(@cell_view.id)}
data-p-type={hook_prop(@cell_view.type)}
data-p-session-path={hook_prop(~p"/sessions/#{@session_id}")}
data-p-evaluation-digest={hook_prop(get_in(@cell_view, [:eval, :evaluation_digest]))}
data-p-smart-cell-js-view-ref={hook_prop(smart_cell_js_view_ref(@cell_view))}
data-p-allowed-uri-schemes={hook_prop(@allowed_uri_schemes)}
>
{render_cell(assigns)}
</div>
"""
end
defp render_cell(%{cell_view: %{type: :markdown}} = assigns) do
~H"""
<.cell_actions>
<:secondary>
<.enable_insert_mode_button />
<.cell_link_button cell_id={@cell_view.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} />
</:secondary>
</.cell_actions>
<.cell_body>
<div class="pb-4" data-el-editor-box>
<.cell_editor
cell_id={@cell_view.id}
tag="primary"
empty={@cell_view.empty}
language="markdown"
/>
</div>
<div
class="markdown break-words"
data-el-markdown-container
id={"markdown-container-#{@cell_view.id}"}
phx-update="ignore"
>
<.content_skeleton empty={@cell_view.empty} />
</div>
</.cell_body>
"""
end
defp render_cell(%{cell_view: %{type: :code}} = assigns) do
~H"""
<.cell_actions>
<:primary>
<.cell_evaluation_button
session_id={@session_id}
cell_id={@cell_view.id}
validity={@cell_view.eval.validity}
status={@cell_view.eval.status}
reevaluate_automatically={@cell_view.reevaluate_automatically}
reevaluates_automatically={@cell_view.eval.reevaluates_automatically}
/>
</:primary>
<:secondary>
<.cell_settings_button cell_id={@cell_view.id} session_id={@session_id} />
<.amplify_output_button />
<.cell_link_button cell_id={@cell_view.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} />
</:secondary>
</.cell_actions>
<.cell_body>
<div class="relative" data-el-cell-body-root>
<div class="relative" data-el-editor-box>
<.cell_editor
cell_id={@cell_view.id}
tag="primary"
empty={@cell_view.empty}
language={@cell_view.language}
intellisense
/>
</div>
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
</div>
</div>
<.doctest_summary cell_id={@cell_view.id} doctest_summary={@cell_view.eval.doctest_summary} />
<.evaluation_outputs
outputs={@streams.outputs}
cell_view={@cell_view}
session_id={@session_id}
session_pid={@session_pid}
client_id={@client_id}
/>
</.cell_body>
"""
end
defp render_cell(%{cell_view: %{type: :setup}} = assigns) do
~H"""
<.cell_actions>
<:primary>
<.setup_cell_evaluation_button
cell_id={@cell_view.id}
validity={@cell_view.eval.validity}
status={@cell_view.eval.status}
runtime={@runtime}
/>
</:primary>
<:secondary>
<.package_search_button session_id={@session_id} runtime={@runtime} />
<.cell_link_button cell_id={@cell_view.id} />
<.setup_cell_info />
</:secondary>
</.cell_actions>
<.cell_body>
<div class="relative" data-el-cell-body-root>
<div data-el-info-box>
<div class="py-2 px-3 flex items-center justify-between border border-gray-200 text-sm text-gray-400 rounded-lg">
<span class="font-medium">Notebook dependencies and setup</span>
</div>
</div>
<div data-el-editor-box>
<.cell_editor
cell_id={@cell_view.id}
tag="primary"
empty={@cell_view.empty}
language="elixir"
intellisense
/>
</div>
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
</div>
</div>
<.evaluation_outputs
outputs={@streams.outputs}
cell_view={@cell_view}
session_id={@session_id}
session_pid={@session_pid}
client_id={@client_id}
/>
</.cell_body>
"""
end
defp render_cell(%{cell_view: %{type: :smart}} = assigns) do
~H"""
<.cell_actions>
<:primary>
<.cell_evaluation_button
session_id={@session_id}
cell_id={@cell_view.id}
validity={@cell_view.eval.validity}
status={@cell_view.eval.status}
reevaluate_automatically={false}
reevaluates_automatically={@cell_view.eval.reevaluates_automatically}
/>
</:primary>
<:secondary>
<.toggle_source_button />
<.convert_smart_cell_button cell_id={@cell_view.id} />
<.amplify_output_button />
<.cell_link_button cell_id={@cell_view.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} />
</:secondary>
</.cell_actions>
<.cell_body>
<div class="relative" data-el-cell-body-root>
<div data-el-ui-box>
<%= case @cell_view.status do %>
<% :started -> %>
<div class={
"flex #{if(@cell_view.editor && @cell_view.editor.placement == :top, do: "flex-col-reverse", else: "flex-col")}"
}>
<.live_component
module={LivebookWeb.JSViewComponent}
id={@cell_view.id}
js_view={@cell_view.js_view}
session_id={@session_id}
client_id={@client_id}
/>
<.cell_editor
:if={@cell_view.editor}
cell_id={@cell_view.id}
tag="secondary"
empty={@cell_view.editor.empty}
language={@cell_view.editor.language}
rounded={@cell_view.editor.placement}
intellisense={@cell_view.editor.language == "elixir"}
hidden={not @cell_view.editor.visible}
/>
</div>
<% :dead -> %>
<div class="info-box">
<%= if @installing? do %>
Waiting for dependency installation to complete...
<% else %>
Run the notebook setup to show the contents of this Smart cell.
<% end %>
</div>
<% :down -> %>
<div class="info-box flex justify-between items-center">
<span>
The Smart cell crashed unexpectedly, this is most likely a bug.
</span>
<.button
color="gray"
phx-click={JS.push("recover_smart_cell", value: %{cell_id: @cell_view.id})}
>
Restart Smart cell
</.button>
</div>
<% :starting -> %>
<div class="delay-200">
<.content_skeleton empty={false} />
</div>
<% end %>
</div>
<div data-el-editor-box>
<div class="relative">
<.cell_editor
cell_id={@cell_view.id}
tag="primary"
empty={@cell_view.empty}
language="elixir"
intellisense
read_only
/>
</div>
</div>
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
</div>
</div>
<.evaluation_outputs
outputs={@streams.outputs}
cell_view={@cell_view}
session_id={@session_id}
session_pid={@session_pid}
client_id={@client_id}
/>
</.cell_body>
"""
end
defp cell_actions(assigns) do
assigns =
assigns
|> assign_new(:primary, fn -> [] end)
|> assign_new(:secondary, fn -> [] end)
~H"""
<div class="mb-1 flex items-center justify-between">
<div class="relative z-20 flex items-center justify-end space-x-2" data-el-actions data-primary>
{render_slot(@primary)}
</div>
<div
class="relative z-20 flex items-center justify-end md:space-x-2"
role="toolbar"
aria-label="cell actions"
data-el-actions
data-secondary
>
{render_slot(@secondary)}
</div>
</div>
"""
end
defp cell_body(assigns) do
~H"""
<!-- By setting tabindex we can programmatically focus this element,
also we actually want to make this element tab-focusable -->
<div class="flex relative focus-visible:outline-none" data-el-cell-body tabindex="0">
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-el-cell-focus-indicator></div>
<div class="w-full">
{render_slot(@inner_block)}
</div>
</div>
"""
end
defp cell_evaluation_button(%{status: :ready} = assigns) do
~H"""
<div class="flex items-center space-x-1">
<button
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
data-el-queue-cell-evaluation-button
data-cell-id={@cell_id}
>
<%= cond do %>
<% @reevaluates_automatically -> %>
<.remix_icon icon="check-line" class="text-xl" />
<span class="text-sm font-medium">Reevaluates automatically</span>
<% @validity == :evaluated -> %>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">Reevaluate</span>
<% true -> %>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">Evaluate</span>
<% end %>
</button>
<.menu id={"cell-#{@cell_id}-evaluation-menu"} position="bottom-left" distant>
<:toggle>
<button class="flex text-gray-600 hover:text-gray-800">
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
</button>
</:toggle>
<.menu_item variant={if(not @reevaluate_automatically, do: "selected", else: "default")}>
<button
role="menuitem"
phx-click={
JS.push("set_reevaluate_automatically", value: %{value: false, cell_id: @cell_id})
}
>
<.remix_icon icon="check-line" class={if(@reevaluate_automatically, do: "invisible")} />
<span>Evaluate on demand</span>
</button>
</.menu_item>
<.menu_item variant={if(@reevaluate_automatically, do: "selected", else: "default")}>
<button
role="menuitem"
phx-click={
JS.push("set_reevaluate_automatically", value: %{value: true, cell_id: @cell_id})
}
>
<.remix_icon icon="check-line" class={if(not @reevaluate_automatically, do: "invisible")} />
<span>Reevaluate automatically</span>
</button>
</.menu_item>
</.menu>
</div>
"""
end
defp cell_evaluation_button(assigns) do
~H"""
<button
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
phx-click="cancel_cell_evaluation"
phx-value-cell_id={@cell_id}
>
<.remix_icon icon="stop-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
Stop
</span>
</button>
"""
end
defp setup_cell_evaluation_button(%{status: :ready} = assigns) do
~H"""
<div class="flex items-center space-x-1">
<button
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
data-el-queue-cell-evaluation-button
data-cell-id={@cell_id}
>
<%= if @validity == :fresh do %>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">Setup</span>
<% else %>
<.remix_icon icon="restart-fill" class="text-xl" />
<span class="text-sm font-medium">Reconnect and setup</span>
<% end %>
</button>
<%= unless Livebook.Runtime.fixed_dependencies?(@runtime) do %>
<.menu id="setup-menu" position="bottom-left" distant>
<:toggle>
<button class="flex text-gray-600 hover:text-gray-800">
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
</button>
</:toggle>
<.menu_item>
<button
role="menuitem"
data-el-queue-cell-evaluation-button
data-cell-id={@cell_id}
data-disable-dependencies-cache
>
<.remix_icon icon="play-circle-fill" />
<span>Setup without cache</span>
</button>
</.menu_item>
</.menu>
<% end %>
</div>
"""
end
defp setup_cell_evaluation_button(assigns) do
~H"""
<button
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
phx-click="cancel_cell_evaluation"
phx-value-cell_id={@cell_id}
>
<.remix_icon icon="stop-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
Stop
</span>
</button>
"""
end
defp enable_insert_mode_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Edit content" data-el-enable-insert-mode-button>
<.icon_button aria-label="edit content">
<.remix_icon icon="pencil-line" />
</.icon_button>
</span>
"""
end
defp toggle_source_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Toggle source" data-el-toggle-source-button>
<.icon_button aria-label="toggle source">
<.remix_icon icon="code-line" />
</.icon_button>
</span>
"""
end
defp convert_smart_cell_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Convert to Code cell">
<.icon_button
aria-label="toggle source"
data-link-package-search
phx-click={JS.push("convert_smart_cell", value: %{cell_id: @cell_id})}
>
<.remix_icon icon="pencil-line" />
</.icon_button>
</span>
"""
end
defp package_search_button(assigns) do
~H"""
<%= if Livebook.Runtime.fixed_dependencies?(@runtime) do %>
<span
class="tooltip top"
data-tooltip="The current runtime does not support adding dependencies"
>
<.icon_button disabled>
<.remix_icon icon="play-list-add-line" />
</.icon_button>
</span>
<% else %>
<span class="tooltip top" data-tooltip="Add package (sp)">
<.icon_button
patch={~p"/sessions/#{@session_id}/package-search"}
role="button"
data-btn-package-search
>
<.remix_icon icon="play-list-add-line" />
</.icon_button>
</span>
<% end %>
"""
end
defp cell_link_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Link">
<.icon_button href={"#cell-#{@cell_id}"} role="button" aria-label="link to cell">
<.remix_icon icon="link" />
</.icon_button>
</span>
"""
end
def amplify_output_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Amplify output" data-el-amplify-outputs-button>
<.icon_button aria-label="amplify outputs">
<.remix_icon icon="zoom-in-line" />
</.icon_button>
</span>
"""
end
defp cell_settings_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Cell settings">
<.icon_button
patch={~p"/sessions/#{@session_id}/cell-settings/#{@cell_id}"}
aria-label="cell settings"
role="button"
>
<.remix_icon icon="settings-3-line" />
</.icon_button>
</span>
"""
end
defp move_cell_up_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Move up">
<.icon_button
aria-label="move cell up"
phx-click="move_cell"
phx-value-cell_id={@cell_id}
phx-value-offset="-1"
>
<.remix_icon icon="arrow-up-s-line" />
</.icon_button>
</span>
"""
end
defp move_cell_down_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Move down">
<.icon_button
aria-label="move cell down"
phx-click="move_cell"
phx-value-cell_id={@cell_id}
phx-value-offset="1"
>
<.remix_icon icon="arrow-down-s-line" />
</.icon_button>
</span>
"""
end
defp delete_cell_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Delete">
<.icon_button
aria-label="delete cell"
phx-click={JS.push("delete_cell", value: %{cell_id: @cell_id})}
>
<.remix_icon icon="delete-bin-6-line" />
</.icon_button>
</span>
"""
end
defp setup_cell_info(assigns) do
~H"""
<span
class="tooltip left"
data-tooltip={
~s'''
The setup cell includes code that initializes the notebook
and should run only once. This is the best place to install
dependencies and set global configuration.\
'''
}
>
<.icon_button>
<.remix_icon icon="question-line" />
</.icon_button>
</span>
"""
end
attr :cell_id, :string, required: true
attr :tag, :string, required: true
attr :empty, :boolean, required: true
attr :language, :string, required: true
attr :intellisense, :boolean, default: false
attr :read_only, :boolean, default: false
attr :rounded, :atom, default: :both
attr :hidden, :boolean, default: false
defp cell_editor(assigns) do
~H"""
<div
class={[@hidden && "hidden"]}
id={"cell-editor-#{@cell_id}-#{@tag}"}
phx-hook="CellEditor"
data-p-cell-id={hook_prop(@cell_id)}
data-p-tag={hook_prop(@tag)}
data-p-language={hook_prop(@language)}
data-p-intellisense={hook_prop(@intellisense)}
data-p-read-only={hook_prop(@read_only)}
>
<div
id={"cell-editor-#{@cell_id}-#{@tag}-container"}
phx-update="ignore"
class={["bg-editor", rounded_class(@rounded)]}
data-el-editor-container
>
<div data-el-skeleton>
<div class="py-3 px-8">
<.content_skeleton bg_class="bg-gray-500" empty={@empty} />
</div>
</div>
</div>
</div>
"""
end
defp rounded_class(:both), do: "rounded-lg"
defp rounded_class(:top), do: "rounded-t-lg"
defp rounded_class(:bottom), do: "rounded-b-lg"
defp doctest_summary(assigns) do
~H"""
<div :if={@doctest_summary.failures_count > 0} class="pt-2" id={"doctest-summary-#{@cell_id}"}>
<div class="error-box py-3">
{doctest_summary_message(@doctest_summary)}
</div>
</div>
"""
end
defp doctest_summary_message(%{doctests_count: total, failures_count: failed}) do
doctests_pl = LivebookWeb.HTMLHelpers.pluralize(total, "doctest", "doctests")
failures_pl = if failed == 1, do: "failure has", else: "failures have"
"#{failed} out of #{doctests_pl} failed (#{failures_pl} been reported above)"
end
defp evaluation_outputs(assigns) do
~H"""
<div
class="flex flex-col"
data-el-outputs-container
id={"outputs-#{@cell_view.id}-#{@cell_view.eval.outputs_batch_number}"}
phx-update="stream"
>
<LivebookWeb.Output.output
:for={{dom_id, output} <- @outputs}
id={dom_id}
output={output.output}
session_id={@session_id}
session_pid={@session_pid}
client_id={@client_id}
cell_id={@cell_view.id}
input_views={@cell_view.eval.input_views}
/>
</div>
"""
end
defp cell_indicators(assigns) do
~H"""
<div class="flex gap-1">
<.cell_indicator :if={has_status?(@cell_view)}>
<.cell_status id={@id} cell_view={@cell_view} />
</.cell_indicator>
<.cell_indicator>
<.language_icon language={cell_language(@cell_view)} class="w-3 h-3" />
</.cell_indicator>
</div>
"""
end
defp cell_indicator(assigns) do
~H"""
<div
data-el-cell-indicator
class="px-1.5 h-[22px] rounded-lg flex items-center border bg-editor-lighter border-editor text-editor"
>
{render_slot(@inner_block)}
</div>
"""
end
defp cell_language(%{language: language}), do: Atom.to_string(language)
defp cell_language(%{type: :smart}), do: "elixir"
defp has_status?(%{eval: %{status: :ready, validity: :fresh}}), do: false
defp has_status?(_cell_view), do: true
defp cell_status(%{cell_view: %{eval: %{status: :evaluating}}} = assigns) do
~H"""
<.cell_status_indicator variant="progressing" change_indicator={true}>
<span
class="font-mono"
id={"#{@id}-cell-timer"}
phx-hook="Timer"
phx-update="ignore"
data-p-start={hook_prop(DateTime.to_iso8601(@cell_view.eval.evaluation_start))}
>
</span>
</.cell_status_indicator>
"""
end
defp cell_status(%{cell_view: %{eval: %{status: :queued}}} = assigns) do
~H"""
<.cell_status_indicator variant="waiting">
Queued
</.cell_status_indicator>
"""
end
defp cell_status(%{cell_view: %{eval: %{validity: :evaluated}}} = assigns) do
~H"""
<.cell_status_indicator
variant={if(@cell_view.eval.errored, do: "error", else: "success")}
change_indicator={true}
tooltip={duration_label(@cell_view.eval.evaluation_time_ms)}
>
Evaluated
</.cell_status_indicator>
"""
end
defp cell_status(%{cell_view: %{eval: %{validity: :stale}}} = assigns) do
~H"""
<.cell_status_indicator
variant="warning"
change_indicator={true}
tooltip={duration_label(@cell_view.eval.evaluation_time_ms)}
>
Stale
</.cell_status_indicator>
"""
end
defp cell_status(%{cell_view: %{eval: %{validity: :aborted}}} = assigns) do
~H"""
<.cell_status_indicator
variant="inactive"
tooltip={duration_label(@cell_view.eval.evaluation_time_ms)}
>
Aborted
</.cell_status_indicator>
"""
end
defp cell_status(assigns), do: ~H""
attr :variant, :string, required: true
attr :tooltip, :string, default: nil
attr :change_indicator, :boolean, default: false
slot :inner_block, required: true
defp cell_status_indicator(assigns) do
~H"""
<div class={[@tooltip && "tooltip", "bottom distant-medium"]} data-tooltip={@tooltip}>
<div class="flex items-center space-x-1">
<div class="flex text-[11px]" data-el-cell-status>
{render_slot(@inner_block)}
<span :if={@change_indicator} data-el-change-indicator>*</span>
</div>
<.status_indicator variant={@variant} />
</div>
</div>
"""
end
defp duration_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 duration_label(_time_ms), do: nil
defp smart_cell_js_view_ref(%{type: :smart, status: :started, js_view: %{ref: ref}}), do: ref
defp smart_cell_js_view_ref(_cell_view), do: nil
end