Use start_async/3 for async file operations and migrate to phx-update="stream" (#2309)

This commit is contained in:
Jonatan Kłosko 2023-10-27 20:49:46 +02:00 committed by GitHub
parent c23372bf53
commit b1ce874826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 367 additions and 271 deletions

View file

@ -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

View file

@ -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
_ ->

View 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

View file

@ -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

View file

@ -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}

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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>

View file

@ -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
_ ->

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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"},

View file

@ -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"},