mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-19 01:46:19 +08:00
Use start_async/3 for async file operations and migrate to phx-update="stream" (#2309)
This commit is contained in:
parent
c23372bf53
commit
b1ce874826
17 changed files with 367 additions and 271 deletions
|
@ -867,7 +867,7 @@ defmodule Livebook.Notebook do
|
|||
do_prune_outputs(outputs, appendable?, acc)
|
||||
|
||||
pruned_tabs_outputs ->
|
||||
output = %{output | outputs: pruned_tabs_outputs, labels: :__pruned__}
|
||||
output = %{output | outputs: pruned_tabs_outputs}
|
||||
do_prune_outputs(outputs, appendable?, [{idx, output} | acc])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -140,22 +140,14 @@ defmodule LivebookWeb.AppSessionLive do
|
|||
</h1>
|
||||
</div>
|
||||
<div class="pt-4 flex flex-col gap-6">
|
||||
<div
|
||||
<.live_component
|
||||
:for={cell_view <- @data_view.cell_views}
|
||||
class="empty:hidden"
|
||||
id={"outputs-#{cell_view.id}-#{cell_view.outputs_batch_number}"}
|
||||
phx-update="append"
|
||||
>
|
||||
<LivebookWeb.Output.outputs
|
||||
outputs={cell_view.outputs}
|
||||
dom_id_map={%{}}
|
||||
session_id={@session.id}
|
||||
session_pid={@session.pid}
|
||||
client_id={@client_id}
|
||||
cell_id={cell_view.id}
|
||||
input_views={cell_view.input_views}
|
||||
/>
|
||||
</div>
|
||||
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",
|
||||
|
@ -413,7 +405,7 @@ defmodule LivebookWeb.AppSessionLive do
|
|||
|
||||
for {{idx, frame}, cell} <- Notebook.find_frame_outputs(data.notebook, ref) do
|
||||
send_update(LivebookWeb.Output.FrameComponent,
|
||||
id: "output-#{idx}",
|
||||
id: "outputs-#{idx}-output",
|
||||
outputs: frame.outputs,
|
||||
update_type: update_type,
|
||||
input_views: input_views_for_cell(cell, data, changed_input_ids)
|
||||
|
@ -434,7 +426,7 @@ defmodule LivebookWeb.AppSessionLive do
|
|||
:markdown -> LivebookWeb.Output.MarkdownComponent
|
||||
end
|
||||
|
||||
send_update(module, id: "output-#{idx}", text: output.text)
|
||||
send_update(module, id: "outputs-#{idx}-output", text: output.text)
|
||||
data_view
|
||||
|
||||
_ ->
|
||||
|
|
43
lib/livebook_web/live/app_session_live/cell_outputs.ex
Normal file
43
lib/livebook_web/live/app_session_live/cell_outputs.ex
Normal file
|
@ -0,0 +1,43 @@
|
|||
defmodule LivebookWeb.AppSessionLive.CellOutputsComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, stream(socket, :outputs, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
stream_items =
|
||||
for {idx, output} <- Enum.reverse(assigns.cell_view.outputs) do
|
||||
%{id: Integer.to_string(idx), idx: idx, output: output}
|
||||
end
|
||||
|
||||
socket = stream(socket, :outputs, stream_items)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"#{@id}-#{@cell_view.outputs_batch_number}"}
|
||||
phx-update="stream"
|
||||
class="empty:hidden"
|
||||
phx-no-format
|
||||
><LivebookWeb.Output.output
|
||||
:for={{dom_id, output} <- @streams.outputs}
|
||||
id={dom_id}
|
||||
output={output.output}
|
||||
session_id={@session.id}
|
||||
session_pid={@session.pid}
|
||||
client_id={@client_id}
|
||||
cell_id={@cell_view.id}
|
||||
input_views={@cell_view.input_views}
|
||||
/></div>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -6,27 +6,21 @@ defmodule LivebookWeb.Output do
|
|||
alias LivebookWeb.Output
|
||||
|
||||
@doc """
|
||||
Renders a list of cell outputs.
|
||||
Renders a single cell output.
|
||||
"""
|
||||
attr :outputs, :list, required: true
|
||||
attr :id, :string, required: true
|
||||
attr :output, :map, 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
|
||||
def output(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}",
|
||||
<div id={@id} class="max-w-full" data-el-output data-border={border?(@output)}>
|
||||
<%= render_output(@output, %{
|
||||
id: "#{@id}-output",
|
||||
session_id: @session_id,
|
||||
session_pid: @session_pid,
|
||||
input_views: @input_views,
|
||||
|
@ -45,7 +39,6 @@ defmodule LivebookWeb.Output do
|
|||
|
||||
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"""
|
||||
|
@ -55,7 +48,6 @@ defmodule LivebookWeb.Output do
|
|||
|
||||
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"""
|
||||
|
@ -65,7 +57,6 @@ defmodule LivebookWeb.Output do
|
|||
|
||||
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"""
|
||||
|
@ -147,21 +138,8 @@ defmodule LivebookWeb.Output do
|
|||
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,
|
||||
|
@ -171,47 +149,18 @@ defmodule LivebookWeb.Output do
|
|||
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>
|
||||
<.live_component
|
||||
module={Output.TabsComponent}
|
||||
id={@id}
|
||||
outputs={@outputs}
|
||||
labels={@labels}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
input_views={@input_views}
|
||||
client_id={@client_id}
|
||||
cell_id={@cell_id}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -236,26 +185,18 @@ defmodule LivebookWeb.Output do
|
|||
}
|
||||
|
||||
~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>
|
||||
<.live_component
|
||||
module={Output.GridComponent}
|
||||
id={@id}
|
||||
outputs={@outputs}
|
||||
columns={@columns}
|
||||
gap={@gap}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
input_views={@input_views}
|
||||
client_id={@client_id}
|
||||
cell_id={@cell_id}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
@ -3,8 +3,7 @@ defmodule LivebookWeb.Output.FrameComponent do
|
|||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, counter: 0, output_count: 0, persistent_id_map: %{}),
|
||||
temporary_assigns: [outputs: []]}
|
||||
{:ok, stream(socket, :outputs, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -14,75 +13,47 @@ defmodule LivebookWeb.Output.FrameComponent do
|
|||
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
socket =
|
||||
if socket.assigns.counter == 0 do
|
||||
assign(socket,
|
||||
counter: 1,
|
||||
output_count: length(outputs),
|
||||
persistent_id_map: map_idx_to_persistent_id(outputs, socket.assigns.id)
|
||||
)
|
||||
else
|
||||
socket
|
||||
end
|
||||
socket = assign_new(socket, :num_outputs, fn -> length(outputs) end)
|
||||
|
||||
socket =
|
||||
case update_type do
|
||||
nil ->
|
||||
assign(socket, outputs: outputs)
|
||||
stream(socket, :outputs, stream_items(outputs))
|
||||
|
||||
:replace ->
|
||||
prev_output_count = socket.assigns.output_count
|
||||
prev_persistent_id_map = socket.assigns.persistent_id_map
|
||||
|
||||
output_count = length(outputs)
|
||||
persistent_id_map = map_idx_to_persistent_id(outputs, socket.assigns.id)
|
||||
|
||||
socket =
|
||||
assign(socket,
|
||||
outputs: outputs,
|
||||
output_count: output_count,
|
||||
persistent_id_map: persistent_id_map
|
||||
)
|
||||
|
||||
less_outputs? = prev_output_count > output_count
|
||||
appended_outputs? = prev_output_count > map_size(prev_persistent_id_map)
|
||||
|
||||
# If there are outputs that we need to remove, increase the counter.
|
||||
# Otherwise we reuse DOM element ids via persistent_id_map
|
||||
if less_outputs? or appended_outputs? do
|
||||
update(socket, :counter, &(&1 + 1))
|
||||
else
|
||||
socket
|
||||
end
|
||||
socket
|
||||
|> assign(num_outputs: length(outputs))
|
||||
|> stream(:outputs, stream_items(outputs), reset: true)
|
||||
|
||||
:append ->
|
||||
socket
|
||||
|> assign(:outputs, outputs)
|
||||
|> update(:output_count, &(length(outputs) + &1))
|
||||
|> update(:num_outputs, &(length(outputs) + &1))
|
||||
|> stream(:outputs, stream_items(outputs))
|
||||
end
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
defp map_idx_to_persistent_id(outputs, root_id) do
|
||||
outputs
|
||||
|> Enum.with_index()
|
||||
|> Map.new(fn {{output_idx, _}, idx} -> {output_idx, "#{root_id}-#{idx}"} end)
|
||||
defp stream_items(outputs) do
|
||||
for {idx, output} <- Enum.reverse(outputs) do
|
||||
%{id: Integer.to_string(idx), idx: idx, output: output}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={"frame-output-#{@id}"}>
|
||||
<%= if @output_count == 0 do %>
|
||||
<div id={@id}>
|
||||
<%= if @num_outputs == 0 do %>
|
||||
<div :if={@placeholder} class="text-gray-300 p-4 rounded-lg border border-gray-200">
|
||||
Nothing here...
|
||||
</div>
|
||||
<% else %>
|
||||
<div id={"frame-outputs-#{@id}-#{@counter}"} phx-update="append">
|
||||
<LivebookWeb.Output.outputs
|
||||
outputs={@outputs}
|
||||
dom_id_map={@persistent_id_map}
|
||||
<div id={"#{@id}-outputs"} phx-update="stream">
|
||||
<LivebookWeb.Output.output
|
||||
:for={{dom_id, output} <- @streams.outputs}
|
||||
id={dom_id}
|
||||
output={output.output}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
input_views={@input_views}
|
||||
|
|
51
lib/livebook_web/live/output/grid_component.ex
Normal file
51
lib/livebook_web/live/output/grid_component.ex
Normal file
|
@ -0,0 +1,51 @@
|
|||
defmodule LivebookWeb.Output.GridComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, stream(socket, :outputs, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{outputs, assigns} = Map.pop!(assigns, :outputs)
|
||||
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
stream_items =
|
||||
for {idx, output} <- Enum.reverse(outputs) do
|
||||
id = "#{socket.assigns.id}-grid-item-#{idx}"
|
||||
%{id: id, idx: idx, output: output}
|
||||
end
|
||||
|
||||
socket = stream(socket, :outputs, stream_items)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~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="stream"
|
||||
>
|
||||
<div :for={{dom_id, output} <- @streams.outputs} id={dom_id}>
|
||||
<LivebookWeb.Output.output
|
||||
id={"#{dom_id}-output"}
|
||||
output={output.output}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
input_views={@input_views}
|
||||
client_id={@client_id}
|
||||
cell_id={@cell_id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -3,17 +3,21 @@ defmodule LivebookWeb.Output.MarkdownComponent do
|
|||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, allowed_uri_schemes: Livebook.Config.allowed_uri_schemes(), chunks: 0),
|
||||
temporary_assigns: [text: nil]}
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(allowed_uri_schemes: Livebook.Config.allowed_uri_schemes())
|
||||
|> stream(:chunks, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{text, assigns} = Map.pop(assigns, :text)
|
||||
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
if text do
|
||||
{:ok, socket |> assign(text: text) |> update(:chunks, &(&1 + 1))}
|
||||
chunk = %{id: Livebook.Utils.random_id(), text: text}
|
||||
{:ok, stream_insert(socket, :chunks, chunk)}
|
||||
else
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -23,21 +27,19 @@ defmodule LivebookWeb.Output.MarkdownComponent do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"markdown-renderer-#{@id}"}
|
||||
id={@id}
|
||||
phx-hook="MarkdownRenderer"
|
||||
data-base-path={~p"/sessions/#{@session_id}"}
|
||||
data-allowed-uri-schemes={Enum.join(@allowed_uri_schemes, ",")}
|
||||
>
|
||||
<div
|
||||
data-template
|
||||
id={"markdown-renderer-#{@id}-template"}
|
||||
id={"#{@id}-template"}
|
||||
class="text-gray-700 whitespace-pre-wrap hidden"
|
||||
phx-update="append"
|
||||
phx-update="stream"
|
||||
phx-no-format
|
||||
><span :if={@text} id={"plain-text-#{@id}-chunk-#{@chunks}"}><%=
|
||||
@text %></span></div>
|
||||
<div data-content class="markdown" id={"markdown-rendered-#{@id}-content"} phx-update="ignore">
|
||||
</div>
|
||||
><span :for={{dom_id, chunk}<- @streams.chunks} id={dom_id}><%= chunk.text %></span></div>
|
||||
<div data-content class="markdown" id={"#{@id}-content"} phx-update="ignore"></div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
@ -3,16 +3,18 @@ defmodule LivebookWeb.Output.PlainTextComponent do
|
|||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, chunks: 0), temporary_assigns: [text: nil]}
|
||||
{:ok, stream(socket, :chunks, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{text, assigns} = Map.pop(assigns, :text)
|
||||
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
if text do
|
||||
{:ok, socket |> assign(text: text) |> update(:chunks, &(&1 + 1))}
|
||||
chunk = %{id: Livebook.Utils.random_id(), text: text}
|
||||
{:ok, stream_insert(socket, :chunks, chunk)}
|
||||
else
|
||||
{:ok, socket}
|
||||
end
|
||||
|
@ -21,13 +23,8 @@ defmodule LivebookWeb.Output.PlainTextComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"plain-text-#{@id}"}
|
||||
class="text-gray-700 whitespace-pre-wrap"
|
||||
phx-update="append"
|
||||
phx-no-format
|
||||
><span :if={@text} id={"plain-text-#{@id}-chunk-#{@chunks}"}><%=
|
||||
@text %></span></div>
|
||||
<div id={@id} class="text-gray-700 whitespace-pre-wrap" phx-update="stream" phx-no-format><span
|
||||
:for={{dom_id, chunk}<- @streams.chunks} id={dom_id}><%= chunk.text %></span></div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
77
lib/livebook_web/live/output/tabs_component.ex
Normal file
77
lib/livebook_web/live/output/tabs_component.ex
Normal file
|
@ -0,0 +1,77 @@
|
|||
defmodule LivebookWeb.Output.TabsComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, stream(socket, :outputs, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{labels, assigns} = Map.pop!(assigns, :labels)
|
||||
{outputs, assigns} = Map.pop!(assigns, :outputs)
|
||||
|
||||
# We compute these only on initial render, when we have all outputs
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:labels, fn ->
|
||||
Enum.zip_with(labels, outputs, fn label, {output_idx, _} -> {output_idx, label} end)
|
||||
end)
|
||||
|> assign_new(:active_idx, fn ->
|
||||
get_in(outputs, [Access.at(0), Access.elem(0)])
|
||||
end)
|
||||
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
stream_items =
|
||||
for {idx, output} <- Enum.reverse(outputs) do
|
||||
id = "#{socket.assigns.id}-tab-content-#{idx}"
|
||||
%{id: id, idx: idx, output: output}
|
||||
end
|
||||
|
||||
socket = stream(socket, :outputs, stream_items)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
<div class="tabs mb-2" id={"#{@id}-tabs"}>
|
||||
<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} [data-tab-content]:not(.hidden)")
|
||||
|> JS.remove_class("hidden", to: ~s/##{@id} [data-tab-content="#{output_idx}"]/)
|
||||
}
|
||||
>
|
||||
<%= label %>
|
||||
</button>
|
||||
</div>
|
||||
<div id={"#{@id}-tab-contents"} phx-update="stream">
|
||||
<div
|
||||
:for={{dom_id, output} <- @streams.outputs}
|
||||
id={dom_id}
|
||||
data-tab-content={output.idx}
|
||||
class={[output.idx != @active_idx && "hidden"]}
|
||||
>
|
||||
<LivebookWeb.Output.output
|
||||
id={"#{dom_id}-output"}
|
||||
output={output.output}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
input_views={@input_views}
|
||||
client_id={@client_id}
|
||||
cell_id={@cell_id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -3,8 +3,10 @@ defmodule LivebookWeb.Output.TerminalTextComponent do
|
|||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, modifiers: [], last_line: nil, last_html_line: nil),
|
||||
temporary_assigns: [html_lines: []]}
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(modifiers: [], last_line: nil, last_html_line: nil)
|
||||
|> stream(:html_lines, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -28,9 +30,13 @@ defmodule LivebookWeb.Output.TerminalTextComponent do
|
|||
|
||||
{html_lines, [last_html_line]} = Enum.split(html_lines, -1)
|
||||
|
||||
stream_items =
|
||||
for html_line <- html_lines, do: %{id: Livebook.Utils.random_id(), html: html_line}
|
||||
|
||||
socket = stream(socket, :html_lines, stream_items)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
html_lines: html_lines,
|
||||
last_html_line: last_html_line,
|
||||
last_line: last_line,
|
||||
modifiers: modifiers
|
||||
|
@ -44,7 +50,7 @@ defmodule LivebookWeb.Output.TerminalTextComponent do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"virtualized-text-#{@id}"}
|
||||
id={@id}
|
||||
class="relative"
|
||||
phx-hook="VirtualizedLines"
|
||||
data-max-height="300"
|
||||
|
@ -54,17 +60,17 @@ defmodule LivebookWeb.Output.TerminalTextComponent do
|
|||
>
|
||||
<% # Note 1: We add a newline to each element, so that multiple lines can be copied properly as element.textContent %>
|
||||
<% # Note 2: We glue the tags together to avoid inserting unintended whitespace %>
|
||||
<div data-template class="hidden" id={"virtualized-text-#{@id}-template"} phx-no-format><div
|
||||
id={"virtualized-text-#{@id}-template-append"}
|
||||
phx-update="append"
|
||||
><%= for html_line <- @html_lines do %><div data-line id={Livebook.Utils.random_id()}><%= [
|
||||
html_line,
|
||||
"\n"
|
||||
] %></div><% end %></div><div data-line><%= @last_html_line %></div></div>
|
||||
<div data-template class="hidden" id={"#{@id}-template"} phx-no-format><div
|
||||
id={"#{@id}-template-append"}
|
||||
phx-update="stream"
|
||||
><div :for={{dom_id, html_line} <- @streams.html_lines} id={dom_id} data-line><%= [
|
||||
html_line.html,
|
||||
"\n"
|
||||
] %></div></div><div data-line><%= @last_html_line %></div></div>
|
||||
<div
|
||||
data-content
|
||||
class="overflow-auto whitespace-pre font-editor text-gray-500 tiny-scrollbar"
|
||||
id={"virtualized-text-#{@id}-content"}
|
||||
id={"#{@id}-content"}
|
||||
phx-update="ignore"
|
||||
>
|
||||
</div>
|
||||
|
@ -72,7 +78,7 @@ defmodule LivebookWeb.Output.TerminalTextComponent do
|
|||
<button
|
||||
class="icon-button bg-gray-100"
|
||||
data-el-clipcopy
|
||||
phx-click={JS.dispatch("lb:clipcopy", to: "#virtualized-text-#{@id}-template")}
|
||||
phx-click={JS.dispatch("lb:clipcopy", to: "##{@id}-template")}
|
||||
>
|
||||
<.remix_icon icon="clipboard-line" class="text-lg" />
|
||||
</button>
|
||||
|
|
|
@ -2862,7 +2862,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
|
||||
for {{idx, frame}, cell} <- Notebook.find_frame_outputs(data.notebook, ref) do
|
||||
send_update(LivebookWeb.Output.FrameComponent,
|
||||
id: "output-#{idx}",
|
||||
id: "outputs-#{idx}-output",
|
||||
outputs: frame.outputs,
|
||||
update_type: update_type,
|
||||
# Note that we are not updating data_view to avoid re-render,
|
||||
|
@ -2886,7 +2886,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
:markdown -> LivebookWeb.Output.MarkdownComponent
|
||||
end
|
||||
|
||||
send_update(module, id: "output-#{idx}", text: output.text)
|
||||
send_update(module, id: "outputs-#{idx}-output", text: output.text)
|
||||
data_view
|
||||
|
||||
_ ->
|
||||
|
|
|
@ -34,18 +34,6 @@ defmodule LivebookWeb.SessionLive.AddFileEntryFileComponent do
|
|||
{:ok, assign(socket, file: file, file_info: file_info, changeset: changeset)}
|
||||
end
|
||||
|
||||
def update(%{file_entry_result: file_entry_result}, socket) do
|
||||
socket = assign(socket, fetching: false)
|
||||
|
||||
case file_entry_result do
|
||||
{:ok, file_entry} ->
|
||||
{:ok, add_file_entry(socket, file_entry)}
|
||||
|
||||
{:error, message} ->
|
||||
{:ok, assign(socket, error_message: Livebook.Utils.upcase_first(message))}
|
||||
end
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|
@ -125,7 +113,13 @@ defmodule LivebookWeb.SessionLive.AddFileEntryFileComponent do
|
|||
file_entry = %{name: data.name, type: :file, file: socket.assigns.file}
|
||||
|
||||
if data.copy do
|
||||
async_create_attachment_file_entry(socket, file_entry)
|
||||
session = socket.assigns.session
|
||||
|
||||
socket =
|
||||
start_async(socket, :create_attachment_file_entry, fn ->
|
||||
Livebook.Session.to_attachment_file_entry(session, file_entry)
|
||||
end)
|
||||
|
||||
{:noreply, assign(socket, fetching: true, error_message: nil)}
|
||||
else
|
||||
{:noreply, add_file_entry(socket, file_entry)}
|
||||
|
@ -136,15 +130,17 @@ defmodule LivebookWeb.SessionLive.AddFileEntryFileComponent do
|
|||
end
|
||||
end
|
||||
|
||||
defp async_create_attachment_file_entry(socket, file_entry) do
|
||||
pid = self()
|
||||
id = socket.assigns.id
|
||||
session = socket.assigns.session
|
||||
@impl true
|
||||
def handle_async(:create_attachment_file_entry, {:ok, file_entry_result}, socket) do
|
||||
socket = assign(socket, fetching: false)
|
||||
|
||||
Task.Supervisor.async_nolink(Livebook.TaskSupervisor, fn ->
|
||||
file_entry_result = Livebook.Session.to_attachment_file_entry(session, file_entry)
|
||||
send_update(pid, __MODULE__, id: id, file_entry_result: file_entry_result)
|
||||
end)
|
||||
case file_entry_result do
|
||||
{:ok, file_entry} ->
|
||||
{:noreply, add_file_entry(socket, file_entry)}
|
||||
|
||||
{:error, message} ->
|
||||
{:noreply, assign(socket, error_message: Livebook.Utils.upcase_first(message))}
|
||||
end
|
||||
end
|
||||
|
||||
defp add_file_entry(socket, file_entry) do
|
||||
|
|
|
@ -8,23 +8,6 @@ defmodule LivebookWeb.SessionLive.AddFileEntryUrlComponent do
|
|||
{:ok, assign(socket, changeset: changeset(), error_message: nil, fetching: false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(%{file_entry_result: file_entry_result}, socket) do
|
||||
socket = assign(socket, fetching: false)
|
||||
|
||||
case file_entry_result do
|
||||
{:ok, file_entry} ->
|
||||
{:ok, add_file_entry(socket, file_entry)}
|
||||
|
||||
{:error, message} ->
|
||||
{:ok, assign(socket, error_message: Livebook.Utils.upcase_first(message))}
|
||||
end
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
{:ok, assign(socket, assigns)}
|
||||
end
|
||||
|
||||
defp changeset(attrs \\ %{}) do
|
||||
data = %{url: nil, name: nil, copy: false}
|
||||
types = %{url: :string, name: :string, copy: :boolean}
|
||||
|
@ -137,7 +120,13 @@ defmodule LivebookWeb.SessionLive.AddFileEntryUrlComponent do
|
|||
file_entry = %{name: data.name, type: :url, url: data.url}
|
||||
|
||||
if data.copy do
|
||||
async_create_attachment_file_entry(socket, file_entry)
|
||||
session = socket.assigns.session
|
||||
|
||||
socket =
|
||||
start_async(socket, :file_entry, fn ->
|
||||
Livebook.Session.to_attachment_file_entry(session, file_entry)
|
||||
end)
|
||||
|
||||
{:noreply, assign(socket, fetching: true, error_message: nil)}
|
||||
else
|
||||
{:noreply, add_file_entry(socket, file_entry)}
|
||||
|
@ -148,15 +137,17 @@ defmodule LivebookWeb.SessionLive.AddFileEntryUrlComponent do
|
|||
end
|
||||
end
|
||||
|
||||
defp async_create_attachment_file_entry(socket, file_entry) do
|
||||
pid = self()
|
||||
id = socket.assigns.id
|
||||
session = socket.assigns.session
|
||||
@impl true
|
||||
def handle_async(:file_entry, {:ok, file_entry_result}, socket) do
|
||||
socket = assign(socket, fetching: false)
|
||||
|
||||
Task.Supervisor.async_nolink(Livebook.TaskSupervisor, fn ->
|
||||
file_entry_result = Livebook.Session.to_attachment_file_entry(session, file_entry)
|
||||
send_update(pid, __MODULE__, id: id, file_entry_result: file_entry_result)
|
||||
end)
|
||||
case file_entry_result do
|
||||
{:ok, file_entry} ->
|
||||
{:noreply, add_file_entry(socket, file_entry)}
|
||||
|
||||
{:error, message} ->
|
||||
{:noreply, assign(socket, error_message: Livebook.Utils.upcase_first(message))}
|
||||
end
|
||||
end
|
||||
|
||||
defp add_file_entry(socket, file_entry) do
|
||||
|
|
|
@ -3,6 +3,32 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, stream(socket, :outputs, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
socket =
|
||||
case assigns.cell_view do
|
||||
%{eval: %{outputs: outputs}} ->
|
||||
stream_items =
|
||||
for {idx, output} <- Enum.reverse(outputs) do
|
||||
%{id: Integer.to_string(idx), idx: idx, output: output}
|
||||
end
|
||||
|
||||
stream(socket, :outputs, stream_items)
|
||||
|
||||
%{} ->
|
||||
socket
|
||||
end
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
@ -99,6 +125,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
</div>
|
||||
<.doctest_summary cell_id={@cell_view.id} doctest_summary={@cell_view.eval.doctest_summary} />
|
||||
<.evaluation_outputs
|
||||
outputs={@streams.outputs}
|
||||
cell_view={@cell_view}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
|
@ -146,6 +173,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
</div>
|
||||
</div>
|
||||
<.evaluation_outputs
|
||||
outputs={@streams.outputs}
|
||||
cell_view={@cell_view}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
|
@ -249,6 +277,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
</div>
|
||||
</div>
|
||||
<.evaluation_outputs
|
||||
outputs={@streams.outputs}
|
||||
cell_view={@cell_view}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
|
@ -634,11 +663,12 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
class="flex flex-col"
|
||||
data-el-outputs-container
|
||||
id={"outputs-#{@cell_view.id}-#{@cell_view.eval.outputs_batch_number}"}
|
||||
phx-update="append"
|
||||
phx-update="stream"
|
||||
>
|
||||
<LivebookWeb.Output.outputs
|
||||
outputs={@cell_view.eval.outputs}
|
||||
dom_id_map={%{}}
|
||||
<LivebookWeb.Output.output
|
||||
:for={{dom_id, output} <- @outputs}
|
||||
id={dom_id}
|
||||
output={output.output}
|
||||
session_id={@session_id}
|
||||
session_pid={@session_pid}
|
||||
client_id={@client_id}
|
||||
|
|
|
@ -9,20 +9,6 @@ defmodule LivebookWeb.SessionLive.FilesListComponent do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def update(%{transfer_file_entry_result: {name, file_entry_result}}, socket) do
|
||||
case file_entry_result do
|
||||
{:ok, file_entry} ->
|
||||
Livebook.Session.add_file_entries(socket.assigns.session.pid, [file_entry])
|
||||
|
||||
{:error, message} ->
|
||||
send(self(), {:put_flash, :error, Livebook.Utils.upcase_first(message)})
|
||||
end
|
||||
|
||||
socket = update(socket, :transferring_file_entry_names, &MapSet.delete(&1, name))
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
|
||||
|
@ -326,22 +312,19 @@ defmodule LivebookWeb.SessionLive.FilesListComponent do
|
|||
end
|
||||
|
||||
def handle_event("transfer_file_entry", %{"name" => name}, socket) do
|
||||
if file_entry = find_file_entry(socket, name) do
|
||||
pid = self()
|
||||
id = socket.assigns.id
|
||||
session = socket.assigns.session
|
||||
socket =
|
||||
if file_entry = find_file_entry(socket, name) do
|
||||
session = socket.assigns.session
|
||||
|
||||
Task.Supervisor.async_nolink(Livebook.TaskSupervisor, fn ->
|
||||
file_entry_result = Livebook.Session.to_attachment_file_entry(session, file_entry)
|
||||
|
||||
send_update(pid, __MODULE__,
|
||||
id: id,
|
||||
transfer_file_entry_result: {name, file_entry_result}
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
socket = update(socket, :transferring_file_entry_names, &MapSet.put(&1, name))
|
||||
socket
|
||||
|> start_async(:transfer_file_entry, fn ->
|
||||
file_entry_result = Livebook.Session.to_attachment_file_entry(session, file_entry)
|
||||
{name, file_entry_result}
|
||||
end)
|
||||
|> update(:transferring_file_entry_names, &MapSet.put(&1, name))
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
@ -354,6 +337,21 @@ defmodule LivebookWeb.SessionLive.FilesListComponent do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_async(:transfer_file_entry, {:ok, {name, file_entry_result}}, socket) do
|
||||
case file_entry_result do
|
||||
{:ok, file_entry} ->
|
||||
Livebook.Session.add_file_entries(socket.assigns.session.pid, [file_entry])
|
||||
|
||||
{:error, message} ->
|
||||
send(self(), {:put_flash, :error, Livebook.Utils.upcase_first(message)})
|
||||
end
|
||||
|
||||
socket = update(socket, :transferring_file_entry_names, &MapSet.delete(&1, name))
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp find_file_entry(socket, name) do
|
||||
Enum.find(socket.assigns.file_entries, &(&1.name == name))
|
||||
end
|
||||
|
|
3
mix.exs
3
mix.exs
|
@ -95,7 +95,8 @@ defmodule Livebook.MixProject do
|
|||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.7.8"},
|
||||
{:phoenix_live_view, "~> 0.20.1"},
|
||||
# {:phoenix_live_view, "~> 0.20.1"},
|
||||
{:phoenix_live_view, github: "phoenixframework/phoenix_live_view", override: true},
|
||||
{:phoenix_html, "~> 3.0"},
|
||||
{:phoenix_live_dashboard, "~> 0.8.0"},
|
||||
{:telemetry_metrics, "~> 0.4"},
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -31,7 +31,7 @@
|
|||
"phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.2", "b9e33c950d1ed98494bfbde1c34c6e51c8a4214f3bea3f07ca9a510643ee1387", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "67a598441b5f583d301a77e0298719f9654887d3d8bf14e80ff0b6acf887ef90"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.1", "92a37acf07afca67ac98bd326532ba8f44ad7d4bdf3e4361b03f7f02594e5ae9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be494fd1215052729298b0e97d5c2ce8e719c00854b82cd8cf15c1cd7fcf6294"},
|
||||
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "2df6832a4f93a730e47f25bb39a57b6714f9da32", []},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
|
||||
"plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"},
|
||||
|
|
Loading…
Add table
Reference in a new issue