2021-11-09 03:45:30 +08:00
|
|
|
defmodule LivebookWeb.Output do
|
|
|
|
use Phoenix.Component
|
|
|
|
|
2022-01-26 04:55:24 +08:00
|
|
|
import LivebookWeb.Helpers
|
2022-09-01 07:45:55 +08:00
|
|
|
import LivebookWeb.LiveHelpers
|
2022-01-26 04:55:24 +08:00
|
|
|
|
2022-08-04 06:24:15 +08:00
|
|
|
alias Phoenix.LiveView.JS
|
2022-01-17 20:24:59 +08:00
|
|
|
alias LivebookWeb.Output
|
2022-09-01 07:45:55 +08:00
|
|
|
alias LivebookWeb.Router.Helpers, as: Routes
|
2022-01-17 20:24:59 +08:00
|
|
|
|
2021-11-09 03:45:30 +08:00
|
|
|
@doc """
|
2021-11-26 01:43:42 +08:00
|
|
|
Renders a list of cell outputs.
|
2021-11-09 03:45:30 +08:00
|
|
|
"""
|
2021-11-26 01:43:42 +08:00
|
|
|
def outputs(assigns) do
|
|
|
|
~H"""
|
2022-01-17 03:37:00 +08:00
|
|
|
<%= for {idx, output} <- Enum.reverse(@outputs) do %>
|
2022-08-02 21:51:02 +08:00
|
|
|
<div
|
|
|
|
class="max-w-full"
|
|
|
|
id={"output-wrapper-#{@dom_id_map[idx] || idx}"}
|
2022-04-04 18:19:11 +08:00
|
|
|
data-el-output
|
2022-01-17 03:37:00 +08:00
|
|
|
data-border={border?(output)}
|
2022-08-02 21:51:02 +08:00
|
|
|
>
|
2022-01-17 03:37:00 +08:00
|
|
|
<%= render_output(output, %{
|
2022-08-02 21:51:02 +08:00
|
|
|
id: "output-#{idx}",
|
|
|
|
socket: @socket,
|
|
|
|
session_id: @session_id,
|
2022-08-05 20:43:41 +08:00
|
|
|
input_values: @input_values,
|
|
|
|
client_id: @client_id
|
2022-08-02 21:51:02 +08:00
|
|
|
}) %>
|
2022-01-17 03:37:00 +08:00
|
|
|
</div>
|
|
|
|
<% end %>
|
2021-11-26 01:43:42 +08:00
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2022-01-17 03:37:00 +08:00
|
|
|
defp border?({:stdout, _text}), do: true
|
|
|
|
defp border?({:text, _text}), do: true
|
2022-09-01 07:45:55 +08:00
|
|
|
defp border?({:error, _message, _type}), do: true
|
2022-08-30 19:15:55 +08:00
|
|
|
defp border?({:grid, _, info}), do: Map.get(info, :boxed, false)
|
2022-01-17 03:37:00 +08:00
|
|
|
defp border?(_output), do: false
|
2021-11-26 01:43:42 +08:00
|
|
|
|
2022-01-17 03:37:00 +08:00
|
|
|
defp render_output({:stdout, text}, %{id: id}) do
|
|
|
|
text = if(text == :__pruned__, do: nil, else: text)
|
2022-03-19 19:22:36 +08:00
|
|
|
live_component(Output.StdoutComponent, id: id, text: text)
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2021-11-26 01:43:42 +08:00
|
|
|
defp render_output({:text, text}, %{id: id}) do
|
2022-01-17 03:37:00 +08:00
|
|
|
assigns = %{id: id, text: text}
|
|
|
|
|
|
|
|
~H"""
|
2022-03-19 19:22:36 +08:00
|
|
|
<Output.TextComponent.render id={@id} content={@text} />
|
2022-01-17 03:37:00 +08:00
|
|
|
"""
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2022-05-17 09:03:18 +08:00
|
|
|
defp render_output({:markdown, markdown}, %{id: id, session_id: session_id}) do
|
|
|
|
live_component(Output.MarkdownComponent,
|
|
|
|
id: id,
|
|
|
|
session_id: session_id,
|
|
|
|
content: markdown
|
|
|
|
)
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2021-11-26 01:43:42 +08:00
|
|
|
defp render_output({:image, content, mime_type}, %{id: id}) do
|
2022-01-17 03:37:00 +08:00
|
|
|
assigns = %{id: id, content: content, mime_type: mime_type}
|
|
|
|
|
|
|
|
~H"""
|
2022-01-17 20:24:59 +08:00
|
|
|
<Output.ImageComponent.render content={@content} mime_type={@mime_type} />
|
2022-01-17 03:37:00 +08:00
|
|
|
"""
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2022-08-05 20:43:41 +08:00
|
|
|
defp render_output({:js, js_info}, %{id: id, session_id: session_id, client_id: client_id}) do
|
2022-02-28 20:53:33 +08:00
|
|
|
live_component(LivebookWeb.JSViewComponent,
|
|
|
|
id: id,
|
|
|
|
js_view: js_info.js_view,
|
2022-03-23 01:25:42 +08:00
|
|
|
session_id: session_id,
|
2022-08-05 20:43:41 +08:00
|
|
|
client_id: client_id,
|
2022-03-23 01:25:42 +08:00
|
|
|
timeout_message: "Output data no longer available, please reevaluate this cell"
|
2022-02-28 20:53:33 +08:00
|
|
|
)
|
2021-11-26 01:43:42 +08:00
|
|
|
end
|
|
|
|
|
2022-01-17 03:37:00 +08:00
|
|
|
defp render_output({:frame, outputs, _info}, %{
|
|
|
|
id: id,
|
2022-08-05 20:43:41 +08:00
|
|
|
session_id: session_id,
|
2022-01-17 03:37:00 +08:00
|
|
|
input_values: input_values,
|
2022-08-05 20:43:41 +08:00
|
|
|
client_id: client_id
|
2022-01-17 03:37:00 +08:00
|
|
|
}) do
|
2022-01-17 20:24:59 +08:00
|
|
|
live_component(Output.FrameComponent,
|
2022-01-17 03:37:00 +08:00
|
|
|
id: id,
|
|
|
|
outputs: outputs,
|
|
|
|
session_id: session_id,
|
2022-08-05 20:43:41 +08:00
|
|
|
input_values: input_values,
|
|
|
|
client_id: client_id
|
2022-01-17 03:37:00 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2022-08-04 06:24:15 +08:00
|
|
|
defp render_output({:tabs, outputs, info}, %{
|
|
|
|
id: id,
|
2022-08-05 21:54:08 +08:00
|
|
|
socket: socket,
|
2022-08-04 06:24:15 +08:00
|
|
|
session_id: session_id,
|
2022-08-05 21:54:08 +08:00
|
|
|
input_values: input_values,
|
|
|
|
client_id: client_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}) do
|
|
|
|
{labels, active_idx} =
|
2022-08-30 19:15:55 +08:00
|
|
|
if info.labels == :__pruned__ do
|
2022-08-04 06:24:15 +08:00
|
|
|
{[], 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,
|
2022-08-05 21:54:08 +08:00
|
|
|
input_values: input_values,
|
|
|
|
client_id: client_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
# 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}
|
2022-08-05 21:54:08 +08:00
|
|
|
client_id={@client_id}
|
2022-08-04 06:24:15 +08:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<% end %>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
|
|
|
defp render_output({:grid, outputs, info}, %{
|
|
|
|
id: id,
|
|
|
|
session_id: session_id,
|
2022-08-05 21:54:08 +08:00
|
|
|
socket: socket,
|
|
|
|
input_values: input_values,
|
|
|
|
client_id: client_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}) do
|
2022-08-30 19:15:55 +08:00
|
|
|
columns = info[:columns] || 1
|
|
|
|
gap = info[:gap] || 8
|
2022-08-04 06:24:15 +08:00
|
|
|
|
|
|
|
assigns = %{
|
|
|
|
id: id,
|
2022-08-30 19:15:55 +08:00
|
|
|
columns: columns,
|
|
|
|
gap: gap,
|
2022-08-04 06:24:15 +08:00
|
|
|
outputs: outputs,
|
|
|
|
socket: socket,
|
|
|
|
session_id: session_id,
|
2022-08-05 21:54:08 +08:00
|
|
|
input_values: input_values,
|
|
|
|
client_id: client_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<div id={@id} class="overflow-auto tiny-scrollbar">
|
|
|
|
<div
|
|
|
|
id={"#{@id}-grid"}
|
2022-08-30 19:15:55 +08:00
|
|
|
class="grid grid-cols-2 w-full"
|
|
|
|
style={"grid-template-columns: repeat(#{@columns}, minmax(0, 1fr)); gap: #{@gap}px"}
|
2022-08-04 06:24:15 +08:00
|
|
|
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}
|
2022-08-05 21:54:08 +08:00
|
|
|
client_id={@client_id}
|
2022-08-04 06:24:15 +08:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<% end %>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2022-08-05 20:43:41 +08:00
|
|
|
defp render_output({:input, attrs}, %{id: id, input_values: input_values, client_id: client_id}) do
|
|
|
|
live_component(Output.InputComponent,
|
|
|
|
id: id,
|
|
|
|
attrs: attrs,
|
|
|
|
input_values: input_values,
|
|
|
|
client_id: client_id
|
|
|
|
)
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2022-08-05 20:43:41 +08:00
|
|
|
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
|
|
|
|
)
|
2021-12-02 23:45:00 +08:00
|
|
|
end
|
|
|
|
|
2022-09-03 02:04:41 +08:00
|
|
|
defp render_output({:error, formatted, {:missing_secret, secret_label}}, %{
|
2022-09-01 07:45:55 +08:00
|
|
|
socket: socket,
|
|
|
|
session_id: session_id
|
|
|
|
}) do
|
2022-09-03 02:04:41 +08:00
|
|
|
assigns = %{message: formatted, secret_label: secret_label}
|
2021-11-09 03:45:30 +08:00
|
|
|
|
|
|
|
~H"""
|
2022-09-01 07:45:55 +08:00
|
|
|
<div class="-m-4 space-x-4 py-4">
|
|
|
|
<div
|
|
|
|
class="flex items-center justify-between font-editor border-b px-4 pb-4 mb-4"
|
|
|
|
style="color: var(--ansi-color-red);"
|
|
|
|
>
|
|
|
|
<div class="flex space-x-2">
|
|
|
|
<.remix_icon icon="close-circle-line" />
|
2022-09-03 02:04:41 +08:00
|
|
|
<span>Missing secret <%= inspect(@secret_label) %></span>
|
2022-09-01 07:45:55 +08:00
|
|
|
</div>
|
2022-09-03 02:04:41 +08:00
|
|
|
<%= live_patch to: Routes.session_path(socket, :secrets, session_id, secret_label: secret_label),
|
2022-09-01 07:45:55 +08:00
|
|
|
class: "button-base button-gray",
|
|
|
|
aria_label: "add secret",
|
|
|
|
role: "button" do %>
|
|
|
|
<span>Add secret</span>
|
|
|
|
<% end %>
|
|
|
|
</div>
|
|
|
|
<%= render_formatted_error_message(@message) %>
|
|
|
|
</div>
|
2021-11-09 03:45:30 +08:00
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2022-09-01 07:45:55 +08:00
|
|
|
defp render_output({:error, formatted, _type}, %{}) do
|
|
|
|
render_formatted_error_message(formatted)
|
|
|
|
end
|
|
|
|
|
2022-01-17 20:24:59 +08:00
|
|
|
# TODO: remove on Livebook v0.7
|
|
|
|
defp render_output(output, %{})
|
|
|
|
when elem(output, 0) in [
|
|
|
|
:vega_lite_static,
|
|
|
|
:vega_lite_dynamic,
|
|
|
|
:table_dynamic,
|
|
|
|
:frame_dynamic
|
|
|
|
] do
|
2022-01-26 04:55:24 +08:00
|
|
|
render_error_message("""
|
2022-01-17 20:24:59 +08:00
|
|
|
Legacy output format: #{inspect(output)}. Please update Kino to
|
|
|
|
the latest version.
|
|
|
|
""")
|
|
|
|
end
|
|
|
|
|
2021-11-26 01:43:42 +08:00
|
|
|
defp render_output(output, %{}) do
|
2022-01-26 04:55:24 +08:00
|
|
|
render_error_message("""
|
2021-11-09 03:45:30 +08:00
|
|
|
Unknown output format: #{inspect(output)}. If you're using Kino,
|
|
|
|
you may want to update Kino and Livebook to the latest version.
|
|
|
|
""")
|
|
|
|
end
|
|
|
|
|
2022-01-26 04:55:24 +08:00
|
|
|
defp render_error_message(message) do
|
2021-11-09 03:45:30 +08:00
|
|
|
assigns = %{message: message}
|
|
|
|
|
|
|
|
~H"""
|
2022-08-02 21:51:02 +08:00
|
|
|
<div
|
|
|
|
class="whitespace-pre-wrap font-editor text-red-600"
|
|
|
|
role="complementary"
|
|
|
|
aria-label="error message"
|
2022-08-03 00:22:49 +08:00
|
|
|
phx-no-format
|
|
|
|
><%= @message %></div>
|
2021-11-09 03:45:30 +08:00
|
|
|
"""
|
|
|
|
end
|
2022-09-01 07:45:55 +08:00
|
|
|
|
|
|
|
defp render_formatted_error_message(formatted) do
|
|
|
|
assigns = %{message: formatted}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<div
|
|
|
|
class="whitespace-pre-wrap font-editor text-gray-500"
|
|
|
|
role="complementary"
|
|
|
|
aria-label="error"
|
|
|
|
phx-no-format
|
|
|
|
><%= ansi_string_to_html(@message) %></div>
|
|
|
|
"""
|
|
|
|
end
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|