defmodule LivebookWeb.SessionLive.CellComponent do use LivebookWeb, :live_component def render(assigns) do ~H"""
<%= render_cell(assigns) %>
""" end def render_cell(%{cell_view: %{type: :markdown}} = assigns) do ~H"""
<.cell_link_button cell_id={@cell_view.id} /> <%= 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 %> <.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} />
<.cell_body>
<.editor cell_view={@cell_view} />
<.content_placeholder bg_class="bg-gray-200" empty={@cell_view.empty?} />
""" end def render_cell(%{cell_view: %{type: :elixir}} = assigns) do ~H"""
<%= if @cell_view.evaluation_status == :ready do %> <%= if @cell_view.validity_status != :fresh and @cell_view.reevaluate_automatically do %> <%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center" do %> <.remix_icon icon="check-line" class="text-xl" /> Reevaluates automatically <% end %> <% else %> <% end %> <% else %> <% end %>
<.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} />
<.cell_body> <.editor cell_view={@cell_view} /> <%= if @cell_view.outputs != [] do %>
<.outputs cell_view={@cell_view} socket={@socket} />
<% end %> """ end def render_cell(%{cell_view: %{type: :input}} = assigns) do ~H"""
<.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} />
<.cell_body>
<%= @cell_view.name %>
<.cell_input cell_view={@cell_view} /> <%= if @cell_view.error do %>
<%= String.capitalize(@cell_view.error) %>
<% end %>
""" end defp cell_input(%{cell_view: %{input_type: :textarea}} = assigns) do ~H""" """ end defp cell_input(%{cell_view: %{input_type: :range}} = assigns) do ~H"""
<%= @cell_view.props.min %>
<%= @cell_view.props.max %>
""" end defp cell_input(%{cell_view: %{input_type: :select}} = assigns) do ~H"""
""" end defp cell_input(%{cell_view: %{input_type: :checkbox}} = assigns) do ~H"""
<.switch_checkbox data-element="input" name="value" checked={@cell_view.value == "true"} />
""" end defp cell_input(assigns) do ~H""" """ 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"""
<%= render_block(@inner_block) %>
""" end defp cell_link_button(assigns) do ~H""" <.remix_icon icon="link" class="text-xl" /> """ end defp cell_settings_button(assigns) do ~H""" <%= 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 %> """ end defp move_cell_up_button(assigns) do ~H""" """ end defp move_cell_down_button(assigns) do ~H""" """ end defp delete_cell_button(assigns) do ~H""" """ end defp editor(assigns) do ~H"""
<.content_placeholder bg_class="bg-gray-500" empty={@cell_view.empty?} />
<%= if @cell_view.type == :elixir do %>
<.cell_status cell_view={@cell_view} />
<% end %>
""" 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 %>
<% else %>
<% 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}> """ end defp cell_status(%{cell_view: %{evaluation_status: :queued}} = assigns) do ~H""" <.status_indicator circle_class="bg-gray-400" animated_circle_class="bg-gray-300"> Queued """ 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 """ end defp cell_status(%{cell_view: %{validity_status: :stale}} = assigns) do ~H""" <.status_indicator circle_class="bg-yellow-200" change_indicator={true}> Stale """ end defp cell_status(%{cell_view: %{validity_status: :aborted}} = assigns) do ~H""" <.status_indicator circle_class="bg-gray-500"> Aborted """ 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"""
<%= render_block(@inner_block) %> <%= if @change_indicator do %> * <% end %>
<%= if @animated_circle_class do %> <% end %>
""" 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"""
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<%= render_output(output, %{ id: "cell-#{@cell_view.id}-evaluation#{evaluation_number(@cell_view.evaluation_status, @cell_view.number_of_evaluations)}-output#{index}", socket: @socket }) %>
<% end %>
""" end defp evaluation_number(:evaluating, number_of_evaluations), do: number_of_evaluations + 1 defp evaluation_number(_evaluation_status, number_of_evaluations), do: number_of_evaluations 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"""
<%= render_error_message_output(@formatted) %>
""" 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"""
<%= @message %>
""" end end