Packages
livebook
0.13.2
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/app_session_live.ex
defmodule LivebookWeb.AppSessionLive do
use LivebookWeb, :live_view
import LivebookWeb.AppComponents
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)
|> prune_outputs()}
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"""
<div class="h-screen flex items-center justify-center">
<div class="flex flex-col space-y-4 items-center">
<a href={~p"/"}>
<img src={~p"/images/logo.png"} height="128" width="128" alt="livebook" />
</a>
<div class="text-2xl text-gray-800">
This app session does not exist
</div>
<div class="max-w-2xl text-center text-gray-700">
<span>Visit the</span>
<.link class="border-b border-gray-700 hover:border-none" navigate={~p"/apps/#{@slug}"}>app page</.link>.
</div>
</div>
</div>
"""
end
def render(assigns) when assigns.app_authenticated? do
~H"""
<div class="h-full relative overflow-y-auto px-4 md:px-20" data-el-notebook>
<div class="w-full max-w-screen-lg py-4 mx-auto" data-el-notebook-content>
<div class="absolute md:fixed right-4 md:left-4 md:right-auto top-3">
<.menu id="app-menu" position={:bottom_right} md_position={:bottom_left}>
<:toggle>
<button class="flex items-center text-gray-900">
<img src={~p"/images/logo.png"} height="40" width="40" alt="logo livebook" />
<.remix_icon icon="arrow-down-s-line" />
</button>
</:toggle>
<.menu_item :if={@livebook_authenticated?}>
<.link navigate={~p"/"} role="menuitem">
<.remix_icon icon="home-6-line" />
<span>Home</span>
</.link>
</.menu_item>
<.menu_item>
<.link navigate={~p"/apps"} role="menuitem">
<.remix_icon icon="layout-grid-fill" />
<span>Apps</span>
</.link>
</.menu_item>
<.menu_item :if={@data_view.multi_session}>
<.link navigate={~p"/apps/#{@data_view.slug}"} role="menuitem">
<.remix_icon icon="play-list-add-line" />
<span>Sessions</span>
</.link>
</.menu_item>
<.menu_item :if={@data_view.show_source}>
<.link
patch={~p"/apps/#{@data_view.slug}/sessions/#{@session.id}/source"}
role="menuitem"
>
<.remix_icon icon="code-line" />
<span>View source</span>
</.link>
</.menu_item>
<.menu_item :if={@livebook_authenticated?}>
<.link patch={~p"/sessions/#{@session.id}"} role="menuitem">
<.remix_icon icon="terminal-line" />
<span>Debug</span>
</.link>
</.menu_item>
</.menu>
</div>
<div data-el-js-view-iframes phx-update="ignore" id="js-view-iframes"></div>
<div class="flex items-center pb-4 mb-2 space-x-4 border-b border-gray-200 pr-20 md:pr-0">
<h1 class="text-3xl font-semibold text-gray-800">
<%= @data_view.notebook_name %>
</h1>
</div>
<div class="pt-4 flex flex-col gap-6">
<.live_component
:for={cell_view <- @data_view.cell_views}
module={LivebookWeb.AppSessionLive.CellOutputsComponent}
id={"outputs-#{cell_view.id}"}
cell_view={cell_view}
session={@session}
client_id={@client_id}
/>
<%= if @data_view.app_status.execution == :error do %>
<div class={[
"flex justify-between items-center px-4 py-2 border-l-4 shadow-custom-1",
"text-red-400 border-red-400"
]}>
<div>
Something went wrong
</div>
<div class="flex items-center gap-6">
<span class="tooltip top" data-tooltip="Debug">
<.link
:if={@livebook_authenticated?}
navigate={~p"/sessions/#{@session.id}" <> "#cell-#{@data_view.errored_cell_id}"}
>
<.remix_icon icon="terminal-line" />
</.link>
</span>
<button
class="px-5 py-2 font-medium text-sm inline-flex rounded-lg border whitespace-nowrap items-center justify-center gap-1 border-red-400 text-red-400 hover:bg-red-50 focus:bg-red-50"
phx-click="queue_errored_cells_evaluation"
>
<.remix_icon icon="play-circle-fill" />
<span>Retry</span>
</button>
</div>
</div>
<% end %>
</div>
<div style="height: 80vh"></div>
</div>
<div class="fixed right-3 bottom-4 flex flex-col gap-2 items-center text-gray-600 w-10">
<span
:if={
@data_view.app_status.execution == :executed and
@data_view.any_stale?
}
class="tooltip left"
data-tooltip={
~S'''
Some inputs have changed.
Click this button to process with latest values.
'''
}
>
<.icon_button phx-click="queue_full_evaluation">
<.remix_icon icon="play-circle-fill" class="text-3xl leading-none" />
</.icon_button>
</span>
<.app_status_circle status={@data_view.app_status} />
</div>
</div>
<.modal
:if={@live_action == :source and @data_view.show_source}
id="source-modal"
show
width={:big}
patch={~p"/apps/#{@data_view.slug}/sessions/#{@session.id}"}
>
<.live_component
module={LivebookWeb.AppSessionLive.SourceComponent}
id="source"
session={@session}
/>
</.modal>
"""
end
def render(assigns), do: auth_placeholder(assigns)
attr :status, :map, required: true
defp app_status_circle(%{status: %{lifecycle: :shutting_down}} = assigns) do
~H"""
<.app_status_indicator text="Shutting down" variant={:inactive} icon="stop-line" />
"""
end
defp app_status_circle(%{status: %{lifecycle: :deactivated}} = assigns) do
~H"""
<.app_status_indicator text="Deactivated" variant={:inactive} icon="stop-line" />
"""
end
defp app_status_circle(%{status: %{execution: :executing}} = assigns) do
~H"""
<.app_status_indicator text="Executing" variant={:progressing} icon="loader-3-line" spinning />
"""
end
defp app_status_circle(%{status: %{execution: :executed}} = assigns) do
~H"""
<.app_status_indicator text="Executed" variant={:success} icon="check-line" />
"""
end
defp app_status_circle(%{status: %{execution: :error}} = assigns) do
~H"""
<.app_status_indicator text="Error" variant={:error} icon="close-line" />
"""
end
defp app_status_circle(%{status: %{execution: :interrupted}} = assigns) do
~H"""
<.app_status_indicator text="Interrupted" variant={:waiting} icon="pause-line" />
"""
end
attr :text, :string, required: true
attr :variant, :atom, required: true
attr :icon, :string, required: true
attr :spinning, :boolean, default: false
defp app_status_indicator(assigns) do
~H"""
<span class="tooltip left" data-tooltip={@text}>
<span class={[
status_circle_class(@variant),
"opacity-75",
"w-6 h-6 rounded-full flex items-center justify-center",
@spinning && "animate-spin"
]}>
<.remix_icon icon={@icon} class="text-white font-bold" />
</span>
</span>
"""
end
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
def handle_event("queue_errored_cells_evaluation", %{}, socket) do
data = socket.private.data
errored_cell_ids =
for {cell_id, %{eval: eval_info}} <- data.cell_infos, eval_info.errored, do: cell_id
Session.queue_full_evaluation(socket.assigns.session.pid, errored_cell_ids)
{:noreply, socket}
end
def handle_event("queue_full_evaluation", %{}, socket) do
Session.queue_full_evaluation(socket.assigns.session.pid, [])
{:noreply, socket}
end
@impl true
def handle_call({:get_input_value, input_id}, _from, socket) do
reply =
case socket.private.data.input_infos do
%{^input_id => %{value: value}} -> {:ok, socket.assigns.session.id, value}
%{} -> :error
end
{:reply, reply, 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,
{:add_cell_evaluation_output, _client_id, _cell_id, _output}
) do
prune_outputs(socket)
end
defp after_operation(
socket,
_prev_socket,
{:add_cell_evaluation_response, _client_id, _cell_id, _output, _metadata}
) do
prune_outputs(socket)
end
defp after_operation(socket, _prev_socket, {:app_deactivate, _client_id}) do
redirect_on_closed(socket)
end
defp after_operation(socket, _prev_socket, {:app_shutdown, _client_id}) do
put_flash(
socket,
:info,
"A new version has been deployed, this session will close once everybody leaves"
)
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, output} ->
case LivebookWeb.SessionLive.send_output_update(prev_data, data, cell_id, output) do
:ok -> data_view
:continue -> data_to_view(data)
end
_ ->
data_to_view(data)
end
end
defp prune_outputs(%{private: %{data: data}} = socket) do
assign_private(
socket,
data: update_in(data.notebook, &Notebook.prune_cell_outputs/1)
)
end
defp data_to_view(data) do
changed_input_ids = Session.Data.changed_input_ids(data)
%{
notebook_name: data.notebook.name,
cell_views:
for {cell, _section} <- Notebook.evaluable_cells_with_section(data.notebook) do
%{
id: cell.id,
input_views: input_views_for_cell(cell, data, changed_input_ids),
outputs: filter_outputs(cell.outputs, data.notebook.app_settings.output_type),
outputs_batch_number: data.cell_infos[cell.id].eval.outputs_batch_number
}
end,
app_status: data.app_data.status,
show_source: data.notebook.app_settings.show_source,
slug: data.notebook.app_settings.slug,
multi_session: data.notebook.app_settings.multi_session,
errored_cell_id: errored_cell_id(data),
any_stale?: any_stale?(data)
}
end
defp errored_cell_id(data) do
data.notebook
|> Notebook.evaluable_cells_with_section()
|> Enum.find_value(fn {cell, _section} ->
data.cell_infos[cell.id].eval.errored && cell.id
end)
end
defp any_stale?(data) do
Enum.any?(data.cell_infos, &match?({_, %{eval: %{validity: :stale}}}, &1))
end
defp input_views_for_cell(cell, data, changed_input_ids) do
input_ids =
for output <- cell.outputs,
input <- Cell.find_inputs_in_output(output),
do: input.id
data.input_infos
|> Map.take(input_ids)
|> Map.new(fn {input_id, %{value: value}} ->
{input_id, %{value: value, changed: MapSet.member?(changed_input_ids, input_id)}}
end)
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 output.type in [:plain_text, :markdown, :image, :js, :control, :input],
do: {idx, output}
defp filter_output({idx, %{type: :tabs} = output}) do
outputs_with_labels =
for {output, label} <- Enum.zip(output.outputs, output.labels),
output = filter_output(output),
do: {output, label}
{outputs, labels} = Enum.unzip(outputs_with_labels)
{idx, %{output | outputs: outputs, labels: labels}}
end
defp filter_output({idx, %{type: :grid} = output}) do
outputs = rich_outputs(output.outputs)
if outputs != [] do
{idx, %{output | outputs: outputs}}
end
end
defp filter_output({idx, %{type: :frame} = output}) do
outputs = rich_outputs(output.outputs)
{idx, %{output | outputs: outputs}}
end
defp filter_output({idx, %{type: :error, context: {:interrupt, _, _}} = output}),
do: {idx, output}
defp filter_output(_output), do: nil
end