defmodule LivebookWeb.AppSessionLive do use LivebookWeb, :live_view import LivebookWeb.AppHelpers alias Livebook.Session alias Livebook.Notebook alias Livebook.Notebook.Cell @impl true def mount(%{"slug" => slug, "id" => session_id}, _session, socket) when socket.assigns.app_authenticated? do {:ok, app} = Livebook.Apps.fetch_app(slug) app_session = Enum.find(app.sessions, &(&1.id == session_id)) if app_session && app_session.app_status.lifecycle == :active do %{pid: session_pid} = app_session session = Session.get_by_pid(session_pid) {data, client_id} = if connected?(socket) do {data, client_id} = Session.register_client(session_pid, self(), socket.assigns.current_user) Session.subscribe(session_id) {data, client_id} else data = Session.get_data(session_pid) {data, nil} end {:ok, socket |> assign( slug: slug, session: session, page_title: get_page_title(data.notebook.name), client_id: client_id, data_view: data_to_view(data) ) |> assign_private(data: data)} else {:ok, assign(socket, nonexistent?: true, slug: slug, page_title: get_page_title(app.notebook_name) )} end end def mount(%{"slug" => slug} = params, _session, socket) do if connected?(socket) do to = if id = params["id"] do ~p"/apps/#{slug}/authenticate?id=#{id}" else ~p"/apps/#{slug}/authenticate" end {:ok, push_navigate(socket, to: to)} else {:ok, socket} end end # Puts the given assigns in `socket.private`, # to ensure they are not used for rendering. defp assign_private(socket, assigns) do Enum.reduce(assigns, socket, fn {key, value}, socket -> put_in(socket.private[key], value) end) end @impl true def render(%{nonexistent?: true} = assigns) when assigns.app_authenticated? do ~H"""
livebook
This app session does not exist
Visit the <.link class="border-b border-gray-700 hover:border-none" navigate={~p"/apps/#{@slug}"}>app page.
""" end def render(assigns) when assigns.app_authenticated? do ~H"""
<.menu id="app-menu" position={:bottom_left}> <:toggle> <.menu_item> <.link navigate={~p"/"} role="menuitem"> <.remix_icon icon="home-6-line" /> Home <.menu_item :if={@data_view.show_source}> <.link patch={~p"/apps/#{@data_view.slug}/#{@session.id}/source"} role="menuitem"> <.remix_icon icon="code-line" /> View source

<%= @data_view.notebook_name %>

<%= if @data_view.app_status.execution == :error do %>
<.remix_icon icon="error-warning-line" class="text-xl" /> Something went wrong
<% else %>
<% end %>
<.app_status status={@data_view.app_status} />
<.modal :if={@live_action == :source and @data_view.show_source} id="source-modal" show width={:big} patch={~p"/apps/#{@data_view.slug}/#{@session.id}"} > <.live_component module={LivebookWeb.AppSessionLive.SourceComponent} id="source" session={@session} /> """ end def render(assigns), do: auth_placeholder(assigns) defp get_page_title(notebook_name) do "Livebook - #{notebook_name}" end @impl true def handle_params(_params, _url, socket), do: {:noreply, socket} @impl true def handle_event("queue_interrupted_cell_evaluation", %{"cell_id" => cell_id}, socket) do data = socket.private.data with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id), true <- data.cell_infos[cell.id].eval.interrupted do Session.queue_full_evaluation(socket.assigns.session.pid, [cell_id]) end {:noreply, socket} end @impl true def handle_info({:operation, operation}, socket) do {:noreply, handle_operation(socket, operation)} end def handle_info({:set_input_values, values, local}, socket) do if local do socket = Enum.reduce(values, socket, fn {input_id, value}, socket -> operation = {:set_input_value, socket.assigns.client_id, input_id, value} handle_operation(socket, operation) end) {:noreply, socket} else for {input_id, value} <- values do Session.set_input_value(socket.assigns.session.pid, input_id, value) end {:noreply, socket} end end def handle_info(:session_closed, socket) do {:noreply, redirect_on_closed(socket)} end def handle_info(_message, socket), do: {:noreply, socket} defp handle_operation(socket, operation) do case Session.Data.apply_operation(socket.private.data, operation) do {:ok, data, _actions} -> socket |> assign_private(data: data) |> assign( data_view: update_data_view(socket.assigns.data_view, socket.private.data, data, operation) ) |> after_operation(socket, operation) :error -> socket end end defp after_operation(socket, _prev_socket, {:app_deactivate, _client_id}) do redirect_on_closed(socket) end defp after_operation(socket, _prev_socket, _operation), do: socket defp redirect_on_closed(socket) do socket |> put_flash(:info, "Session has been closed") |> push_navigate(to: ~p"/") end defp update_data_view(data_view, _prev_data, data, operation) do case operation do # See LivebookWeb.SessionLive for more details {:add_cell_evaluation_output, _client_id, _cell_id, {:frame, _outputs, %{type: type, ref: ref}}} when type != :default -> for {idx, {:frame, frame_outputs, _}} <- Notebook.find_frame_outputs(data.notebook, ref) do send_update(LivebookWeb.Output.FrameComponent, id: "output-#{idx}", outputs: frame_outputs, update_type: type ) end data_view _ -> data_to_view(data) end end defp data_to_view(data) do %{ notebook_name: data.notebook.name, output_views: for( {cell_id, output} <- visible_outputs(data.notebook), do: %{ output: output, input_values: input_values_for_output(output, data), cell_id: cell_id } ), app_status: data.app_data.status, show_source: data.notebook.app_settings.show_source, slug: data.notebook.app_settings.slug } end defp input_values_for_output(output, data) do input_ids = for attrs <- Cell.find_inputs_in_output(output), do: attrs.id Map.take(data.input_values, input_ids) end defp visible_outputs(notebook) do for section <- Enum.reverse(notebook.sections), cell <- Enum.reverse(section.cells), Cell.evaluable?(cell), output <- filter_outputs(cell.outputs, notebook.app_settings.output_type), do: {cell.id, output} end defp filter_outputs(outputs, :all), do: outputs defp filter_outputs(outputs, :rich), do: rich_outputs(outputs) defp rich_outputs(outputs) do for output <- outputs, output = filter_output(output), do: output end defp filter_output({idx, output}) when elem(output, 0) in [:plain_text, :markdown, :image, :js, :control, :input], do: {idx, output} defp filter_output({idx, {:tabs, outputs, metadata}}) do outputs_with_labels = for {output, label} <- Enum.zip(outputs, metadata.labels), output = filter_output(output), do: {output, label} {outputs, labels} = Enum.unzip(outputs_with_labels) {idx, {:tabs, outputs, %{metadata | labels: labels}}} end defp filter_output({idx, {:grid, outputs, metadata}}) do outputs = rich_outputs(outputs) if outputs != [] do {idx, {:grid, outputs, metadata}} end end defp filter_output({idx, {:frame, outputs, metadata}}) do outputs = rich_outputs(outputs) {idx, {:frame, outputs, metadata}} end defp filter_output({idx, {:error, _message, {:interrupt, _, _}} = output}), do: {idx, output} defp filter_output(_output), do: nil defp show_app_status?(%{execution: :executed, lifecycle: :active}), do: false defp show_app_status?(_status), do: true end