Redesign deploy and runtime panels (#2478)

Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
José Valim 2024-02-09 11:58:20 +01:00 committed by GitHub
parent c9d505a2b4
commit f3206d9791
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 277 additions and 204 deletions

View file

@ -279,11 +279,6 @@ solely client-side operations.
@apply text-gray-50 bg-gray-700;
}
[data-el-session][data-js-side-panel-content="app-info"]
[data-el-app-indicator] {
@apply border-gray-700;
}
[data-el-clients-list-item]:not([data-js-followed]) [data-meta="unfollow"] {
@apply hidden;
}

View file

@ -14,6 +14,7 @@ Example usage:
display: flex;
--distance: 4px;
--arrow-size: 5px;
--arrow-side-offset: 10px;
--show-delay: 0.5s;
}
@ -82,7 +83,17 @@ otherwise there's a tiny space between them.
transform: translate(-50%, calc(1px - var(--arrow-size) - var(--distance)));
}
.tooltip.top:after {
.tooltip.top-right:before {
bottom: 100%;
left: 50%;
transform: translate(
calc(0px - var(--arrow-side-offset)),
calc(1px - var(--arrow-size) - var(--distance))
);
}
.tooltip.top:after,
.tooltip.top-right:after {
bottom: 100%;
left: 50%;
transform: translate(-50%, calc(0px - var(--distance)));
@ -100,8 +111,11 @@ otherwise there's a tiny space between them.
.tooltip.bottom-left:before {
top: 100%;
right: 0;
transform: translate(0%, calc(var(--arrow-size) - 1px + var(--distance)));
left: 50%;
transform: translate(
calc(-100% + var(--arrow-side-offset)),
calc(var(--arrow-size) - 1px + var(--distance))
);
}
.tooltip.bottom:after,

View file

@ -68,15 +68,15 @@ defmodule LivebookWeb.AppComponents do
@doc """
Shows a confirmation modal and closes the app on confirm.
"""
def confirm_app_termination(socket, app_pid) do
def confirm_app_termination(socket, app_pid, title \\ "app") do
on_confirm = fn socket ->
Livebook.App.close(app_pid)
socket
end
confirm(socket, on_confirm,
title: "Terminate app",
description: "All app sessions will be immediately terminated.",
title: "Terminate #{title}",
description: "All #{title} sessions will be immediately terminated.",
confirm_text: "Terminate",
confirm_icon: "delete-bin-6-line"
)

View file

@ -99,7 +99,7 @@ defmodule LivebookWeb.LayoutComponents do
</div>
<.sidebar_link title="Home" icon="home-6-line" to={~p"/"} current={@current_page} />
<.sidebar_link
title="Apps"
title="Local apps"
icon="rocket-line"
to={~p"/apps-dashboard"}
current={@current_page}

View file

@ -26,15 +26,18 @@ defmodule LivebookWeb.AppsDashboardLive do
current_user={@current_user}
saved_hubs={@saved_hubs}
>
<div class="p-4 md:px-12 md:py-7 max-w-screen-lg mx-auto">
<div class="space-y-2 p-4 md:px-12 md:py-7 max-w-screen-lg mx-auto">
<div class="flex items-center justify-between">
<LayoutComponents.title text="Apps" />
<LayoutComponents.title text="Local apps" />
<.link navigate={~p"/apps"} class="flex items-center text-blue-600">
<span class="font-semibold">Listing</span>
<.remix_icon icon="arrow-right-line" class="align-middle ml-1" />
</.link>
</div>
<div class="mt-10">
<p class="text-gray-700 text-sm">
An overview of all deployed applications and previews running on this instance.
</p>
<div class="pt-6">
<.app_list apps={@apps} />
</div>
</div>
@ -46,7 +49,7 @@ defmodule LivebookWeb.AppsDashboardLive do
~H"""
<.no_entries>
You do not have any apps running. <br />
You can deploy new apps by opening a notebook and clicking
You can preview and deploy new apps by opening a notebook and clicking
<.remix_icon icon="rocket-line" class="align-top text-lg" /> in the sidebar.
</.no_entries>
"""
@ -70,10 +73,7 @@ defmodule LivebookWeb.AppsDashboardLive do
</div>
<div class="flex-col mb-8">
<div class="p-4 border-x border-t border-gray-200 rounded-t-lg ">
<div class="uppercase text-gray-500 text-sm font-medium leading-normal tracking-wider">
App Info
</div>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-[minmax(0,_2fr)_minmax(0,_2fr)_minmax(0,_1fr)_minmax(0,_1fr)_minmax(0,_1fr)] gap-4 mt-3">
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-[minmax(0,_2fr)_minmax(0,_2fr)_minmax(0,_1fr)_minmax(0,_1fr)_minmax(0,_1fr)] gap-4">
<div class="break-words">
<.labeled_text label="Name">
<%= app.notebook_name %>
@ -196,7 +196,10 @@ defmodule LivebookWeb.AppsDashboardLive do
<div class="grid grid-cols-[minmax(0,_0.5fr)_minmax(0,_0.75fr)_minmax(0,_0.5fr)_minmax(0,_0.5fr)_minmax(0,_0.5fr)] md:grid-cols-[minmax(0,_2fr)_minmax(0,_2fr)_minmax(0,_1fr)_minmax(0,_1fr)_minmax(0,_1fr)] gap-4 px-2">
<div
:for={col <- @col}
class={["text-gray-500 text-sm font-normal", align_to_class(col[:align])]}
class={[
"text-gray-500 text-sm font-normal flex items-center",
align_to_class(col[:align])
]}
>
<%= col[:label] %>
</div>
@ -205,7 +208,10 @@ defmodule LivebookWeb.AppsDashboardLive do
<div class="grid grid-cols-[minmax(0,_0.5fr)_minmax(0,_0.75fr)_minmax(0,_0.5fr)_minmax(0,_0.5fr)_minmax(0,_0.5fr)] md:grid-cols-[minmax(0,_2fr)_minmax(0,_2fr)_minmax(0,_1fr)_minmax(0,_1fr)_minmax(0,_1fr)] gap-4">
<div
:for={col <- @col}
class={["py-2 text-gray-800 text-sm font-semibold", align_to_class(col[:align])]}
class={[
"py-2 text-gray-800 text-sm font-semibold flex items-center",
align_to_class(col[:align])
]}
>
<%= render_slot(col, row) %>
</div>

View file

@ -59,7 +59,7 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
~H"""
<div class="p-6 flex flex-col space-y-8">
<h3 class="text-2xl font-semibold text-gray-800">
App deployment
App deployment with Docker
</h3>
<.content
file={@file}
@ -149,7 +149,7 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
<% end %>
<% end %>
</div>
<div class="flex flex-col gap-2">
<div :if={@warnings != []} class="flex flex-col gap-2">
<.message_box :for={warning <- @warnings} kind={:warning}>
<%= raw(warning) %>
</.message_box>

View file

@ -27,140 +27,188 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
</div>
</div>
<% else %>
<div class="mt-5 flex flex-col gap-6">
<div class="flex flex-col gap-3 mt-2">
<.message_box
:if={@any_session_secrets?}
kind={:warning}
message="The notebook uses session secrets, but those are not available to deployed apps. Convert them to Hub secrets instead."
/>
<div class="flex flex-col gap-3">
<div class="flex gap-2">
<.button
phx-click="deploy_app"
disabled={not Livebook.Notebook.AppSettings.valid?(@settings)}
>
<.remix_icon icon="rocket-line" />
<span>Deploy</span>
</.button>
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/app"}>
Configure
</.button>
<div class="flex flex-col space-y-3">
<.labeled_text label="Slug" one_line>
<%= @settings.slug || "?" %>
</.labeled_text>
<.labeled_text label="Session type" one_line>
<%= if @settings.multi_session, do: "Multi", else: "Single" %>
</.labeled_text>
<.labeled_text label="Access" one_line>
<%= if @settings.access_type == :public do %>
No password <.remix_icon icon="lock-unlock-line" />
<% else %>
Password protected <.remix_icon icon="lock-password-line" />
<% end %>
</.labeled_text>
</div>
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/app"}>
Configure
</.button>
</div>
<h3 class="mt-12 uppercase text-sm font-semibold text-gray-500">
Remote deployment
</h3>
<div class="mt-2 flex flex-col gap-2">
<%!-- TODO: Livebook Teams flow --%>
<.button color="blue" patch={~p"/sessions/#{@session.id}/app-docker"}>
<.remix_icon icon="rocket-line" /> Deploy with Livebook Teams
</.button>
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/app-docker"}>
<.remix_icon icon="ship-line" /> Manual Docker deployment
</.button>
</div>
<h3 class="mt-12 uppercase text-sm font-semibold text-gray-500">
Local preview
</h3>
<div class="flex flex-col mt-2 space-y-4">
<div :if={@app} class="flex flex-col space-y-3">
<.labeled_text label="URL" one_line>
<a href={~p"/apps/#{@app.slug}"}>
<%= ~p"/apps/#{@app.slug}" %>
</a>
</.labeled_text>
<.labeled_text :if={@app.multi_session} label="Latest version" one_line>
v<%= @app.version %>
</.labeled_text>
<div :if={@app.sessions != []}>
<span class="text-sm text-gray-500">Running sessions</span>
<div class="mt-2 flex flex-col space-y-4">
<.app_sessions app={@app} myself={@myself} />
</div>
</div>
<.link
class="text-sm text-gray-700 hover:text-blue-600"
patch={~p"/sessions/#{@session.id}/app-docker"}
</div>
<div class={["grid gap-2", @app && "grid-cols-2"]}>
<span
class={[
"flex flex-col",
not Livebook.Notebook.AppSettings.valid?(@settings) && "tooltip top"
]}
data-tooltip="You must configure the app to preview it"
>
<.remix_icon icon="arrow-right-line" />
<span>Deploy with Docker</span>
</.link>
<%= if @app do %>
<.button
color="gray"
outlined
phx-click="deploy_app"
disabled={not Livebook.Notebook.AppSettings.valid?(@settings)}
>
<.remix_icon icon="slideshow-4-line" /> Relaunch
</.button>
<% else %>
<.button
color="blue"
phx-click="deploy_app"
disabled={not Livebook.Notebook.AppSettings.valid?(@settings)}
>
<.remix_icon icon="slideshow-4-line" /> Launch preview
</.button>
<% end %>
</span>
<.button
:if={@app}
color="red"
outlined
type="button"
phx-click="terminate_app"
phx-target={@myself}
>
Terminate
</.button>
</div>
</div>
<%= if @app do %>
<h3 class="mt-10 uppercase text-sm font-semibold text-gray-500">
Latest deployment
</h3>
<div class="mt-2 border border-gray-200 rounded-lg">
<div class="p-4 flex flex-col gap-3">
<.labeled_text label="URL" one_line>
<a href={~p"/apps/#{@app.slug}"}>
<%= ~p"/apps/#{@app.slug}" %>
</a>
</.labeled_text>
<div class="flex gap-3">
<.labeled_text label="Session type" one_line class="grow">
<%= if(@app.multi_session, do: "Multi", else: "Single") %>
</.labeled_text>
<.labeled_text label="Version" one_line class="grow">
v<%= @app.version %>
</.labeled_text>
</div>
</div>
<div class="border-t border-gray-200 px-3 py-2 flex space-x-2">
<div class="grow" />
<span class="tooltip top" data-tooltip="Terminate">
<.icon_button
aria-label="terminate app"
phx-click={JS.push("terminate_app", target: @myself)}
>
<.remix_icon icon="delete-bin-6-line" />
</.icon_button>
</span>
</div>
</div>
<h3 class="mt-10 uppercase text-sm font-semibold text-gray-500">
Running sessions
</h3>
<div class="mt-2 flex flex-col space-y-4">
<div :for={app_session <- @app.sessions} class="w-full border border-gray-200 rounded-lg">
<div class="p-4 flex gap-3">
<.labeled_text label="Status" class="grow">
<a
class="inline-block"
aria-label="debug app"
href={app_session.app_status == :error && ~p"/sessions/#{app_session.id}"}
target="_blank"
>
<.app_status status={app_session.app_status} />
</a>
</.labeled_text>
<.labeled_text label="Version" class="grow">
v<%= app_session.version %>
</.labeled_text>
</div>
<div class="border-t border-gray-200 px-3 py-2 flex space-x-2">
<span class="tooltip top" data-tooltip="Open">
<.icon_button
disabled={app_session.app_status.lifecycle}
aria-label="open app"
href={~p"/apps/#{@app.slug}/#{app_session.id}"}
>
<.remix_icon icon="link" />
</.icon_button>
</span>
<div class="grow" />
<span class="tooltip top" data-tooltip="Debug">
<.icon_button aria-label="debug app" href={~p"/sessions/#{app_session.id}"}>
<.remix_icon icon="terminal-line" />
</.icon_button>
</span>
<%= if app_session.app_status.lifecycle == :active do %>
<span class="tooltip top" data-tooltip="Deactivate">
<.icon_button
aria-label="deactivate app session"
phx-click={
JS.push("deactivate_app_session",
value: %{session_id: app_session.id},
target: @myself
)
}
>
<.remix_icon icon="stop-circle-line" />
</.icon_button>
</span>
<% else %>
<span class="tooltip top" data-tooltip="Terminate">
<.icon_button
aria-label="terminate app session"
phx-click={
JS.push("terminate_app_session",
value: %{session_id: app_session.id},
target: @myself
)
}
>
<.remix_icon icon="delete-bin-6-line" />
</.icon_button>
</span>
<% end %>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
"""
end
defp app_sessions(assigns) do
~H"""
<div :for={app_session <- @app.sessions} class="w-full border border-gray-200 rounded-lg">
<div class="px-4 py-3 flex gap-3">
<.labeled_text label="Status" class="grow">
<a
class="inline-block"
aria-label="debug app"
href={app_session.app_status == :error && ~p"/sessions/#{app_session.id}"}
target="_blank"
>
<.app_status status={app_session.app_status} />
</a>
</.labeled_text>
<.labeled_text label="Version" class="grow">
v<%= app_session.version %>
</.labeled_text>
</div>
<div class="border-t border-gray-200 px-3 py-1 flex space-x-2">
<div class="grow" />
<span class="tooltip top" data-tooltip="Open">
<.icon_button
disabled={app_session.app_status.lifecycle != :active}
aria-label="open app"
href={~p"/apps/#{@app.slug}/#{app_session.id}"}
>
<.remix_icon icon="link" />
</.icon_button>
</span>
<span class="tooltip top" data-tooltip="Debug">
<.icon_button aria-label="debug app" href={~p"/sessions/#{app_session.id}"}>
<.remix_icon icon="terminal-line" />
</.icon_button>
</span>
<%= if app_session.app_status.lifecycle == :active do %>
<span class="tooltip top" data-tooltip="Deactivate">
<.icon_button
aria-label="deactivate app session"
phx-click={
JS.push("deactivate_app_session",
value: %{session_id: app_session.id},
target: @myself
)
}
>
<.remix_icon icon="stop-circle-line" />
</.icon_button>
</span>
<% else %>
<span class="tooltip top" data-tooltip="Terminate">
<.icon_button
aria-label="terminate app session"
phx-click={
JS.push("terminate_app_session",
value: %{session_id: app_session.id},
target: @myself
)
}
>
<.remix_icon icon="delete-bin-6-line" />
</.icon_button>
</span>
<% end %>
</div>
</div>
"""
end
defp app_info_icon(assigns) do
~H"""
<span
@ -183,7 +231,7 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
@impl true
def handle_event("terminate_app", %{}, socket) do
{:noreply, confirm_app_termination(socket, socket.assigns.app.pid)}
{:noreply, confirm_app_termination(socket, socket.assigns.app.pid, "preview")}
end
def handle_event("terminate_app_session", %{"session_id" => session_id}, socket) do

View file

@ -24,7 +24,14 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do
<h3 class="text-2xl font-semibold text-gray-800">
App settings
</h3>
<.form :let={f} for={@changeset} phx-change="validate" phx-target={@myself} autocomplete="off">
<.form
:let={f}
for={@changeset}
phx-change="validate"
phx-submit="save"
phx-target={@myself}
autocomplete="off"
>
<div class="flex flex-col space-y-4">
<.text_field field={f[:slug]} label="Slug" spellcheck="false" phx-debounce />
<div class="flex flex-col space-y-1">
@ -118,13 +125,8 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do
<% end %>
</div>
<div class="mt-8 flex space-x-2">
<.button
type="button"
phx-click={JS.patch(~p"/sessions/#{@session.id}") |> JS.push("deploy_app")}
disabled={not @changeset.valid?}
>
<.remix_icon icon="rocket-line" />
<span>Deploy</span>
<.button type="submit" disabled={not @changeset.valid?}>
<span>Save</span>
</.button>
<.button color="gray" outlined type="reset" name="reset">
Reset
@ -148,10 +150,14 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do
|> AppSettings.change(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("save", %{"app_settings" => params}, socket) do
with {:ok, settings} <- AppSettings.update(socket.assigns.settings, params) do
Livebook.Session.set_app_settings(socket.assigns.session.pid, settings)
end
{:noreply, assign(socket, changeset: changeset)}
{:noreply, push_patch(socket, to: ~p"/sessions/#{socket.assigns.session.id}")}
end
end

View file

@ -30,7 +30,7 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
<%= @error_message %>
</div>
<p class="text-gray-700">
Start a new local node to handle code evaluation.
Start a new local node to evaluate code.
</p>
<.button phx-click="init">
<%= if(matching_runtime?(@current_runtime), do: "Reconnect", else: "Connect") %>

View file

@ -323,20 +323,11 @@ defmodule LivebookWeb.SessionLive.Render do
button_attrs={["data-el-files-list-toggle": true]}
/>
<div class="relative">
<.button_item
icon="rocket-line"
label="App settings (sa)"
button_attrs={["data-el-app-info-toggle": true]}
/>
<div
data-el-app-indicator
class={[
"absolute w-[12px] h-[12px] border-gray-900 border-2 rounded-full right-1.5 top-1.5 pointer-events-none",
app_status_color(app_status(@app))
]}
/>
</div>
<.button_item
icon="rocket-line"
label="App settings (sa)"
button_attrs={["data-el-app-info-toggle": true]}
/>
<div class="grow"></div>
@ -373,17 +364,6 @@ defmodule LivebookWeb.SessionLive.Render do
"""
end
defp app_status(%{sessions: [app_session | _]}), do: app_session.app_status
defp app_status(_), do: nil
defp app_status_color(nil), do: "bg-gray-400"
defp app_status_color(%{lifecycle: :shutting_down}), do: "bg-gray-500"
defp app_status_color(%{lifecycle: :deactivated}), do: "bg-gray-500"
defp app_status_color(%{execution: :executing}), do: "bg-blue-500"
defp app_status_color(%{execution: :executed}), do: "bg-green-bright-400"
defp app_status_color(%{execution: :error}), do: "bg-red-400"
defp app_status_color(%{execution: :interrupted}), do: "bg-gray-400"
def side_panel(assigns) do
~H"""
<div
@ -601,9 +581,19 @@ defmodule LivebookWeb.SessionLive.Render do
<h3 class="uppercase text-sm font-semibold text-gray-500">
Runtime
</h3>
<.icon_button patch={~p"/sessions/#{@session.id}/settings/runtime"}>
<.remix_icon icon="settings-3-line" />
</.icon_button>
<span
class="tooltip bottom-left"
data-tooltip={
~S'''
The runtime configures which Erlang VM
instance the notebook code runs on.
'''
}
>
<.icon_button>
<.remix_icon icon="question-line" />
</.icon_button>
</span>
</div>
<div class="flex flex-col mt-2 space-y-4">
<div class="flex flex-col space-y-3">
@ -615,37 +605,47 @@ defmodule LivebookWeb.SessionLive.Render do
<%= value %>
</.labeled_text>
</div>
<div class="flex space-x-2">
<div class="grid grid-cols-2 gap-2">
<%= if Runtime.connected?(@data_view.runtime) do %>
<.button phx-click="reconnect_runtime">
<.remix_icon icon="wireless-charging-line" />
<span>Reconnect</span>
</.button>
<.button color="red" outlined type="button" phx-click="disconnect_runtime">
Disconnect
</.button>
<% else %>
<.button phx-click="connect_runtime">
<.remix_icon icon="wireless-charging-line" />
<span>Connect</span>
</.button>
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/runtime"}>
Configure
</.button>
<% end %>
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/runtime"}>
Configure
</.button>
</div>
<div class="flex flex-col pt-6 space-y-2">
<%= if uses_memory?(@session.memory_usage) do %>
<.memory_info memory_usage={@session.memory_usage} />
<% else %>
<div class="text-sm text-gray-800 flex flex-col">
<span class="w-full uppercase font-semibold text-gray-500">Memory</span>
<p class="py-1">
<%= format_bytes(@session.memory_usage.system.free) %> available out of <%= format_bytes(
@session.memory_usage.system.total
) %>
</p>
</div>
<% end %>
<.button
:if={Runtime.connected?(@data_view.runtime)}
color="red"
outlined
type="button"
phx-click="disconnect_runtime"
>
Disconnect
</.button>
</div>
<%= if uses_memory?(@session.memory_usage) do %>
<.memory_info memory_usage={@session.memory_usage} />
<% else %>
<div class="mb-1 text-sm text-gray-800 py-6 flex flex-col">
<span class="w-full uppercase font-semibold text-gray-500">Memory</span>
<p class="py-1">
<%= format_bytes(@session.memory_usage.system.free) %> available out of <%= format_bytes(
@session.memory_usage.system.total
) %>
</p>
</div>
<% end %>
</div>
</div>
"""
@ -655,7 +655,7 @@ defmodule LivebookWeb.SessionLive.Render do
assigns = assign(assigns, :runtime_memory, runtime_memory(assigns.memory_usage))
~H"""
<div class="py-6 flex flex-col justify-center">
<div class="flex flex-col justify-center">
<div class="mb-1 text-sm text-gray-800 flex flex-row justify-between">
<span class="text-gray-500 font-semibold uppercase">Memory</span>
<span class="text-right">

View file

@ -2127,7 +2127,11 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_change(%{"app_settings" => %{"slug" => slug}})
view
|> element(~s/#app-settings-modal button/, "Deploy")
|> element(~s/#app-settings-modal form/)
|> render_submit(%{"app_settings" => %{"slug" => slug}})
view
|> element(~s/[data-el-app-info] button/, "Launch preview")
|> render_click()
assert_receive {:app_created, %{slug: ^slug} = app}