Add runtime info panel (#692)

* Add runtime info panel

* Remove intro from the runtime modal

* Show default runtime in the panel if no runtime is set
This commit is contained in:
Jonatan Kłosko 2021-11-09 18:37:22 +01:00 committed by GitHub
parent 1842c203ab
commit d8d52c9e89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 202 additions and 161 deletions

View file

@ -109,6 +109,11 @@ solely client-side operations.
@apply hidden;
}
[data-element="session"]:not([data-js-side-panel-content="runtime-info"])
[data-element="runtime-info"] {
@apply hidden;
}
[data-element="session"][data-js-side-panel-content="sections-list"]
[data-element="sections-list-toggle"] {
@apply text-gray-50 bg-gray-700;
@ -119,6 +124,11 @@ solely client-side operations.
@apply text-gray-50 bg-gray-700;
}
[data-element="session"][data-js-side-panel-content="runtime-info"]
[data-element="runtime-info-toggle"] {
@apply text-gray-50 bg-gray-700;
}
[data-element="section-headline"]:not(:hover)
[data-element="section-name"]:not(:focus)
+ [data-element="section-actions"]:not(:focus-within) {

View file

@ -32,13 +32,13 @@ Example usage:
white-space: pre;
text-align: center;
display: block;
z-index: 100;
background-color: #1c273c;
color: #f0f5f9;
font-size: 12px;
font-weight: 500;
border-radius: 4px;
padding: 3px 12px;
z-index: 100;
visibility: hidden;
transition-property: visibility;
transition-duration: 0s;
@ -50,6 +50,7 @@ Example usage:
content: "";
position: absolute;
display: block;
z-index: 100;
/* For the arrow we use the triangle trick: https://css-tricks.com/snippets/css/css-triangle/ */
border-width: var(--arrow-size);
border-style: solid;

View file

@ -119,6 +119,10 @@ const Session = {
toggleClientsList(this);
});
getRuntimeInfoToggle().addEventListener("click", (event) => {
toggleRuntimeInfo(this);
});
getNotebook().addEventListener("scroll", (event) => {
updateSectionListHighlight();
});
@ -335,7 +339,7 @@ function handleDocumentKeyDown(hook, event) {
} else if (keyBuffer.tryMatch(["s", "u"])) {
toggleClientsList(hook);
} else if (keyBuffer.tryMatch(["s", "r"])) {
showNotebookRuntimeSettings(hook);
toggleRuntimeInfo(hook);
} else if (keyBuffer.tryMatch(["s", "b"])) {
showBin(hook);
} else if (keyBuffer.tryMatch(["e", "x"])) {
@ -581,23 +585,23 @@ function updateSectionListHighlight() {
// User action handlers (mostly keybindings)
function toggleSectionsList(hook) {
if (hook.el.getAttribute("data-js-side-panel-content") === "sections-list") {
hook.el.removeAttribute("data-js-side-panel-content");
} else {
hook.el.setAttribute("data-js-side-panel-content", "sections-list");
}
toggleSidePanelContent(hook, "sections-list");
}
function toggleClientsList(hook) {
if (hook.el.getAttribute("data-js-side-panel-content") === "clients-list") {
hook.el.removeAttribute("data-js-side-panel-content");
} else {
hook.el.setAttribute("data-js-side-panel-content", "clients-list");
}
toggleSidePanelContent(hook, "clients-list");
}
function showNotebookRuntimeSettings(hook) {
hook.pushEvent("show_runtime_settings", {});
function toggleRuntimeInfo(hook) {
toggleSidePanelContent(hook, "runtime-info");
}
function toggleSidePanelContent(hook, name) {
if (hook.el.getAttribute("data-js-side-panel-content") === name) {
hook.el.removeAttribute("data-js-side-panel-content");
} else {
hook.el.setAttribute("data-js-side-panel-content", name);
}
}
function showBin(hook) {
@ -1024,6 +1028,10 @@ function getClientsListToggle() {
return document.querySelector(`[data-element="clients-list-toggle"]`);
}
function getRuntimeInfoToggle() {
return document.querySelector(`[data-element="runtime-info-toggle"]`);
}
function cancelEvent(event) {
// Cancel any default browser behavior.
event.preventDefault();

View file

@ -37,7 +37,8 @@ defmodule LivebookWeb.SessionLive do
platform: platform,
self: self(),
data_view: data_to_view(data),
autofocus_cell_id: autofocus_cell_id(data.notebook)
autofocus_cell_id: autofocus_cell_id(data.notebook),
empty_default_runtime: Livebook.Config.default_runtime() |> elem(0) |> struct()
)
|> assign_private(data: data)
|> allow_upload(:cell_image,
@ -87,11 +88,10 @@ defmodule LivebookWeb.SessionLive do
icon="group-fill"
label="Connected users (su)"
data_element="clients-list-toggle" />
<SidebarHelpers.link_item
<SidebarHelpers.button_item
icon="cpu-line"
label="Runtime settings (sr)"
path={Routes.session_path(@socket, :runtime_settings, @session.id)}
active={@live_action == :runtime_settings} />
data_element="runtime-info-toggle" />
<SidebarHelpers.link_item
icon="delete-bin-6-fill"
label="Bin (sb)"
@ -110,81 +110,13 @@ defmodule LivebookWeb.SessionLive do
<div class="flex flex-col h-full w-full max-w-xs absolute z-30 top-0 left-[64px] overflow-y-auto shadow-xl md:static md:shadow-none bg-gray-50 border-r border-gray-100 px-6 py-10"
data-element="side-panel">
<div data-element="sections-list">
<div class="flex flex-col flex-grow">
<h3 class="text-lg font-semibold text-gray-800">
Sections
</h3>
<div class="flex flex-col mt-4 space-y-4">
<%= for section_item <- @data_view.sections_items do %>
<div class="flex items-center">
<button class="flex-grow flex items-center text-gray-500 hover:text-gray-900"
data-element="sections-list-item"
data-section-id={section_item.id}>
<span class="flex items-center space-x-1">
<span><%= section_item.name %></span>
<%= if section_item.parent do %>
<%# Note: the container has overflow-y auto, so we cannot set overflow-x visible,
consequently we show the tooltip wrapped to a fixed number of characters %>
<span {branching_tooltip_attrs(section_item.name, section_item.parent.name)}>
<.remix_icon icon="git-branch-line" class="text-lg font-normal leading-none flip-horizontally" />
</span>
<% end %>
</span>
</button>
<.session_status status={elem(section_item.status, 0)} cell_id={elem(section_item.status, 1)} />
</div>
<% end %>
</div>
<button class="inline-flex items-center justify-center p-8 py-1 mt-8 space-x-2 text-sm font-medium text-gray-500 border border-gray-400 border-dashed rounded-xl hover:bg-gray-100"
phx-click="append_section">
<.remix_icon icon="add-line" class="text-lg align-center" />
<span>New section</span>
</button>
</div>
<.sections_list data_view={@data_view} />
</div>
<div data-element="clients-list">
<div class="flex flex-col flex-grow">
<div class="flex items-center justify-between space-x-4">
<h3 class="text-lg font-semibold text-gray-800 flex-lg">
Users
</h3>
<span class="flex items-center p-2 space-x-2 text-sm bg-gray-200 rounded-lg">
<span class="inline-flex w-3 h-3 bg-green-600 rounded-full"></span>
<span><%= length(@data_view.clients) %> connected</span>
</span>
</div>
<div class="flex flex-col mt-4 space-y-4">
<%= for {client_pid, user} <- @data_view.clients do %>
<div class="flex items-center justify-between space-x-2"
id={"clients-list-item-#{inspect(client_pid)}"}
data-element="clients-list-item"
data-client-pid={inspect(client_pid)}>
<button class="flex items-center space-x-2 text-gray-500 hover:text-gray-900 disabled:pointer-events-none"
disabled={client_pid == @self}
data-element="client-link">
<.user_avatar user={user} class="flex-shrink-0 h-7 w-7" text_class="text-xs" />
<span><%= user.name || "Anonymous" %></span>
</button>
<%= if client_pid != @self do %>
<span class="tooltip left" data-tooltip="Follow this user"
data-element="client-follow-toggle"
data-meta="follow">
<button class="icon-button" aria-label="follow this user">
<.remix_icon icon="pushpin-line" class="text-lg" />
</button>
</span>
<span class="tooltip left" data-tooltip="Unfollow this user"
data-element="client-follow-toggle"
data-meta="unfollow">
<button class="icon-button" aria-label="unfollow this user">
<.remix_icon icon="pushpin-fill" class="text-lg" />
</button>
</span>
<% end %>
</div>
<% end %>
</div>
</div>
<.clients_list data_view={@data_view} self={@self} />
</div>
<div data-element="runtime-info">
<.runtime_info data_view={@data_view} session={@session} socket={@socket} empty_default_runtime={@empty_default_runtime} />
</div>
</div>
<div class="flex-grow overflow-y-auto scroll-smooth" data-element="notebook">
@ -355,6 +287,143 @@ defmodule LivebookWeb.SessionLive do
"""
end
defp sections_list(assigns) do
~H"""
<div class="flex flex-col flex-grow">
<h3 class="text-lg font-semibold text-gray-800">
Sections
</h3>
<div class="flex flex-col mt-4 space-y-4">
<%= for section_item <- @data_view.sections_items do %>
<div class="flex items-center">
<button class="flex-grow flex items-center text-gray-500 hover:text-gray-900"
data-element="sections-list-item"
data-section-id={section_item.id}>
<span class="flex items-center space-x-1">
<span><%= section_item.name %></span>
<%= if section_item.parent do %>
<%# Note: the container has overflow-y auto, so we cannot set overflow-x visible,
consequently we show the tooltip wrapped to a fixed number of characters %>
<span {branching_tooltip_attrs(section_item.name, section_item.parent.name)}>
<.remix_icon icon="git-branch-line" class="text-lg font-normal leading-none flip-horizontally" />
</span>
<% end %>
</span>
</button>
<.session_status status={elem(section_item.status, 0)} cell_id={elem(section_item.status, 1)} />
</div>
<% end %>
</div>
<button class="inline-flex items-center justify-center p-8 py-1 mt-8 space-x-2 text-sm font-medium text-gray-500 border border-gray-400 border-dashed rounded-xl hover:bg-gray-100"
phx-click="append_section">
<.remix_icon icon="add-line" class="text-lg align-center" />
<span>New section</span>
</button>
</div>
"""
end
defp clients_list(assigns) do
~H"""
<div class="flex flex-col flex-grow">
<div class="flex items-center justify-between space-x-4">
<h3 class="text-lg font-semibold text-gray-800 flex-lg">
Users
</h3>
<span class="flex items-center p-2 space-x-2 text-sm bg-gray-200 rounded-lg">
<span class="inline-flex w-3 h-3 bg-green-600 rounded-full"></span>
<span><%= length(@data_view.clients) %> connected</span>
</span>
</div>
<div class="flex flex-col mt-4 space-y-4">
<%= for {client_pid, user} <- @data_view.clients do %>
<div class="flex items-center justify-between space-x-2"
id={"clients-list-item-#{inspect(client_pid)}"}
data-element="clients-list-item"
data-client-pid={inspect(client_pid)}>
<button class="flex items-center space-x-2 text-gray-500 hover:text-gray-900 disabled:pointer-events-none"
disabled={client_pid == @self}
data-element="client-link">
<.user_avatar user={user} class="flex-shrink-0 h-7 w-7" text_class="text-xs" />
<span><%= user.name || "Anonymous" %></span>
</button>
<%= if client_pid != @self do %>
<span class="tooltip left" data-tooltip="Follow this user"
data-element="client-follow-toggle"
data-meta="follow">
<button class="icon-button" aria-label="follow this user">
<.remix_icon icon="pushpin-line" class="text-lg" />
</button>
</span>
<span class="tooltip left" data-tooltip="Unfollow this user"
data-element="client-follow-toggle"
data-meta="unfollow">
<button class="icon-button" aria-label="unfollow this user">
<.remix_icon icon="pushpin-fill" class="text-lg" />
</button>
</span>
<% end %>
</div>
<% end %>
</div>
</div>
"""
end
defp runtime_info(assigns) do
~H"""
<div class="flex flex-col flex-grow">
<h3 class="text-lg font-semibold text-gray-800">
Runtime
</h3>
<div class="flex flex-col mt-4 space-y-4">
<%= if @data_view.runtime do %>
<div class="flex flex-col space-y-3">
<.labeled_text label="Type" text={runtime_type_label(@data_view.runtime)} />
<.labeled_text label="Node name" text={@data_view.runtime.node} />
</div>
<div class="flex flex-col space-y-3">
<div class="flex space-x-2">
<button class="button button-outlined-blue w-full" phx-click="restart_runtime">
Reconnect
</button>
<button class="button button-outlined-red w-full"
type="button"
phx-click="disconnect_runtime">
Disconnect
</button>
</div>
<%= live_patch to: Routes.session_path(@socket, :runtime_settings, @session.id),
class: "button button-gray button-square-icon",
type: "button" do %>
<.remix_icon icon="settings-3-line" />
<% end %>
</div>
<% else %>
<div class="flex flex-col space-y-3">
<.labeled_text label="Type" text={runtime_type_label(@empty_default_runtime)} />
</div>
<div class="flex space-x-2">
<button class="button button-blue" phx-click="connect_default_runtime">
Connect
</button>
<%= live_patch to: Routes.session_path(@socket, :runtime_settings, @session.id),
class: "button button-gray button-square-icon",
type: "button" do %>
<.remix_icon icon="settings-3-line" />
<% end %>
</div>
<% end %>
</div>
</div>
"""
end
defp runtime_type_label(%Runtime.ElixirStandalone{}), do: "Elixir standalone"
defp runtime_type_label(%Runtime.MixStandalone{}), do: "Mix standalone"
defp runtime_type_label(%Runtime.Attached{}), do: "Attached"
defp runtime_type_label(%Runtime.Embedded{}), do: "Embedded"
defp session_status(%{status: :evaluating} = assigns) do
~H"""
<button data-element="focus-cell-button" data-target={@cell_id}>
@ -660,13 +729,6 @@ defmodule LivebookWeb.SessionLive do
push_patch(socket, to: Routes.session_path(socket, :shortcuts, socket.assigns.session.id))}
end
def handle_event("show_runtime_settings", %{}, socket) do
{:noreply,
push_patch(socket,
to: Routes.session_path(socket, :runtime_settings, socket.assigns.session.id)
)}
end
def handle_event("show_bin", %{}, socket) do
{:noreply,
push_patch(socket, to: Routes.session_path(socket, :bin, socket.assigns.session.id))}
@ -690,6 +752,27 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket}
end
def handle_event("connect_default_runtime", %{}, socket) do
{runtime_module, args} = Livebook.Config.default_runtime()
socket =
case apply(runtime_module, :init, args) do
{:ok, runtime} ->
Session.connect_runtime(socket.assigns.session.pid, runtime)
socket
{:error, message} ->
put_flash(socket, :error, "Failed to setup runtime - #{message}")
end
{:noreply, socket}
end
def handle_event("disconnect_runtime", %{}, socket) do
Session.disconnect_runtime(socket.assigns.session.pid)
{:noreply, socket}
end
def handle_event("intellisense_request", %{"cell_id" => cell_id} = params, socket) do
request =
case params do

View file

@ -2,7 +2,6 @@ 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
@ -28,7 +27,6 @@ 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.

View file

@ -2,7 +2,6 @@ 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
@ -22,7 +21,6 @@ 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.
</p>

View file

@ -2,7 +2,6 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive 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
@ -17,7 +16,6 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
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

View file

@ -2,7 +2,6 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
use LivebookWeb, :live_view
alias Livebook.{Session, Runtime, Utils, FileSystem}
alias LivebookWeb.SessionLive.RuntimeHelpers
@type status :: :initial | :initializing | :finished
@ -27,7 +26,6 @@ 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

@ -1,7 +1,7 @@
defmodule LivebookWeb.SessionLive.RuntimeComponent do
use LivebookWeb, :live_component
alias Livebook.{Session, Runtime}
alias Livebook.Runtime
@impl true
def mount(socket) do
@ -31,31 +31,11 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
@impl true
def render(assigns) do
~H"""
<div class="p-6 pb-4 max-w-4xl flex flex-col space-y-3">
<div class="p-6 pb-4 max-w-4xl flex flex-col space-y-5">
<h3 class="text-2xl font-semibold text-gray-800">
Runtime
</h3>
<div class="w-full flex-col space-y-5">
<p class="text-gray-700">
The code is evaluated in a separate Elixir runtime (node),
which you can configure yourself here.
</p>
<div class="flex items-center justify-between border border-gray-200 rounded-lg p-4">
<%= if @runtime do %>
<.labeled_text label="Type" text={runtime_type_label(@runtime)} />
<.labeled_text label="Node name" text={@runtime.node} />
<button class="button button-outlined-red"
type="button"
phx-click="disconnect"
phx-target={@myself}>
Disconnect
</button>
<% else %>
<p class="text-sm text-gray-700">
No connected node
</p>
<% end %>
</div>
<div class="flex space-x-4">
<.choice_button
active={@type == "elixir_standalone"}
@ -96,11 +76,6 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
"""
end
defp runtime_type_label(%Runtime.ElixirStandalone{}), do: "Elixir standalone"
defp runtime_type_label(%Runtime.MixStandalone{}), do: "Mix standalone"
defp runtime_type_label(%Runtime.Attached{}), do: "Attached"
defp runtime_type_label(%Runtime.Embedded{}), do: "Embedded"
defp runtime_type(%Runtime.ElixirStandalone{}), do: "elixir_standalone"
defp runtime_type(%Runtime.MixStandalone{}), do: "mix_standalone"
defp runtime_type(%Runtime.Attached{}), do: "attached"
@ -115,10 +90,4 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
def handle_event("set_runtime_type", %{"type" => type}, socket) do
{:noreply, assign(socket, type: type)}
end
def handle_event("disconnect", _params, socket) do
Session.disconnect_runtime(socket.assigns.session.pid)
{:noreply, socket}
end
end

View file

@ -1,22 +0,0 @@
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