From 194e31e2d964b220d0c4efc1a2ba680443b9f993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 4 Aug 2022 00:24:15 +0200 Subject: [PATCH] Add support for layout outputs (#1326) --- lib/livebook/live_markdown/export.ex | 19 +++ lib/livebook/notebook.ex | 34 ++++-- lib/livebook/notebook/cell.ex | 2 +- lib/livebook/runtime.ex | 4 + lib/livebook_web/live/output.ex | 123 ++++++++++++++++++++ lib/livebook_web/live/sidebar_helpers.ex | 4 +- test/livebook/live_markdown/export_test.exs | 102 ++++++++++++++++ 7 files changed, 277 insertions(+), 11 deletions(-) diff --git a/lib/livebook/live_markdown/export.ex b/lib/livebook/live_markdown/export.ex index 05c32442c..1e8be2e04 100644 --- a/lib/livebook/live_markdown/export.ex +++ b/lib/livebook/live_markdown/export.ex @@ -201,6 +201,25 @@ defmodule Livebook.LiveMarkdown.Export do end end + defp render_output({:tabs, outputs, _info}, ctx) do + Enum.find_value(outputs, :ignored, fn {_idx, output} -> + case render_output(output, ctx) do + :ignored -> nil + rendered -> rendered + end + end) + end + + defp render_output({:grid, outputs, _info}, ctx) do + outputs + |> Enum.map(fn {_idx, output} -> render_output(output, ctx) end) + |> Enum.reject(&(&1 == :ignored)) + |> case do + [] -> :ignored + rendered -> Enum.intersperse(rendered, "\n\n") + end + end + defp render_output(_output, _ctx), do: :ignored defp encode_js_data(data) when is_binary(data), do: {:ok, data} diff --git a/lib/livebook/notebook.ex b/lib/livebook/notebook.ex index f41785455..3e0c1997d 100644 --- a/lib/livebook/notebook.ex +++ b/lib/livebook/notebook.ex @@ -590,9 +590,14 @@ defmodule Livebook.Notebook do defp find_assets_info_in_outputs(outputs, hash) do Enum.find_value(outputs, fn - {_idx, {:js, %{js_view: %{assets: %{hash: ^hash} = assets_info}}}} -> assets_info - {_idx, {:frame, outputs, _}} -> find_assets_info_in_outputs(outputs, hash) - _ -> nil + {_idx, {:js, %{js_view: %{assets: %{hash: ^hash} = assets_info}}}} -> + assets_info + + {_idx, {type, outputs, _}} when type in [:frame, :tabs, :grid] -> + find_assets_info_in_outputs(outputs, hash) + + _ -> + nil end) end @@ -650,11 +655,11 @@ defmodule Livebook.Notebook do defp apply_frame_update(_outputs, new_outputs, :replace), do: new_outputs defp apply_frame_update(outputs, new_outputs, :append), do: new_outputs ++ outputs + defp add_output(outputs, {_idx, :ignored}), do: outputs + defp add_output([], {idx, {:stdout, text}}), do: [{idx, {:stdout, normalize_stdout(text)}}] - defp add_output(outputs, {_idx, :ignored}), do: outputs - defp add_output([], output), do: [output] # Session clients prune stdout content and handle subsequent @@ -696,9 +701,9 @@ defmodule Livebook.Notebook do Enum.map_reduce(outputs, counter, &index_output/2) end - defp index_output({:frame, outputs, info}, counter) do + defp index_output({type, outputs, info}, counter) when type in [:frame, :tabs, :grid] do {outputs, counter} = index_outputs(outputs, counter) - {{counter, {:frame, outputs, info}}, counter + 1} + {{counter, {type, outputs, info}}, counter + 1} end defp index_output(output, counter) do @@ -721,7 +726,8 @@ defmodule Livebook.Notebook do [output] end - defp do_find_frame_outputs({_idx, {:frame, outputs, _info}}, ref) do + defp do_find_frame_outputs({_idx, {type, outputs, _info}}, ref) + when type in [:frame, :tabs, :grid] do Enum.flat_map(outputs, &do_find_frame_outputs(&1, ref)) end @@ -758,6 +764,18 @@ defmodule Livebook.Notebook do do_prune_outputs(outputs, [{idx, {:frame, prune_outputs(frame_outputs), info}} | acc]) end + # Keep layout output and its relevant contents + defp do_prune_outputs([{idx, {type, tabs_outputs, _info}} | outputs], acc) + when type in [:tabs, :grid] do + case prune_outputs(tabs_outputs) do + [] -> + do_prune_outputs(outputs, acc) + + pruned_tabs_outputs -> + do_prune_outputs(outputs, [{idx, {type, pruned_tabs_outputs, :__pruned__}} | acc]) + end + end + # Keep outputs that get re-rendered defp do_prune_outputs([{idx, output} | outputs], acc) when elem(output, 0) in [:input, :control, :error] do diff --git a/lib/livebook/notebook/cell.ex b/lib/livebook/notebook/cell.ex index a3639d48d..5d91c2fde 100644 --- a/lib/livebook/notebook/cell.ex +++ b/lib/livebook/notebook/cell.ex @@ -62,7 +62,7 @@ defmodule Livebook.Notebook.Cell do Keyword.values(fields) end - def find_inputs_in_output({_idx, {:frame, outputs, _}}) do + def find_inputs_in_output({_idx, {type, outputs, _}}) when type in [:frame, :tabs, :grid] do Enum.flat_map(outputs, &find_inputs_in_output/1) end diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index e127b0a05..394c2b991 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -52,6 +52,10 @@ defprotocol Livebook.Runtime do | {:js, info :: map()} # Outputs placeholder | {:frame, outputs :: list(output()), info :: map()} + # Outputs in tabs + | {:tabs, outputs :: list(output()), info :: map()} + # Outputs in grid + | {:grid, outputs :: list(output()), info :: map()} # An input field | {:input, attrs :: map()} # A control element diff --git a/lib/livebook_web/live/output.ex b/lib/livebook_web/live/output.ex index 330dcfb5a..f0db98403 100644 --- a/lib/livebook_web/live/output.ex +++ b/lib/livebook_web/live/output.ex @@ -3,6 +3,7 @@ defmodule LivebookWeb.Output do import LivebookWeb.Helpers + alias Phoenix.LiveView.JS alias LivebookWeb.Output @doc """ @@ -35,6 +36,8 @@ defmodule LivebookWeb.Output do defp border?(_output), do: false defp wrapper?({:frame, _outputs, _info}), do: true + defp wrapper?({:tabs, _tabs, _info}), do: true + defp wrapper?({:grid, _tabs, _info}), do: true defp wrapper?(_output), do: false defp render_output({:stdout, text}, %{id: id}) do @@ -88,6 +91,126 @@ defmodule LivebookWeb.Output do ) end + defp render_output({:tabs, outputs, info}, %{ + id: id, + input_values: input_values, + session_id: session_id, + socket: socket + }) do + {labels, active_idx} = + if info == :__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 + } + + # After pruning we don't render labels and we render only those + # outputs that are kept during pruning + + ~H""" +
+
+ <%= for {output_idx, label} <- @labels do %> + + <% end %> +
+
+ <%= for {output_idx, output} <- @outputs do %> + <% # We use data-keep-attribute, because we know active_idx only on the first render %> +
+ <.outputs + outputs={[{output_idx, output}]} + dom_id_map={%{}} + socket={@socket} + session_id={@session_id} + input_values={@input_values} + /> +
+ <% end %> +
+
+ """ + end + + defp render_output({:grid, outputs, info}, %{ + id: id, + input_values: input_values, + session_id: session_id, + socket: socket + }) do + style = + if info == :__pruned__ do + nil + else + columns = info[:columns] || 1 + "grid-template-columns: repeat(#{columns}, minmax(0, 1fr));" + end + + assigns = %{ + id: id, + style: style, + outputs: outputs, + socket: socket, + session_id: session_id, + input_values: input_values + } + + ~H""" +
+
+ <%= for {output_idx, output} <- @outputs do %> +
+ <.outputs + outputs={[{output_idx, output}]} + dom_id_map={%{}} + socket={@socket} + session_id={@session_id} + input_values={@input_values} + /> +
+ <% end %> +
+
+ """ + end + defp render_output({:input, attrs}, %{id: id, input_values: input_values}) do live_component(Output.InputComponent, id: id, attrs: attrs, input_values: input_values) end diff --git a/lib/livebook_web/live/sidebar_helpers.ex b/lib/livebook_web/live/sidebar_helpers.ex index da96ca1c2..a6680a9f1 100644 --- a/lib/livebook_web/live/sidebar_helpers.ex +++ b/lib/livebook_web/live/sidebar_helpers.ex @@ -70,11 +70,11 @@ defmodule LivebookWeb.SidebarHelpers do <% end %>