Packages
livebook
0.11.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/output.ex
defmodule LivebookWeb.Output do
use LivebookWeb, :html
import LivebookWeb.Helpers
alias LivebookWeb.Output
@doc """
Renders a list of cell outputs.
"""
attr :outputs, :list, required: true
attr :session_id, :string, required: true
attr :session_pid, :any, required: true
attr :input_views, :map, required: true
attr :dom_id_map, :map, required: true
attr :client_id, :string, required: true
attr :cell_id, :string, required: true
def outputs(assigns) do
~H"""
<div
:for={{idx, output} <- Enum.reverse(@outputs)}
class="max-w-full"
id={"output-wrapper-#{@dom_id_map[idx] || idx}"}
data-el-output
data-border={border?(output)}
>
<%= render_output(output, %{
id: "output-#{idx}",
session_id: @session_id,
session_pid: @session_pid,
input_views: @input_views,
client_id: @client_id,
cell_id: @cell_id
}) %>
</div>
"""
end
defp border?(%{type: :terminal_text}), do: true
defp border?(%{type: :error, context: {:interrupt, _, _}}), do: false
defp border?(%{type: :error}), do: true
defp border?(%{type: :grid, boxed: boxed}), do: boxed
defp border?(_output), do: false
defp render_output(%{type: :terminal_text, text: text}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
assigns = %{id: id, text: text}
~H"""
<.live_component module={Output.TerminalTextComponent} id={@id} text={@text} />
"""
end
defp render_output(%{type: :plain_text, text: text}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
assigns = %{id: id, text: text}
~H"""
<.live_component module={Output.PlainTextComponent} id={@id} text={@text} />
"""
end
defp render_output(%{type: :markdown, text: text}, %{id: id, session_id: session_id}) do
text = if(text == :__pruned__, do: nil, else: text)
assigns = %{id: id, session_id: session_id, text: text}
~H"""
<.live_component module={Output.MarkdownComponent} id={@id} session_id={@session_id} text={@text} />
"""
end
defp render_output(%{type: :image} = output, %{id: id}) do
assigns = %{id: id, content: output.content, mime_type: output.mime_type}
~H"""
<Output.ImageComponent.render id={@id} content={@content} mime_type={@mime_type} />
"""
end
defp render_output(%{type: :js} = output, %{
id: id,
session_id: session_id,
client_id: client_id
}) do
assigns = %{
id: id,
js_view: output.js_view,
session_id: session_id,
client_id: client_id
}
~H"""
<.live_component
module={LivebookWeb.JSViewComponent}
id={@id}
js_view={@js_view}
session_id={@session_id}
client_id={@client_id}
timeout_message="Output data no longer available, please reevaluate this cell"
/>
"""
end
defp render_output(%{type: :frame} = output, %{
id: id,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
client_id: client_id,
cell_id: cell_id
}) do
assigns = %{
id: id,
outputs: output.outputs,
placeholder: output.placeholder,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
client_id: client_id,
cell_id: cell_id
}
~H"""
<.live_component
module={Output.FrameComponent}
id={@id}
outputs={@outputs}
placeholder={@placeholder}
session_id={@session_id}
session_pid={@session_pid}
input_views={@input_views}
client_id={@client_id}
cell_id={@cell_id}
/>
"""
end
defp render_output(%{type: :tabs, outputs: outputs, labels: labels}, %{
id: id,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
client_id: client_id,
cell_id: cell_id
}) do
{labels, active_idx} =
if labels == :__pruned__ do
{[], nil}
else
labels =
Enum.zip_with(labels, outputs, fn label, {output_idx, _} -> {output_idx, label} end)
active_idx = get_in(outputs, [Access.at(0), Access.elem(0)])
{labels, active_idx}
end
assigns = %{
id: id,
active_idx: active_idx,
labels: labels,
outputs: outputs,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
client_id: client_id,
cell_id: cell_id
}
# After pruning we don't render labels and we render only those
# outputs that are kept during pruning
~H"""
<div id={@id}>
<div class="tabs mb-2" id={"#{@id}-tabs"} phx-update="append">
<button
:for={{output_idx, label} <- @labels}
id={"#{@id}-tabs-#{output_idx}"}
class={["tab", output_idx == @active_idx && "active"]}
phx-click={
JS.remove_class("active", to: "##{@id}-tabs .tab.active")
|> JS.add_class("active")
|> JS.add_class("hidden", to: "##{@id}-tab-contents > *:not(.hidden)")
|> JS.remove_class("hidden", to: "##{@id}-tab-content-#{output_idx}")
}
>
<%= label %>
</button>
</div>
<div id={"#{@id}-tab-contents"} phx-update="append">
<% # We use data-keep-attribute, because we know active_idx only on the first render %>
<div
:for={{output_idx, output} <- @outputs}
id={"#{@id}-tab-content-#{output_idx}"}
data-tab-content={output_idx}
class={[output_idx != @active_idx && "hidden"]}
data-keep-attribute="class"
>
<.outputs
outputs={[{output_idx, output}]}
dom_id_map={%{}}
session_id={@session_id}
session_pid={@session_pid}
input_views={@input_views}
client_id={@client_id}
cell_id={@cell_id}
/>
</div>
</div>
</div>
"""
end
defp render_output(%{type: :grid} = grid, %{
id: id,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
client_id: client_id,
cell_id: cell_id
}) do
assigns = %{
id: id,
columns: grid.columns,
gap: grid.gap,
outputs: grid.outputs,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
client_id: client_id,
cell_id: cell_id
}
~H"""
<div id={@id} class="overflow-auto tiny-scrollbar">
<div
id={"#{@id}-grid"}
class="grid grid-cols-2 w-full"
style={"grid-template-columns: repeat(#{@columns}, minmax(0, 1fr)); gap: #{@gap}px"}
phx-update="append"
>
<div :for={{output_idx, output} <- @outputs} id={"#{@id}-grid-item-#{output_idx}"}>
<.outputs
outputs={[{output_idx, output}]}
dom_id_map={%{}}
session_id={@session_id}
session_pid={@session_pid}
input_views={@input_views}
client_id={@client_id}
cell_id={@cell_id}
/>
</div>
</div>
</div>
"""
end
defp render_output(%{type: :input} = input, %{
id: id,
input_views: input_views,
session_pid: session_pid,
client_id: client_id
}) do
assigns = %{
id: id,
input: input,
input_views: input_views,
session_pid: session_pid,
client_id: client_id
}
~H"""
<.live_component
module={Output.InputComponent}
id={@id}
input={@input}
input_views={@input_views}
session_pid={@session_pid}
client_id={@client_id}
/>
"""
end
defp render_output(%{type: :control} = control, %{
id: id,
input_views: input_views,
session_pid: session_pid,
client_id: client_id,
cell_id: cell_id
}) do
assigns = %{
id: id,
control: control,
input_views: input_views,
session_pid: session_pid,
client_id: client_id,
cell_id: cell_id
}
~H"""
<.live_component
module={Output.ControlComponent}
id={@id}
control={@control}
input_views={@input_views}
session_pid={@session_pid}
client_id={@client_id}
cell_id={@cell_id}
/>
"""
end
defp render_output(
%{type: :error, context: {:missing_secret, secret_name}} = output,
%{session_id: session_id}
) do
assigns = %{message: output.message, secret_name: secret_name, session_id: session_id}
~H"""
<div class="-m-4 space-x-4 py-4">
<div
class="flex items-center justify-between border-b px-4 pb-4 mb-4"
style="color: var(--ansi-color-red);"
>
<div class="flex space-x-2 font-editor">
<.remix_icon icon="close-circle-line" />
<span>Missing secret <%= inspect(@secret_name) %></span>
</div>
<.link
patch={~p"/sessions/#{@session_id}/secrets?secret_name=#{@secret_name}"}
class="button-base button-gray"
>
Add secret
</.link>
</div>
<%= render_formatted_error_message(@message) %>
</div>
"""
end
defp render_output(
%{type: :error, context: {:file_entry_forbidden, file_entry_name}} = output,
%{session_id: session_id}
) do
assigns = %{message: output.message, file_entry_name: file_entry_name, session_id: session_id}
~H"""
<div class="-m-4 space-x-4 py-4">
<div
class="flex items-center justify-between border-b px-4 pb-4 mb-4"
style="color: var(--ansi-color-red);"
>
<div class="flex space-x-2 font-editor">
<.remix_icon icon="close-circle-line" />
<span>Forbidden access to file <%= inspect(@file_entry_name) %></span>
</div>
<button
class="button-base button-gray"
phx-click={JS.push("review_file_entry_access", value: %{name: @file_entry_name})}
>
Review access
</button>
</div>
<%= render_formatted_error_message(@message) %>
</div>
"""
end
defp render_output(
%{type: :error, context: {:interrupt, variant, message}},
%{cell_id: cell_id}
) do
assigns = %{variant: variant, message: message, cell_id: cell_id}
~H"""
<div class={[
"flex justify-between items-center px-4 py-2 border-l-4 shadow-custom-1",
case @variant do
:error -> "text-red-400 border-red-400"
:normal -> "text-gray-500 border-gray-300"
end
]}>
<div>
<%= @message %>
</div>
<button
class={[
"button-base bg-transparent",
case @variant do
:error -> "border-red-400 text-red-400 hover:bg-red-50 focus:bg-red-50"
:normal -> "border-gray-300 text-gray-500 hover:bg-gray-100 focus:bg-gray-100"
end
]}
phx-click="queue_interrupted_cell_evaluation"
phx-value-cell_id={@cell_id}
>
<.remix_icon icon="play-circle-fill" class="align-middle mr-1" />
<span>Continue</span>
</button>
</div>
"""
end
defp render_output(%{type: :error, message: message}, %{}) do
render_formatted_error_message(message)
end
defp render_output(output, %{}) do
req = Livebook.Runtime.Definitions.kino_requirement()
render_error_message("""
Unknown output format: #{inspect(output)}. You may want to explicitly \
add {:kino, "#{req}"} as a notebook dependency or update to the latest \
Livebook.
""")
end
defp render_error_message(message) do
assigns = %{message: message}
~H"""
<div
class="whitespace-pre-wrap break-words font-editor text-red-600"
role="complementary"
aria-label="error message"
phx-no-format
><%= @message %></div>
"""
end
defp render_formatted_error_message(formatted) do
assigns = %{message: formatted}
~H"""
<div
class="whitespace-pre-wrap break-words font-editor text-gray-500"
role="complementary"
aria-label="error"
phx-no-format
><%= ansi_string_to_html(@message) %></div>
"""
end
end