mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-11 15:34:37 +08:00
Move app settings to a modal and add explanations (#1914)
Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
7340c6b204
commit
d1ba07e0ea
7 changed files with 370 additions and 304 deletions
|
@ -14,6 +14,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
|
||||
attr :rest, :global, include: ~w(autocomplete readonly disabled)
|
||||
|
@ -22,7 +23,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
assigns = assigns_from_field(assigns)
|
||||
|
||||
~H"""
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors}>
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
|
||||
<input
|
||||
type="text"
|
||||
name={@name}
|
||||
|
@ -44,6 +45,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :resizable, :boolean, default: false
|
||||
|
||||
|
@ -53,7 +55,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
assigns = assigns_from_field(assigns)
|
||||
|
||||
~H"""
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors}>
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
|
||||
<textarea
|
||||
id={@id || @name}
|
||||
name={@name}
|
||||
|
@ -91,6 +93,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
|
||||
attr :rest, :global, include: ~w(autocomplete readonly disabled)
|
||||
|
@ -99,7 +102,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
assigns = assigns_from_field(assigns)
|
||||
|
||||
~H"""
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors}>
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
|
||||
<.with_password_toggle id={@id <> "-toggle"}>
|
||||
<input
|
||||
type="password"
|
||||
|
@ -123,6 +126,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :randomize, JS, default: %JS{}
|
||||
attr :rest, :global
|
||||
|
@ -131,7 +135,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
assigns = assigns_from_field(assigns)
|
||||
|
||||
~H"""
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors}>
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div
|
||||
class="border-[3px] rounded-lg p-1 flex justify-center items-center"
|
||||
|
@ -168,11 +172,11 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :disabled, :boolean, default: false
|
||||
attr :checked_value, :string, default: "true"
|
||||
attr :unchecked_value, :string, default: "false"
|
||||
attr :tooltip, :string, default: nil
|
||||
|
||||
attr :rest, :global
|
||||
|
||||
|
@ -182,12 +186,9 @@ defmodule LivebookWeb.FormComponents do
|
|||
~H"""
|
||||
<div phx-feedback-for={@name} class={[@errors != [] && "show-errors"]}>
|
||||
<div class="flex items-center gap-1 sm:gap-3 justify-between">
|
||||
<span
|
||||
:if={@label}
|
||||
class={["text-gray-700", @tooltip && "tooltip top"]}
|
||||
data-tooltip={@tooltip}
|
||||
>
|
||||
<span :if={@label} class="text-gray-700 flex gap-1 items-center">
|
||||
<%= @label %>
|
||||
<.help :if={@help} text={@help} />
|
||||
</span>
|
||||
<label class={[
|
||||
"relative inline-block w-14 h-7 select-none",
|
||||
|
@ -227,6 +228,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :checked_value, :string, default: "true"
|
||||
attr :unchecked_value, :string, default: "false"
|
||||
|
@ -249,7 +251,10 @@ defmodule LivebookWeb.FormComponents do
|
|||
checked={to_string(@value) == @checked_value}
|
||||
{@rest}
|
||||
/>
|
||||
<span :if={@label} class="text-gray-700"><%= @label %></span>
|
||||
<span :if={@label} class="text-gray-700 flex gap-1 items-center">
|
||||
<%= @label %>
|
||||
<.help :if={@help} text={@help} />
|
||||
</span>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
|
@ -265,6 +270,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :options, :list, default: [], doc: "a list of `{value, description}` tuples"
|
||||
|
||||
|
@ -275,7 +281,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
|
||||
~H"""
|
||||
<div phx-feedback-for={@name} class={[@errors != [] && "show-errors"]}>
|
||||
<.label :if={@label} for={@id}><%= @label %></.label>
|
||||
<.label :if={@label} for={@id} help={@help}><%= @label %></.label>
|
||||
<div class="flex gap-4 text-gray-600">
|
||||
<label :for={{value, description} <- @options} class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
|
@ -304,6 +310,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :options, :list, default: [], doc: "a list of `{value, description}` tuples"
|
||||
attr :full_width, :boolean, default: false
|
||||
|
@ -315,7 +322,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
|
||||
~H"""
|
||||
<div phx-feedback-for={@name} class={[@errors != [] && "show-errors"]}>
|
||||
<.label :if={@label} for={@id}><%= @label %></.label>
|
||||
<.label :if={@label} for={@id} help={@help}><%= @label %></.label>
|
||||
<div class="flex">
|
||||
<label
|
||||
:for={{value, description} <- @options}
|
||||
|
@ -352,6 +359,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :rest, :global
|
||||
|
||||
|
@ -359,7 +367,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
assigns = assigns_from_field(assigns)
|
||||
|
||||
~H"""
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors}>
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
|
||||
<div class="flex border bg-gray-50 rounded-lg space-x-4 items-center">
|
||||
<div id={"#{@id}-picker"} class="flex w-full" phx-hook="EmojiPicker">
|
||||
<div class="grow p-1 pl-3">
|
||||
|
@ -396,6 +404,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :value, :any
|
||||
attr :errors, :list, default: []
|
||||
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form"
|
||||
attr :help, :string, default: nil
|
||||
|
||||
attr :options, :list, default: []
|
||||
attr :prompt, :string, default: nil
|
||||
|
@ -406,7 +415,7 @@ defmodule LivebookWeb.FormComponents do
|
|||
assigns = assigns_from_field(assigns)
|
||||
|
||||
~H"""
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors}>
|
||||
<.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
|
||||
<select id={@id} name={@name} class="input" {@rest}>
|
||||
<option :if={@prompt} value="" disabled selected><%= @prompt %></option>
|
||||
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
|
||||
|
@ -440,12 +449,13 @@ defmodule LivebookWeb.FormComponents do
|
|||
attr :name, :any, required: true
|
||||
attr :label, :string, required: true
|
||||
attr :errors, :list, required: true
|
||||
attr :help, :string, required: true
|
||||
slot :inner_block, required: true
|
||||
|
||||
defp field_wrapper(assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@name} class={[@errors != [] && "show-errors"]}>
|
||||
<.label :if={@label} for={@id}><%= @label %></.label>
|
||||
<.label :if={@label} for={@id} help={@help}><%= @label %></.label>
|
||||
<%= render_slot(@inner_block) %>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
|
@ -456,12 +466,14 @@ defmodule LivebookWeb.FormComponents do
|
|||
Renders a label.
|
||||
"""
|
||||
attr :for, :string, default: nil
|
||||
attr :help, :string, default: nil
|
||||
slot :inner_block, required: true
|
||||
|
||||
def label(assigns) do
|
||||
~H"""
|
||||
<label for={@for} class="mb-1 block text-sm text-gray-800 font-medium">
|
||||
<label for={@for} class="mb-1 block text-sm text-gray-800 font-medium flex items-center gap-1">
|
||||
<%= render_slot(@inner_block) %>
|
||||
<.help :if={@help} text={@help} />
|
||||
</label>
|
||||
"""
|
||||
end
|
||||
|
@ -479,6 +491,14 @@ defmodule LivebookWeb.FormComponents do
|
|||
"""
|
||||
end
|
||||
|
||||
defp help(assigns) do
|
||||
~H"""
|
||||
<span class="cursor-pointer tooltip top" data-tooltip={@text}>
|
||||
<.remix_icon icon="question-line" class="text-sm leading-none" />
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a wrapper around password input with an added visibility
|
||||
toggle button.
|
||||
|
|
|
@ -143,7 +143,7 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
|
|||
:if={@live_action in [:new_secret, :edit_secret]}
|
||||
id="secrets-modal"
|
||||
show
|
||||
width={:big}
|
||||
width={:medium}
|
||||
patch={~p"/hub/#{@hub.id}"}
|
||||
>
|
||||
<.live_component
|
||||
|
|
|
@ -429,6 +429,21 @@ defmodule LivebookWeb.SessionLive do
|
|||
/>
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :app_settings}
|
||||
id="app-settings-modal"
|
||||
show
|
||||
width={:medium}
|
||||
patch={@self_path}
|
||||
>
|
||||
<.live_component
|
||||
module={LivebookWeb.SessionLive.AppSettingsComponent}
|
||||
id="app-settings"
|
||||
session={@session}
|
||||
settings={@data_view.app_settings}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :shortcuts}
|
||||
id="shortcuts-modal"
|
||||
|
@ -529,7 +544,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
) %>
|
||||
</.modal>
|
||||
|
||||
<.modal :if={@live_action == :secrets} id="secrets-modal" show width={:big} patch={@self_path}>
|
||||
<.modal :if={@live_action == :secrets} id="secrets-modal" show width={:medium} patch={@self_path}>
|
||||
<.live_component
|
||||
module={LivebookWeb.SessionLive.SecretsComponent}
|
||||
id="secrets"
|
||||
|
@ -1233,6 +1248,31 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("deploy_app", %{}, socket) do
|
||||
on_confirm = fn socket ->
|
||||
Livebook.Session.deploy_app(socket.assigns.session.pid)
|
||||
socket
|
||||
end
|
||||
|
||||
data = socket.private.data
|
||||
slug = data.notebook.app_settings.slug
|
||||
slug_taken? = slug != data.deployed_app_slug and Livebook.Apps.exists?(slug)
|
||||
|
||||
socket =
|
||||
if slug_taken? do
|
||||
confirm(socket, on_confirm,
|
||||
title: "Deploy app",
|
||||
description:
|
||||
"An app with this slug already exists, do you want to deploy a new version?",
|
||||
confirm_text: "Replace"
|
||||
)
|
||||
else
|
||||
on_confirm.(socket)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("intellisense_request", %{"cell_id" => cell_id} = params, socket) do
|
||||
request =
|
||||
case params do
|
||||
|
|
|
@ -3,149 +3,150 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
|
|||
|
||||
import LivebookWeb.AppHelpers
|
||||
|
||||
alias Livebook.Notebook.AppSettings
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, deploy_confirmation: false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
changeset =
|
||||
case socket.assigns do
|
||||
%{changeset: changeset} when changeset.data == assigns.settings -> changeset
|
||||
_ -> AppSettings.change(assigns.settings)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(changeset: changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="uppercase text-sm font-semibold text-gray-500">
|
||||
App settings
|
||||
App
|
||||
</h3>
|
||||
<.app_info_icon />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<.app_form
|
||||
changeset={@changeset}
|
||||
deploy_confirmation={@deploy_confirmation}
|
||||
session={@session}
|
||||
myself={@myself}
|
||||
/>
|
||||
</div>
|
||||
<%= if @app do %>
|
||||
<h3 class="mt-16 uppercase text-sm font-semibold text-gray-500">
|
||||
Deployment
|
||||
</h3>
|
||||
<div class="mt-2 border border-gray-200 rounded-lg">
|
||||
<div class="p-4 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 label="Version" one_line>
|
||||
v<%= @app.version %>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Session type" one_line>
|
||||
<%= if(@app.multi_session, do: "Multi", else: "Single") %>
|
||||
</.labeled_text>
|
||||
</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">
|
||||
<button
|
||||
class="icon-button"
|
||||
aria-label="terminate app"
|
||||
phx-click={JS.push("terminate_app", target: @myself)}
|
||||
>
|
||||
<.remix_icon icon="delete-bin-6-line" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<%= if @session.mode == :app do %>
|
||||
<div class="mt-5 flex flex-col space-y-6">
|
||||
<span class="text-gray-700 text-sm">
|
||||
This session is a running app. To deploy a modified version, you can fork it.
|
||||
</span>
|
||||
<div>
|
||||
<button class="button-base button-blue" phx-click="fork_session">
|
||||
<.remix_icon icon="git-branch-line" />
|
||||
<span>Fork</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-gray-600 font-medium text-sm">
|
||||
App sessions
|
||||
<% else %>
|
||||
<div class="mt-5 flex space-x-2">
|
||||
<button
|
||||
class="button-base button-blue"
|
||||
phx-click="deploy_app"
|
||||
disabled={not Livebook.Notebook.AppSettings.valid?(@settings)}
|
||||
>
|
||||
<.remix_icon icon="rocket-line" class="align-middle mr-1" />
|
||||
<span>Deploy</span>
|
||||
</button>
|
||||
<.link
|
||||
patch={~p"/sessions/#{@session.id}/settings/app"}
|
||||
class="button-base button-outlined-gray bg-transparent"
|
||||
>
|
||||
Configure
|
||||
</.link>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-col space-y-4">
|
||||
<div :for={app_session <- @app.sessions} class="border border-gray-200 rounded-lg">
|
||||
<%= 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 space-y-3">
|
||||
<.labeled_text label="Status">
|
||||
<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} />
|
||||
<.labeled_text label="URL" one_line>
|
||||
<a href={~p"/apps/#{@app.slug}"}>
|
||||
<%= ~p"/apps/#{@app.slug}" %>
|
||||
</a>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Version">
|
||||
v<%= app_session.version %>
|
||||
<.labeled_text label="Version" one_line>
|
||||
v<%= @app.version %>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Session type" one_line>
|
||||
<%= if(@app.multi_session, do: "Multi", else: "Single") %>
|
||||
</.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">
|
||||
<a
|
||||
class={[
|
||||
"icon-button",
|
||||
not Livebook.Session.Data.app_active?(app_session.app_status) && "disabled"
|
||||
]}
|
||||
aria-label="open app"
|
||||
href={~p"/apps/#{@app.slug}/#{app_session.id}"}
|
||||
>
|
||||
<.remix_icon icon="link" class="text-lg" />
|
||||
</a>
|
||||
</span>
|
||||
<div class="grow" />
|
||||
<span class="tooltip top" data-tooltip="Debug">
|
||||
<a class="icon-button" aria-label="debug app" href={~p"/sessions/#{app_session.id}"}>
|
||||
<.remix_icon icon="terminal-line" class="text-lg" />
|
||||
</a>
|
||||
<span class="tooltip top" data-tooltip="Terminate">
|
||||
<button
|
||||
class="icon-button"
|
||||
aria-label="terminate app"
|
||||
phx-click={JS.push("terminate_app", target: @myself)}
|
||||
>
|
||||
<.remix_icon icon="delete-bin-6-line" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<%= if app_session.app_status in [:deactivated, :shutting_down] do %>
|
||||
<span class="tooltip top" data-tooltip="Terminate">
|
||||
<button
|
||||
class="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" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="tooltip top" data-tooltip="Deactivate">
|
||||
<button
|
||||
class="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" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</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="border border-gray-200 rounded-lg">
|
||||
<div class="p-4 flex flex-col space-y-3">
|
||||
<.labeled_text label="Status">
|
||||
<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">
|
||||
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">
|
||||
<a
|
||||
class={[
|
||||
"icon-button",
|
||||
not Livebook.Session.Data.app_active?(app_session.app_status) && "disabled"
|
||||
]}
|
||||
aria-label="open app"
|
||||
href={~p"/apps/#{@app.slug}/#{app_session.id}"}
|
||||
>
|
||||
<.remix_icon icon="link" class="text-lg" />
|
||||
</a>
|
||||
</span>
|
||||
<div class="grow" />
|
||||
<span class="tooltip top" data-tooltip="Debug">
|
||||
<a class="icon-button" aria-label="debug app" href={~p"/sessions/#{app_session.id}"}>
|
||||
<.remix_icon icon="terminal-line" class="text-lg" />
|
||||
</a>
|
||||
</span>
|
||||
<%= if app_session.app_status in [:deactivated, :shutting_down] do %>
|
||||
<span class="tooltip top" data-tooltip="Terminate">
|
||||
<button
|
||||
class="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" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="tooltip top" data-tooltip="Deactivate">
|
||||
<button
|
||||
class="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" class="text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
|
@ -169,165 +170,7 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp app_form(%{session: %{mode: :app}} = assigns) do
|
||||
~H"""
|
||||
<div class="mt-5 flex flex-col space-y-6">
|
||||
<span class="text-gray-700 text-sm">
|
||||
This session is a running app. To deploy a modified version, you can fork it.
|
||||
</span>
|
||||
<div>
|
||||
<button class="button-base button-blue" phx-click="fork_session">
|
||||
<.remix_icon icon="git-branch-line" />
|
||||
<span>Fork</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp app_form(%{deploy_confirmation: true} = assigns) do
|
||||
~H"""
|
||||
<div class="mt-5">
|
||||
<span class="text-gray-700 text-sm">
|
||||
An app with this slug already exists, do you want to deploy a new version?
|
||||
</span>
|
||||
<div class="mt-5 flex space-x-2">
|
||||
<button
|
||||
class="button-base button-red"
|
||||
phx-click="deploy_confirmation_confirm"
|
||||
phx-target={@myself}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
<button
|
||||
class="button-base button-outlined-gray bg-transparent"
|
||||
phx-click="deploy_confirmation_cancel"
|
||||
phx-target={@myself}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp app_form(assigns) do
|
||||
~H"""
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
phx-submit="deploy"
|
||||
phx-change="validate"
|
||||
phx-target={@myself}
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<.text_field
|
||||
field={f[:slug]}
|
||||
label="Slug"
|
||||
spellcheck="false"
|
||||
phx-debounce
|
||||
class="bg-gray-100"
|
||||
/>
|
||||
<.radio_button_group_field
|
||||
field={f[:multi_session]}
|
||||
options={[{"false", "Single"}, {"true", "Multi"}]}
|
||||
label="Session type"
|
||||
full_width
|
||||
/>
|
||||
<.select_field
|
||||
field={f[:auto_shutdown_ms]}
|
||||
label="Shutdown after inactivity"
|
||||
options={[
|
||||
{"Never", ""},
|
||||
{"5 seconds", "5000"},
|
||||
{"1 minute", "60000"},
|
||||
{"1 hour", "3600000"}
|
||||
]}
|
||||
/>
|
||||
<%= unless Ecto.Changeset.get_field(@changeset, :multi_session) do %>
|
||||
<.checkbox_field field={f[:zero_downtime]} label="Zero-downtime deployment" />
|
||||
<% end %>
|
||||
<%= if Ecto.Changeset.get_field(@changeset, :multi_session) do %>
|
||||
<.checkbox_field field={f[:show_existing_sessions]} label="List existing sessions" />
|
||||
<% end %>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<.checkbox_field
|
||||
field={f[:access_type]}
|
||||
label="Password-protected"
|
||||
checked_value="protected"
|
||||
unchecked_value="public"
|
||||
/>
|
||||
<%= if Ecto.Changeset.get_field(@changeset, :access_type) == :protected do %>
|
||||
<.password_field field={f[:password]} spellcheck="false" phx-debounce class="bg-gray-100" />
|
||||
<% end %>
|
||||
</div>
|
||||
<.checkbox_field field={f[:show_source]} label="Show source" />
|
||||
<.checkbox_field
|
||||
field={f[:output_type]}
|
||||
label="Only render rich outputs"
|
||||
checked_value="rich"
|
||||
unchecked_value="all"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 flex space-x-2">
|
||||
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
|
||||
Deploy
|
||||
</button>
|
||||
<button class="button-base button-outlined-gray bg-transparent" type="reset" name="reset">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"_target" => ["reset"]}, socket) do
|
||||
settings = AppSettings.new()
|
||||
Livebook.Session.set_app_settings(socket.assigns.session.pid, settings)
|
||||
{:noreply, assign(socket, changeset: AppSettings.change(settings))}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"app_settings" => params}, socket) do
|
||||
changeset =
|
||||
socket.assigns.settings
|
||||
|> AppSettings.change(params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
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)}
|
||||
end
|
||||
|
||||
def handle_event("deploy", %{"app_settings" => params}, socket) do
|
||||
case AppSettings.update(socket.assigns.settings, params) do
|
||||
{:ok, settings} ->
|
||||
Livebook.Session.set_app_settings(socket.assigns.session.pid, settings)
|
||||
|
||||
if slug_taken?(settings.slug, socket.assigns.deployed_app_slug) do
|
||||
{:noreply, assign(socket, deploy_confirmation: true)}
|
||||
else
|
||||
Livebook.Session.deploy_app(socket.assigns.session.pid)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("deploy_confirmation_confirm", %{}, socket) do
|
||||
Livebook.Session.deploy_app(socket.assigns.session.pid)
|
||||
{:noreply, assign(socket, deploy_confirmation: false)}
|
||||
end
|
||||
|
||||
def handle_event("deploy_confirmation_cancel", %{}, socket) do
|
||||
{:noreply, assign(socket, deploy_confirmation: false)}
|
||||
end
|
||||
|
||||
def handle_event("terminate_app", %{}, socket) do
|
||||
{:noreply, confirm_app_termination(socket, socket.assigns.app.pid)}
|
||||
end
|
||||
|
@ -343,8 +186,4 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
|
|||
Livebook.Session.app_deactivate(app_session.pid)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp slug_taken?(slug, deployed_app_slug) do
|
||||
slug != deployed_app_slug and Livebook.Apps.exists?(slug)
|
||||
end
|
||||
end
|
||||
|
|
158
lib/livebook_web/live/session_live/app_settings_component.ex
Normal file
158
lib/livebook_web/live/session_live/app_settings_component.ex
Normal file
|
@ -0,0 +1,158 @@
|
|||
defmodule LivebookWeb.SessionLive.AppSettingsComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.Notebook.AppSettings
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
changeset =
|
||||
case socket.assigns do
|
||||
%{changeset: changeset} when changeset.data == assigns.settings -> changeset
|
||||
_ -> AppSettings.change(assigns.settings)
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(changeset: changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-8">
|
||||
<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">
|
||||
<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">
|
||||
<.checkbox_field
|
||||
field={f[:access_type]}
|
||||
label="Password-protected"
|
||||
checked_value="protected"
|
||||
unchecked_value="public"
|
||||
/>
|
||||
<%= if Ecto.Changeset.get_field(@changeset, :access_type) == :protected do %>
|
||||
<.password_field field={f[:password]} spellcheck="false" phx-debounce />
|
||||
<% end %>
|
||||
</div>
|
||||
<.radio_button_group_field
|
||||
field={f[:multi_session]}
|
||||
options={[{"false", "Single-session"}, {"true", "Multi-session"}]}
|
||||
label="Session type"
|
||||
help={
|
||||
~S'''
|
||||
Whether all users should share the
|
||||
same session or be able to manually
|
||||
start multiple sessions.
|
||||
'''
|
||||
}
|
||||
full_width
|
||||
/>
|
||||
<.select_field
|
||||
field={f[:auto_shutdown_ms]}
|
||||
label="Shutdown after inactivity"
|
||||
options={[
|
||||
{"Never", ""},
|
||||
{"5 seconds", "5000"},
|
||||
{"1 minute", "60000"},
|
||||
{"1 hour", "3600000"}
|
||||
]}
|
||||
help={
|
||||
~S'''
|
||||
Shuts down the session if it has no
|
||||
users for the specified amount of time.
|
||||
'''
|
||||
}
|
||||
/>
|
||||
<.checkbox_field
|
||||
field={f[:show_source]}
|
||||
label="Show source"
|
||||
help={
|
||||
~S'''
|
||||
When enabled, it makes notebook source
|
||||
accessible in the app menu.
|
||||
'''
|
||||
}
|
||||
/>
|
||||
<.checkbox_field
|
||||
field={f[:output_type]}
|
||||
label="Only render rich outputs"
|
||||
checked_value="rich"
|
||||
unchecked_value="all"
|
||||
help={
|
||||
~S'''
|
||||
When enabled, hides simple outputs
|
||||
and only shows rich elements, such
|
||||
as inputs, frames, tables, etc.
|
||||
'''
|
||||
}
|
||||
/>
|
||||
<%= if Ecto.Changeset.get_field(@changeset, :multi_session) do %>
|
||||
<.checkbox_field
|
||||
field={f[:show_existing_sessions]}
|
||||
label="List existing sessions"
|
||||
help={
|
||||
~S'''
|
||||
When enabled, the user can see all
|
||||
currently running sessions and join
|
||||
them before creating a new session.
|
||||
'''
|
||||
}
|
||||
/>
|
||||
<% else %>
|
||||
<.checkbox_field
|
||||
field={f[:zero_downtime]}
|
||||
label="Zero-downtime deployment"
|
||||
help={
|
||||
~S'''
|
||||
When enabled, a new version only becomes
|
||||
available after it executes all of its
|
||||
cells, making sure it is ready to use.
|
||||
If an error happens, deploy is aborted.
|
||||
'''
|
||||
}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mt-8 flex space-x-2">
|
||||
<button
|
||||
class="button-base button-blue"
|
||||
type="button"
|
||||
phx-click={JS.patch(~p"/sessions/#{@session.id}") |> JS.push("deploy_app")}
|
||||
disabled={not @changeset.valid?}
|
||||
>
|
||||
<.remix_icon icon="rocket-line" class="align-middle mr-1" />
|
||||
<span>Deploy</span>
|
||||
</button>
|
||||
<button class="button-base button-outlined-gray" type="reset" name="reset">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"_target" => ["reset"]}, socket) do
|
||||
settings = AppSettings.new()
|
||||
Livebook.Session.set_app_settings(socket.assigns.session.pid, settings)
|
||||
{:noreply, assign(socket, changeset: AppSettings.change(settings))}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"app_settings" => params}, socket) do
|
||||
changeset =
|
||||
socket.assigns.settings
|
||||
|> AppSettings.change(params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
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)}
|
||||
end
|
||||
end
|
|
@ -87,6 +87,7 @@ defmodule LivebookWeb.Router do
|
|||
live "/sessions/:id/secrets", SessionLive, :secrets
|
||||
live "/sessions/:id/settings/runtime", SessionLive, :runtime_settings
|
||||
live "/sessions/:id/settings/file", SessionLive, :file_settings
|
||||
live "/sessions/:id/settings/app", SessionLive, :app_settings
|
||||
live "/sessions/:id/bin", SessionLive, :bin
|
||||
get "/sessions/:id/export/download/:format", SessionController, :download_source
|
||||
live "/sessions/:id/export/:tab", SessionLive, :export
|
||||
|
|
|
@ -1409,8 +1409,16 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
Livebook.Apps.subscribe()
|
||||
|
||||
view
|
||||
|> element(~s/[data-el-app-info] form/)
|
||||
|> render_submit(%{"app_settings" => %{"slug" => slug}})
|
||||
|> element(~s/[data-el-app-info] a/, "Configure")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element(~s/#app-settings-modal form/)
|
||||
|> render_change(%{"app_settings" => %{"slug" => slug}})
|
||||
|
||||
view
|
||||
|> element(~s/#app-settings-modal button/, "Deploy")
|
||||
|> render_click()
|
||||
|
||||
assert_receive {:app_created, %{slug: ^slug} = app}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue