livebook/lib/livebook_web/live/session_live/cell_component.ex
2025-05-29 22:47:59 +02:00

937 lines
28 KiB
Elixir

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-setup={@cell_view[:setup]}
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, setup: true, language: :elixir}} = 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: :code, setup: true, language: :"pyproject.toml"}} = assigns
) do
~H"""
<.cell_actions>
<:primary>
<div class="flex gap-1 items-center text-gray-500 text-sm">
<.language_icon language="python" class="w-4 h-4" />
<span>Python (pyproject.toml)</span>
</div>
</:primary>
<:secondary>
<.cell_link_button cell_id={@cell_view.id} />
<.disable_language_button language={:python} />
<.pyproject_toml_cell_info />
</:secondary>
</.cell_actions>
<.cell_body>
<div class="relative" data-el-cell-body-root>
<div data-el-editor-box>
<.cell_editor
cell_id={@cell_view.id}
tag="primary"
empty={@cell_view.empty}
language="pyproject.toml"
/>
</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: :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={@cell_view.language == :elixir}
/>
</div>
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} langauge_toggle />
</div>
</div>
<div :if={@cell_view.language not in @enabled_languages} class="mt-2">
<.message_box kind="error">
<div class="flex items-center justify-between">
{language_name(@cell_view.language)} is not enabled for the current notebook.
<button
class="flex gap-1 items-center font-medium text-blue-600"
phx-click="enable_language"
phx-value-language="python"
>
Enable Python
</button>
</div>
</.message_box>
</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: :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={@cell_view.reevaluate_automatically}
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 disable_language_button(assigns) do
~H"""
<span class="tooltip top" data-tooltip="Delete">
<.icon_button
aria-label="delete cell"
phx-click="disable_language"
phx-value-language={@language}
>
<.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
defp pyproject_toml_cell_info(assigns) do
~H"""
<span
class="tooltip left"
data-tooltip={
~s'''
This cell specifies the Python environment using pyproject.toml
configuration. While standardized to a certain extent, this
configuration is used specifically with the uv package manager.\
'''
}
>
<.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
attr :id, :string, required: true
attr :cell_view, :map, required: true
attr :langauge_toggle, :boolean, default: false
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>
<%= if @langauge_toggle do %>
<.menu id={"cell-#{@id}-language-menu"} position="bottom-right">
<:toggle>
<.cell_indicator class="cursor-pointer">
<.language_icon language={cell_language(@cell_view)} class="w-3 h-3" />
</.cell_indicator>
</:toggle>
<.menu_item :for={language <- Livebook.Notebook.Cell.Code.languages()}>
<button
role="menuitem"
phx-click="set_cell_language"
phx-value-language={language.language}
phx-value-cell_id={@id}
>
<.cell_icon cell_type={:code} language={language.language} />
<span>{language.name}</span>
</button>
</.menu_item>
</.menu>
<% else %>
<.cell_indicator>
<.language_icon language={cell_language(@cell_view)} class="w-3 h-3" />
</.cell_indicator>
<% end %>
</div>
"""
end
attr :class, :string, default: nil
slot :inner_block, required: true
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",
@class
]}
>
{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
defp language_name(language) do
Enum.find_value(
Livebook.Notebook.Cell.Code.languages(),
&(&1.language == language && &1.name)
)
end
end