mirror of
https://github.com/livebook-dev/livebook.git
synced 2026-03-16 19:24:35 +08:00
Improve runtimes UI (#655)
* Show reconnect for all runtime types when applicable * Make it clear which runtime is the default * Show Mix.install restart suggestion only for standalone runtimes * Fix tests not to rely on the default runtime tab
This commit is contained in:
parent
8fe8d27d3d
commit
4493a60380
16 changed files with 114 additions and 17 deletions
|
|
@ -199,4 +199,15 @@ defprotocol Livebook.Runtime do
|
|||
"""
|
||||
@spec duplicate(Runtime.t()) :: {:ok, Runtime.t()} | {:error, String.t()}
|
||||
def duplicate(runtime)
|
||||
|
||||
@doc """
|
||||
Returns true if the given runtime is self-contained.
|
||||
|
||||
A standalone runtime always starts fresh and frees all
|
||||
resources when terminated. This may not be the case for
|
||||
for runtimes that connect to an external running system
|
||||
and use it for code evaluation.
|
||||
"""
|
||||
@spec standalone?(Runtime.t()) :: boolean()
|
||||
def standalone?(runtime)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -71,4 +71,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
|
|||
{:error, :unreachable} -> {:error, "node #{inspect(runtime.node)} is unreachable"}
|
||||
end
|
||||
end
|
||||
|
||||
def standalone?(_runtime), do: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -97,4 +97,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
|||
def duplicate(_runtime) do
|
||||
Livebook.Runtime.ElixirStandalone.init()
|
||||
end
|
||||
|
||||
def standalone?(_runtime), do: true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -70,4 +70,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
|
|||
def duplicate(_runtime) do
|
||||
Livebook.Runtime.Embedded.init()
|
||||
end
|
||||
|
||||
def standalone?(_runtime), do: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -150,4 +150,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do
|
|||
def duplicate(runtime) do
|
||||
Livebook.Runtime.MixStandalone.init(runtime.project_path)
|
||||
end
|
||||
|
||||
def standalone?(_runtime), do: true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
id: section_view.id,
|
||||
index: index,
|
||||
session_id: @session.id,
|
||||
runtime: @data_view.runtime,
|
||||
section_view: section_view %>
|
||||
<% end %>
|
||||
<div style="height: 80vh"></div>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
use LivebookWeb, :live_view
|
||||
|
||||
alias Livebook.{Session, Runtime, Utils}
|
||||
alias LivebookWeb.SessionLive.RuntimeHelpers
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
end
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
session: session,
|
||||
current_runtime: current_runtime,
|
||||
error_message: nil,
|
||||
data: initial_data(current_runtime)
|
||||
)}
|
||||
|
|
@ -22,6 +28,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
<%= @error_message %>
|
||||
</div>
|
||||
<% end %>
|
||||
<RuntimeHelpers.default_runtime_note module={Runtime.Attached} />
|
||||
<p class="text-gray-700">
|
||||
Connect the session to an already running node
|
||||
and evaluate code in the context of that node.
|
||||
|
|
@ -57,13 +64,19 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
<button class="mt-5 button button-blue"
|
||||
type="submit"
|
||||
disabled={not data_valid?(@data)}>
|
||||
Connect
|
||||
<%= if(matching_runtime?(@current_runtime, @data), do: "Reconnect", else: "Connect") %>
|
||||
</button>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp matching_runtime?(%Runtime.Attached{} = runtime, data) do
|
||||
initial_data(runtime) == data
|
||||
end
|
||||
|
||||
defp matching_runtime?(_runtime, _data), do: false
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"data" => data}, socket) do
|
||||
{:noreply, assign(socket, data: data)}
|
||||
|
|
@ -76,7 +89,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
case Runtime.Attached.init(node, cookie) do
|
||||
{:ok, runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
{:noreply, assign(socket, data: data, error_message: nil)}
|
||||
{:noreply, assign(socket, data: initial_data(runtime), error_message: nil)}
|
||||
|
||||
{:error, error} ->
|
||||
message = runtime_error_to_message(error)
|
||||
|
|
@ -84,6 +97,13 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
|||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:operation, {:set_runtime, _pid, runtime}}, socket) do
|
||||
{:noreply, assign(socket, current_runtime: runtime)}
|
||||
end
|
||||
|
||||
def handle_info(_message, socket), do: {:noreply, socket}
|
||||
|
||||
defp initial_data(%Runtime.Attached{node: node, cookie: cookie}) do
|
||||
%{
|
||||
"name" => Atom.to_string(node),
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
|
||||
<%= if @cell_view.outputs != [] do %>
|
||||
<div class="mt-2">
|
||||
<.outputs cell_view={@cell_view} socket={@socket} />
|
||||
<.outputs cell_view={@cell_view} runtime={@runtime} socket={@socket} />
|
||||
</div>
|
||||
<% end %>
|
||||
</.cell_body>
|
||||
|
|
@ -431,7 +431,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
|
||||
<%= render_output(output, %{
|
||||
id: "cell-#{@cell_view.id}-evaluation#{evaluation_number(@cell_view.evaluation_status, @cell_view.number_of_evaluations)}-output#{index}",
|
||||
socket: @socket
|
||||
socket: @socket,
|
||||
runtime: @runtime
|
||||
}) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -483,17 +484,26 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
)
|
||||
end
|
||||
|
||||
defp render_output({:error, formatted, :runtime_restart_required}, %{}) do
|
||||
assigns = %{formatted: formatted}
|
||||
defp render_output({:error, formatted, :runtime_restart_required}, %{runtime: runtime})
|
||||
when runtime != nil do
|
||||
assigns = %{formatted: formatted, is_standalone: Livebook.Runtime.standalone?(runtime)}
|
||||
|
||||
~H"""
|
||||
<div class="flex flex-col space-y-4">
|
||||
<%= render_error_message_output(@formatted) %>
|
||||
<div>
|
||||
<button class="button button-gray" phx-click="restart_runtime">
|
||||
Restart runtime
|
||||
</button>
|
||||
</div>
|
||||
<%= if @is_standalone do %>
|
||||
<div>
|
||||
<button class="button button-gray" phx-click="restart_runtime">
|
||||
Restart runtime
|
||||
</button>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-red-600">
|
||||
<span class="font-semibold">Note:</span>
|
||||
This operation requires restarting the runtime, but we cannot
|
||||
do it automatically for the current runtime
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
|
|||
use LivebookWeb, :live_view
|
||||
|
||||
alias Livebook.{Session, Runtime}
|
||||
alias LivebookWeb.SessionLive.RuntimeHelpers
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do
|
||||
|
|
@ -21,10 +22,9 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
|
|||
<%= @error_message %>
|
||||
</div>
|
||||
<% end %>
|
||||
<RuntimeHelpers.default_runtime_note module={Runtime.ElixirStandalone} />
|
||||
<p class="text-gray-700">
|
||||
Start a new local node to handle code evaluation.
|
||||
This is the default runtime and is started automatically
|
||||
as soon as you evaluate the first cell.
|
||||
</p>
|
||||
<button class="button button-blue" phx-click="init">
|
||||
<%= if(matching_runtime?(@current_runtime), do: "Reconnect", else: "Connect") %>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
|||
use LivebookWeb, :live_view
|
||||
|
||||
alias Livebook.{Session, Runtime}
|
||||
alias LivebookWeb.SessionLive.RuntimeHelpers
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session" => session}, socket) do
|
||||
{:ok, assign(socket, session: session)}
|
||||
def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
end
|
||||
|
||||
{:ok, assign(socket, session: session, current_runtime: current_runtime)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<RuntimeHelpers.default_runtime_note module={Runtime.Embedded} />
|
||||
<p class="text-gray-700">
|
||||
Run the notebook code within the Livebook node itself.
|
||||
This is reserved for specific cases where there is no option
|
||||
|
|
@ -26,16 +32,26 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
|||
may interfere with code from another notebook.
|
||||
</p>
|
||||
<button class="button button-blue" phx-click="init">
|
||||
Connect
|
||||
<%= if(matching_runtime?(@current_runtime), do: "Reconnect", else: "Connect") %>
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp matching_runtime?(%Runtime.Embedded{}), do: true
|
||||
defp matching_runtime?(_runtime), do: false
|
||||
|
||||
@impl true
|
||||
def handle_event("init", _params, socket) do
|
||||
{:ok, runtime} = Runtime.Embedded.init()
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:operation, {:set_runtime, _pid, runtime}}, socket) do
|
||||
{:noreply, assign(socket, current_runtime: runtime)}
|
||||
end
|
||||
|
||||
def handle_info(_message, socket), do: {:noreply, socket}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
|||
use LivebookWeb, :live_view
|
||||
|
||||
alias Livebook.{Session, Runtime, Utils, FileSystem}
|
||||
alias LivebookWeb.SessionLive.RuntimeHelpers
|
||||
|
||||
@type status :: :initial | :initializing | :finished
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex-col space-y-5">
|
||||
<RuntimeHelpers.default_runtime_note module={Runtime.MixStandalone} />
|
||||
<p class="text-gray-700">
|
||||
Start a new local node in the context of a Mix project.
|
||||
This way all your code and dependencies will be available
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
if assigns.runtime do
|
||||
runtime_type(assigns.runtime)
|
||||
else
|
||||
"elixir_standalone"
|
||||
{runtime_module, _args} = Livebook.Config.default_runtime()
|
||||
runtime_module |> struct() |> runtime_type()
|
||||
end
|
||||
|
||||
Map.put(assigns, :type, type)
|
||||
|
|
|
|||
22
lib/livebook_web/live/session_live/runtime_helpers.ex
Normal file
22
lib/livebook_web/live/session_live/runtime_helpers.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
defmodule LivebookWeb.SessionLive.RuntimeHelpers do
|
||||
use Phoenix.Component
|
||||
|
||||
@doc """
|
||||
Displays an info text if `@module` is the default runtime.
|
||||
"""
|
||||
def default_runtime_note(assigns) do
|
||||
~H"""
|
||||
<%= if default_runtime_module?(@module) do %>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Note: This is the <span class="font-semibold">default runtime</span> and starts
|
||||
automatically as soon as you evaluate the first cell.
|
||||
</p>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
defp default_runtime_module?(module) do
|
||||
{default_module, _args} = Livebook.Config.default_runtime()
|
||||
default_module == module
|
||||
end
|
||||
end
|
||||
|
|
@ -96,6 +96,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
<%= live_component LivebookWeb.SessionLive.CellComponent,
|
||||
id: cell_view.id,
|
||||
session_id: @session_id,
|
||||
runtime: @runtime,
|
||||
cell_view: cell_view %>
|
||||
<% end %>
|
||||
|
||||
|
|
|
|||
|
|
@ -189,6 +189,10 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
view
|
||||
|> element("button", "Elixir standalone")
|
||||
|> render_click()
|
||||
|
||||
[elixir_standalone_view] = live_children(view)
|
||||
|
||||
elixir_standalone_view
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@ defmodule Livebook.Runtime.NoopRuntime do
|
|||
def drop_container(_, _), do: :ok
|
||||
def handle_intellisense(_, _, _, _, _), do: :ok
|
||||
def duplicate(_), do: {:ok, Livebook.Runtime.NoopRuntime.new()}
|
||||
def standalone?(_runtime), do: false
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue