Packages
livebook
0.8.1
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
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/output.ex
defmodule LivebookWeb.Output do
use Phoenix.Component
import LivebookWeb.Helpers
import LivebookWeb.LiveHelpers
alias Phoenix.LiveView.JS
alias LivebookWeb.Output
alias LivebookWeb.Router.Helpers, as: Routes
@doc """
Renders a list of cell outputs.
"""
def outputs(assigns) do
~H"""
<%= for {idx, output} <- Enum.reverse(@outputs) do %>
<div
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}",
socket: @socket,
session_id: @session_id,
input_values: @input_values,
client_id: @client_id
}) %>
</div>
<% end %>
"""
end
defp border?({:stdout, _text}), do: true
defp border?({:text, _text}), do: true
defp border?({:error, _message, _type}), do: true
defp border?({:grid, _, info}), do: Map.get(info, :boxed, false)
defp border?(_output), do: false
defp render_output({:stdout, text}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
live_component(Output.StdoutComponent, id: id, text: text)
end
defp render_output({:text, text}, %{id: id}) do
assigns = %{id: id, text: text}
~H"""
<Output.TextComponent.render id={@id} content={@text} />
"""
end
defp render_output({:markdown, markdown}, %{id: id, session_id: session_id}) do
live_component(Output.MarkdownComponent,
id: id,
session_id: session_id,
content: markdown
)
end
defp render_output({:image, content, mime_type}, %{id: id}) do
assigns = %{id: id, content: content, mime_type: mime_type}
~H"""
<Output.ImageComponent.render id={@id} content={@content} mime_type={@mime_type} />
"""
end
defp render_output({:js, js_info}, %{id: id, session_id: session_id, client_id: client_id}) do
live_component(LivebookWeb.JSViewComponent,
id: id,
js_view: js_info.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({:frame, outputs, _info}, %{
id: id,
session_id: session_id,
input_values: input_values,
client_id: client_id
}) do
live_component(Output.FrameComponent,
id: id,
outputs: outputs,
session_id: session_id,
input_values: input_values,
client_id: client_id
)
end
defp render_output({:tabs, outputs, info}, %{
id: id,
socket: socket,
session_id: session_id,
input_values: input_values,
client_id: client_id
}) do
{labels, active_idx} =
if info.labels == :__pruned__ do
{[], nil}
else
labels =
Enum.zip_with(info.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,
socket: socket,
session_id: session_id,
input_values: input_values,
client_id: client_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">
<%= for {output_idx, label} <- @labels do %>
<button
id={"#{@id}-tabs-#{output_idx}"}
class={"tab #{if(output_idx == @active_idx, do: "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>
<% end %>
</div>
<div id={"#{@id}-tab-contents"} phx-update="append">
<%= for {output_idx, output} <- @outputs do %>
<% # We use data-keep-attribute, because we know active_idx only on the first render %>
<div
id={"#{@id}-tab-content-#{output_idx}"}
data-tab-content={output_idx}
class={"#{if(output_idx != @active_idx, do: "hidden")}"}
data-keep-attribute="class"
>
<.outputs
outputs={[{output_idx, output}]}
dom_id_map={%{}}
socket={@socket}
session_id={@session_id}
input_values={@input_values}
client_id={@client_id}
/>
</div>
<% end %>
</div>
</div>
"""
end
defp render_output({:grid, outputs, info}, %{
id: id,
session_id: session_id,
socket: socket,
input_values: input_values,
client_id: client_id
}) do
columns = info[:columns] || 1
gap = info[:gap] || 8
assigns = %{
id: id,
columns: columns,
gap: gap,
outputs: outputs,
socket: socket,
session_id: session_id,
input_values: input_values,
client_id: client_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"
>
<%= for {output_idx, output} <- @outputs do %>
<div id={"#{@id}-grid-item-#{output_idx}"}>
<.outputs
outputs={[{output_idx, output}]}
dom_id_map={%{}}
socket={@socket}
session_id={@session_id}
input_values={@input_values}
client_id={@client_id}
/>
</div>
<% end %>
</div>
</div>
"""
end
defp render_output({:input, attrs}, %{
id: id,
input_values: input_values,
client_id: client_id,
session_id: session_id
}) do
live_component(Output.InputComponent,
id: id,
attrs: attrs,
input_values: input_values,
client_id: client_id,
session_id: session_id
)
end
defp render_output({:control, attrs}, %{
id: id,
input_values: input_values,
client_id: client_id
}) do
live_component(Output.ControlComponent,
id: id,
attrs: attrs,
input_values: input_values,
client_id: client_id
)
end
defp render_output({:error, formatted, {:missing_secret, secret_name}}, %{
socket: socket,
session_id: session_id
}) do
assigns = %{
message: formatted,
secret_name: secret_name,
socket: socket,
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>
<%= live_patch to: Routes.session_path(@socket, :secrets, @session_id, secret_name: @secret_name),
class: "button-base button-gray",
aria_label: "add secret",
role: "button" do %>
<span>Add secret</span>
<% end %>
</div>
<%= render_formatted_error_message(@message) %>
</div>
"""
end
defp render_output({:error, formatted, _type}, %{}) do
render_formatted_error_message(formatted)
end
defp render_output(output, %{}) do
render_error_message("""
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(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