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:
Jonatan Kłosko 2021-10-28 19:41:07 +02:00 committed by GitHub
parent 8fe8d27d3d
commit 4493a60380
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 114 additions and 17 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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") %>

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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