Add Form to "+ Block" (#1750)

Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
ByeongUk Choi 2023-05-09 23:49:02 +09:00 committed by GitHub
parent 6061901ee9
commit 6dd19a8dc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 536 additions and 267 deletions

View file

@ -61,7 +61,13 @@ const ConfirmModal = {
}
};
// Events dispatched with JS.dispatch
window.addEventListener("lb:confirm_request", this.handleConfirmRequest);
// Events dispatched with push_event
window.addEventListener(
"phx:lb:confirm_request",
this.handleConfirmRequest
);
this.el.addEventListener("lb:confirm", (event) => {
const { opt_out_id } = confirmEvent.detail;
@ -71,12 +77,21 @@ const ConfirmModal = {
store(OPT_OUT_IDS_KEY, optedOutIds);
}
liveSocket.execJS(confirmEvent.target, confirmEvent.detail.on_confirm);
// Events dispatched with push_event have window as target,
// in which case we pass body, which is an actual element
const target =
confirmEvent.target === window ? document.body : confirmEvent.target;
liveSocket.execJS(target, confirmEvent.detail.on_confirm);
});
},
destroyed() {
window.removeEventListener("lb:confirm_request", this.handleConfirmRequest);
window.removeEventListener(
"phx:lb:confirm_request",
this.handleConfirmRequest
);
},
};

View file

@ -209,17 +209,13 @@ defprotocol Livebook.Runtime do
* `{:runtime_smart_cell_definitions, list(smart_cell_definition())}`
Additionally, the runtime may report extra definitions that require
installing external packages, as described by `:requirement`. Also
see `add_dependencies/3`.
installing external packages, as described by `:requirement_presets`.
Also see `add_dependencies/3`.
"""
@type smart_cell_definition :: %{
kind: String.t(),
name: String.t(),
requirement: nil | smart_cell_requirement()
}
@type smart_cell_requirement :: %{
variants:
requirement_presets:
list(%{
name: String.t(),
packages: list(%{name: String.t(), dependency: dependency()})
@ -238,6 +234,19 @@ defprotocol Livebook.Runtime do
dependency: dependency()
}
@typedoc """
An information about a predefined code block.
"""
@type code_block_definition :: %{
name: String.t(),
variants:
list(%{
name: String.t(),
source: String.t(),
packages: list(%{name: String.t(), dependency: dependency()})
})
}
@typedoc """
A JavaScript view definition.
@ -535,6 +544,18 @@ defprotocol Livebook.Runtime do
{:ok, String.t()} | {:error, String.t()}
def add_dependencies(runtime, code, dependencies)
@doc """
Checks if the given dependencies are installed within the runtime.
"""
@spec has_dependencies?(t(), list(dependency())) :: boolean()
def has_dependencies?(runtime, dependencies)
@doc """
Returns a list of predefined code blocks.
"""
@spec code_block_definitions(t()) :: list(code_block_definition())
def code_block_definitions(runtime)
@doc """
Looks up packages matching the given search.

View file

@ -154,6 +154,14 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
raise "not supported"
end
def has_dependencies?(runtime, dependencies) do
RuntimeServer.has_dependencies?(runtime.server_pid, dependencies)
end
def code_block_definitions(_runtime) do
Livebook.Runtime.Definitions.code_block_definitions()
end
def search_packages(_runtime, _send_to, _search) do
raise "not supported"
end

View file

@ -0,0 +1,192 @@
defmodule Livebook.Runtime.Definitions do
@moduledoc false
kino = %{
name: "kino",
dependency: %{dep: {:kino, "~> 0.9.3"}, config: []}
}
kino_vega_lite = %{
name: "kino_vega_lite",
dependency: %{dep: {:kino_vega_lite, "~> 0.1.7"}, config: []}
}
kino_db = %{
name: "kino_db",
dependency: %{dep: {:kino_db, "~> 0.2.1"}, config: []}
}
kino_maplibre = %{
name: "kino_maplibre",
dependency: %{dep: {:kino_maplibre, "~> 0.1.7"}, config: []}
}
kino_slack = %{
name: "kino_slack",
dependency: %{dep: {:kino_slack, "~> 0.1.1"}, config: []}
}
kino_bumblebee = %{
name: "kino_bumblebee",
dependency: %{dep: {:kino_bumblebee, "~> 0.3.0"}, config: []}
}
exla = %{
name: "exla",
dependency: %{dep: {:exla, "~> 0.5.1"}, config: [nx: [default_backend: EXLA.Backend]]}
}
torchx = %{
name: "torchx",
dependency: %{dep: {:torchx, "~> 0.5.1"}, config: [nx: [default_backend: Torchx.Backend]]}
}
kino_explorer = %{
name: "kino_explorer",
dependency: %{dep: {:kino_explorer, "~> 0.1.4"}, config: []}
}
windows? = match?({:win32, _}, :os.type())
nx_backend_package = if(windows?, do: torchx, else: exla)
@smart_cell_definitions [
%{
kind: "Elixir.KinoDB.ConnectionCell",
name: "Database connection",
requirement_presets: [
%{
name: "Amazon Athena",
packages: [
kino_db,
%{
name: "req_athena",
dependency: %{dep: {:req_athena, "~> 0.1.3"}, config: []}
}
]
},
%{
name: "Google BigQuery",
packages: [
kino_db,
%{
name: "req_bigquery",
dependency: %{dep: {:req_bigquery, "~> 0.1.1"}, config: []}
}
]
},
%{
name: "MySQL",
packages: [
kino_db,
%{name: "myxql", dependency: %{dep: {:myxql, "~> 0.6.2"}, config: []}}
]
},
%{
name: "PostgreSQL",
packages: [
kino_db,
%{name: "postgrex", dependency: %{dep: {:postgrex, "~> 0.16.3"}, config: []}}
]
},
%{
name: "SQLite",
packages: [
kino_db,
%{name: "exqlite", dependency: %{dep: {:exqlite, "~> 0.11.0"}, config: []}}
]
}
]
},
%{
kind: "Elixir.KinoDB.SQLCell",
name: "SQL query",
requirement_presets: [
%{
name: "Default",
packages: [kino_db]
}
]
},
%{
kind: "Elixir.KinoVegaLite.ChartCell",
name: "Chart",
requirement_presets: [
%{
name: "Default",
packages: [kino_vega_lite]
}
]
},
%{
kind: "Elixir.KinoMapLibre.MapCell",
name: "Map",
requirement_presets: [
%{
name: "Default",
packages: [kino_maplibre]
}
]
},
%{
kind: "Elixir.KinoSlack.MessageCell",
name: "Slack message",
requirement_presets: [
%{
name: "Default",
packages: [kino_slack]
}
]
},
%{
kind: "Elixir.KinoBumblebee.TaskCell",
name: "Neural Network task",
requirement_presets: [
%{
name: "Default",
packages: [kino_bumblebee, nx_backend_package]
}
]
},
%{
kind: "Elixir.KinoExplorer.DataTransformCell",
name: "Data transform",
requirement_presets: [
%{
name: "Default",
packages: [kino_explorer]
}
]
}
]
@code_block_definitions [
%{
name: "Form",
variants: [
%{
name: "Default",
source: """
form =
Kino.Control.form(
[
name: Kino.Input.text("Name")
],
submit: "Submit"
)
Kino.listen(form, fn event ->
IO.inspect(event)
end)
form\
""",
packages: [kino]
}
]
}
]
def smart_cell_definitions(), do: @smart_cell_definitions
def code_block_definitions(), do: @code_block_definitions
end

View file

@ -17,173 +17,6 @@ defmodule Livebook.Runtime.ElixirStandalone do
server_pid: pid() | nil
}
kino_vega_lite = %{
name: "kino_vega_lite",
dependency: %{dep: {:kino_vega_lite, "~> 0.1.7"}, config: []}
}
kino_db = %{
name: "kino_db",
dependency: %{dep: {:kino_db, "~> 0.2.1"}, config: []}
}
kino_maplibre = %{
name: "kino_maplibre",
dependency: %{dep: {:kino_maplibre, "~> 0.1.7"}, config: []}
}
kino_slack = %{
name: "kino_slack",
dependency: %{dep: {:kino_slack, "~> 0.1.1"}, config: []}
}
kino_bumblebee = %{
name: "kino_bumblebee",
dependency: %{dep: {:kino_bumblebee, "~> 0.3.0"}, config: []}
}
exla = %{
name: "exla",
dependency: %{dep: {:exla, "~> 0.5.1"}, config: [nx: [default_backend: EXLA.Backend]]}
}
torchx = %{
name: "torchx",
dependency: %{dep: {:torchx, "~> 0.5.1"}, config: [nx: [default_backend: Torchx.Backend]]}
}
kino_explorer = %{
name: "kino_explorer",
dependency: %{dep: {:kino_explorer, "~> 0.1.4"}, config: []}
}
windows? = match?({:win32, _}, :os.type())
nx_backend_package = if(windows?, do: torchx, else: exla)
@extra_smart_cell_definitions [
%{
kind: "Elixir.KinoDB.ConnectionCell",
name: "Database connection",
requirement: %{
variants: [
%{
name: "Amazon Athena",
packages: [
kino_db,
%{
name: "req_athena",
dependency: %{dep: {:req_athena, "~> 0.1.3"}, config: []}
}
]
},
%{
name: "Google BigQuery",
packages: [
kino_db,
%{
name: "req_bigquery",
dependency: %{dep: {:req_bigquery, "~> 0.1.1"}, config: []}
}
]
},
%{
name: "MySQL",
packages: [
kino_db,
%{name: "myxql", dependency: %{dep: {:myxql, "~> 0.6.2"}, config: []}}
]
},
%{
name: "PostgreSQL",
packages: [
kino_db,
%{name: "postgrex", dependency: %{dep: {:postgrex, "~> 0.16.3"}, config: []}}
]
},
%{
name: "SQLite",
packages: [
kino_db,
%{name: "exqlite", dependency: %{dep: {:exqlite, "~> 0.11.0"}, config: []}}
]
}
]
}
},
%{
kind: "Elixir.KinoDB.SQLCell",
name: "SQL query",
requirement: %{
variants: [
%{
name: "Default",
packages: [kino_db]
}
]
}
},
%{
kind: "Elixir.KinoVegaLite.ChartCell",
name: "Chart",
requirement: %{
variants: [
%{
name: "Default",
packages: [kino_vega_lite]
}
]
}
},
%{
kind: "Elixir.KinoMapLibre.MapCell",
name: "Map",
requirement: %{
variants: [
%{
name: "Default",
packages: [kino_maplibre]
}
]
}
},
%{
kind: "Elixir.KinoSlack.MessageCell",
name: "Slack message",
requirement: %{
variants: [
%{
name: "Default",
packages: [kino_slack]
}
]
}
},
%{
kind: "Elixir.KinoBumblebee.TaskCell",
name: "Neural Network task",
requirement: %{
variants: [
%{
name: "Default",
packages: [kino_bumblebee, nx_backend_package]
}
]
}
},
%{
kind: "Elixir.KinoExplorer.DataTransformCell",
name: "Data transform",
requirement: %{
variants: [
%{
name: "Default",
packages: [kino_explorer]
}
]
}
}
]
@doc """
Returns a new runtime instance.
"""
@ -213,7 +46,9 @@ defmodule Livebook.Runtime.ElixirStandalone do
argv = [parent_node]
init_opts = [
runtime_server_opts: [extra_smart_cell_definitions: @extra_smart_cell_definitions]
runtime_server_opts: [
extra_smart_cell_definitions: Livebook.Runtime.Definitions.smart_cell_definitions()
]
]
with {:ok, elixir_path} <- find_elixir_executable(),
@ -321,6 +156,14 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
Livebook.Runtime.Dependencies.add_dependencies(code, dependencies)
end
def has_dependencies?(runtime, dependencies) do
RuntimeServer.has_dependencies?(runtime.server_pid, dependencies)
end
def code_block_definitions(_runtime) do
Livebook.Runtime.Definitions.code_block_definitions()
end
def search_packages(_runtime, send_to, search) do
Livebook.Runtime.Dependencies.search_packages_on_hex(send_to, search)
end

View file

@ -124,6 +124,14 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
Livebook.Runtime.Dependencies.add_dependencies(code, dependencies)
end
def has_dependencies?(runtime, dependencies) do
RuntimeServer.has_dependencies?(runtime.server_pid, dependencies)
end
def code_block_definitions(_runtime) do
Livebook.Runtime.Definitions.code_block_definitions()
end
def search_packages(_runtime, send_to, search) do
{mod, fun, args} = config()[:load_packages]
packages = apply(mod, fun, args)

View file

@ -21,6 +21,7 @@ defmodule Livebook.Runtime.ErlDist do
# Modules to load into the connected node.
def required_modules do
[
Livebook.Runtime.Definitions,
Livebook.Runtime.Evaluator,
Livebook.Runtime.Evaluator.IOProxy,
Livebook.Runtime.Evaluator.Tracer,

View file

@ -243,6 +243,15 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
GenServer.cast(pid, {:stop_smart_cell, ref})
end
@doc """
Checks if the given dependencies are already installed within the
runtime.
"""
@spec has_dependencies?(pid(), list(Runtime.dependency())) :: boolean()
def has_dependencies?(pid, dependencies) do
GenServer.call(pid, {:has_dependencies?, dependencies})
end
@doc """
Disables dependencies cache globally.
"""
@ -645,6 +654,11 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
{:reply, reply, state}
end
def handle_call({:has_dependencies?, dependencies}, _from, state) do
has_dependencies? = Enum.all?(dependencies, &dependency_installed?/1)
{:reply, has_dependencies?, state}
end
defp file_path(state, file_id) do
if tmp_dir = state.tmp_dir do
Path.join([tmp_dir, "files", file_id])
@ -698,7 +712,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
else
available_defs =
for definition <- smart_cell_definitions,
do: %{kind: definition.kind, name: definition.name, requirement: nil}
do: %{kind: definition.kind, name: definition.name, requirement_presets: []}
defs = Enum.uniq_by(available_defs ++ state.extra_smart_cell_definitions, & &1.kind)
@ -853,4 +867,9 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|> Enum.reduce(:erlang.md5_init(), &:erlang.md5_update(&2, &1))
|> :erlang.md5_final()
end
defp dependency_installed?(dependency) do
name = elem(dependency.dep, 0)
Application.spec(name) != nil
end
end

View file

@ -1908,7 +1908,7 @@ defmodule Livebook.Session.Data do
kinds =
for definition <- data.smart_cell_definitions,
definition.requirement == nil,
definition.requirement_presets == [],
do: definition.kind
cells_ready_to_start = Enum.filter(dead_cells, fn {cell, _} -> cell.kind in kinds end)

View file

@ -276,8 +276,23 @@ defmodule LivebookWeb.CoreComponents do
}>
Delete
</button>
"""
def with_confirm(js \\ %JS{}, on_confirm, opts) do
JS.dispatch(js, "lb:confirm_request", detail: confirm_payload(on_confirm, opts))
end
@doc """
Asks the client to show a confirmation modal before executing the
given JS action.
Same as `with_confirm/3`, except it is triggered from the server.
"""
def confirm(socket, on_confirm, opts) do
Phoenix.LiveView.push_event(socket, "lb:confirm_request", confirm_payload(on_confirm, opts))
end
defp confirm_payload(on_confirm, opts) do
opts =
Keyword.validate!(
opts,
@ -292,18 +307,16 @@ defmodule LivebookWeb.CoreComponents do
]
)
JS.dispatch(js, "lb:confirm_request",
detail: %{
on_confirm: Jason.encode!(on_confirm.ops),
title: opts[:title],
description: Keyword.fetch!(opts, :description),
confirm_text: opts[:confirm_text],
confirm_icon: opts[:confirm_icon],
danger: opts[:danger],
html: opts[:html],
opt_out_id: opts[:opt_out_id]
}
)
%{
on_confirm: Jason.encode!(on_confirm.ops),
title: opts[:title],
description: Keyword.fetch!(opts, :description),
confirm_text: opts[:confirm_text],
confirm_icon: opts[:confirm_icon],
danger: opts[:danger],
html: opts[:html],
opt_out_id: opts[:opt_out_id]
}
end
@doc """

View file

@ -98,4 +98,10 @@ defmodule LivebookWeb.Helpers do
{leading, [last]} = Enum.split(list, -1)
Enum.join(leading, ", ") <> " and " <> last
end
@doc """
Wraps the given text in a `<code>` tag.
"""
@spec code_tag(String.t()) :: String.t()
def code_tag(text), do: "<code>#{text}</code>"
end

View file

@ -917,6 +917,83 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket}
end
def handle_event("insert_code_block_below", params, socket) do
data = socket.private.data
add_dependencies? = params["add_dependencies"] == true
with {:ok, section, index} <-
section_with_next_index(data.notebook, params["section_id"], params["cell_id"]),
{:ok, definition} <- code_block_definition_by_name(data, params["definition_name"]) do
variant = Enum.fetch!(definition.variants, params["variant_idx"])
dependencies = Enum.map(variant.packages, & &1.dependency)
has_dependencies? =
dependencies == [] or Livebook.Runtime.has_dependencies?(data.runtime, dependencies)
cond do
has_dependencies? or add_dependencies? ->
attrs = %{source: variant.source}
Session.insert_cell(socket.assigns.session.pid, section.id, index, :code, attrs)
socket =
if has_dependencies? do
socket
else
add_dependencies_and_reevaluate(socket, dependencies)
end
{:noreply, socket}
Livebook.Runtime.fixed_dependencies?(data.runtime) ->
{:noreply,
put_flash(socket, :error, "This runtime doesn't support adding dependencies")}
true ->
js = JS.push("insert_code_block_below", value: put_in(params["add_dependencies"], true))
socket = confirm_add_packages(socket, js, variant.packages, definition.name, "block")
{:noreply, socket}
end
else
_ -> {:noreply, socket}
end
end
def handle_event("insert_smart_cell_below", params, socket) do
data = socket.private.data
add_dependencies? = params["add_dependencies"] == true
with {:ok, section, index} <-
section_with_next_index(data.notebook, params["section_id"], params["cell_id"]),
{:ok, definition} <- smart_cell_definition_by_kind(data, params["kind"]) do
preset =
if preset_idx = params["preset_idx"] do
Enum.at(definition.requirement_presets, preset_idx)
end
if preset == nil or add_dependencies? do
attrs = %{kind: params["kind"]}
Session.insert_cell(socket.assigns.session.pid, section.id, index, :smart, attrs)
socket =
if preset == nil do
socket
else
{:ok, preset} = Enum.fetch(definition.requirement_presets, preset_idx)
dependencies = Enum.map(preset.packages, & &1.dependency)
add_dependencies_and_reevaluate(socket, dependencies)
end
{:noreply, socket}
else
js = JS.push("insert_smart_cell_below", value: put_in(params["add_dependencies"], true))
socket = confirm_add_packages(socket, js, preset.packages, definition.name, "smart cell")
{:noreply, socket}
end
else
_ -> {:noreply, socket}
end
end
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
Session.delete_cell(socket.assigns.session.pid, cell_id)
@ -1005,17 +1082,8 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket}
end
def handle_event(
"add_smart_cell_dependencies",
%{"kind" => kind, "variant_idx" => variant_idx},
socket
) do
with %{requirement: %{variants: variants}} <-
Enum.find(socket.private.data.smart_cell_definitions, &(&1.kind == kind)),
{:ok, variant} <- Enum.fetch(variants, variant_idx) do
dependencies = Enum.map(variant.packages, & &1.dependency)
Session.add_dependencies(socket.assigns.session.pid, dependencies)
end
def handle_event("add_form_cell_dependencies", %{}, socket) do
Session.add_dependencies(socket.assigns.session.pid, [%{dep: {:kino, "~> 0.8.1"}, config: []}])
{status, socket} = maybe_reconnect_runtime(socket)
@ -1067,12 +1135,6 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket}
end
def handle_event("queue_cells_reevaluation", %{}, socket) do
Session.queue_cells_reevaluation(socket.assigns.session.pid)
{:noreply, socket}
end
def handle_event(
"set_reevaluate_automatically",
%{"value" => value, "cell_id" => cell_id},
@ -1666,10 +1728,6 @@ defmodule LivebookWeb.SessionLive do
defp cell_type_and_attrs_from_params(%{"type" => "markdown"}), do: {:markdown, %{}}
defp cell_type_and_attrs_from_params(%{"type" => "code"}), do: {:code, %{}}
defp cell_type_and_attrs_from_params(%{"type" => "smart", "kind" => kind}) do
{:smart, %{kind: kind}}
end
defp cell_type_and_attrs_from_params(%{"type" => "diagram"}) do
source = """
<!-- Learn more at https://mermaid-js.github.io/mermaid -->
@ -1773,6 +1831,55 @@ defmodule LivebookWeb.SessionLive do
for info <- starred_notebooks, into: MapSet.new(), do: info.file
end
defp code_block_definition_by_name(data, name) do
data.runtime
|> Livebook.Runtime.code_block_definitions()
|> Enum.find_value(:error, &(&1.name == name && {:ok, &1}))
end
defp smart_cell_definition_by_kind(data, kind) do
Enum.find_value(data.smart_cell_definitions, :error, &(&1.kind == kind && {:ok, &1}))
end
defp add_dependencies_and_reevaluate(socket, dependencies) do
Session.add_dependencies(socket.assigns.session.pid, dependencies)
{status, socket} = maybe_reconnect_runtime(socket)
if status == :ok do
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
Session.queue_cells_reevaluation(socket.assigns.session.pid)
end
socket
end
defp confirm_add_packages(socket, js, packages, target_name, target_type) do
confirm(socket, js,
title: "Add packages",
description:
case packages do
[package] ->
~s'''
The <span class="font-semibold">#{target_name}“</span> #{target_type} requires
the #{code_tag(package.name)} package. Do you want to add it as a dependency
and restart?
'''
packages ->
~s'''
The <span class="font-semibold">#{target_name}“</span> #{target_type} requires the
#{packages |> Enum.map(&code_tag(&1.name)) |> format_items()} packages. Do you want
to add them as dependencies and restart?
'''
end,
confirm_text: "Add and restart",
confirm_icon: "add-line",
danger: false,
html: true
)
end
# Builds view-specific structure of data by cherry-picking
# only the relevant attributes.
# We then use `@data_view` in the templates and consequently

View file

@ -1,6 +1,8 @@
defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
use LivebookWeb, :live_component
defguardp is_many(list) when tl(list) != []
def render(assigns) do
~H"""
<div
@ -73,22 +75,22 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
<span>Image</span>
</.link>
</.menu_item>
<.menu_item :for={definition <- Livebook.Runtime.code_block_definitions(@runtime)}>
<.code_block_insert_button
definition={definition}
runtime={@runtime}
section_id={@section_id}
cell_id={@cell_id}
/>
</.menu_item>
</.menu>
<%= cond do %>
<% not Livebook.Runtime.connected?(@runtime) -> %>
<button
class="button-base button-small"
phx-click={
with_confirm(
JS.push("setup_default_runtime"),
title: "Setup runtime",
description: ~s'''
To see the available smart cells, you need a connected runtime.
Do you want to connect and setup the default one?
''',
confirm_text: "Setup runtime",
confirm_icon: "play-line",
danger: false
setup_runtime_with_confirm(
"To see the available smart cells, you need a connected runtime."
)
}
>
@ -117,18 +119,19 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
"""
end
defp smart_cell_insert_button(%{definition: %{requirement: %{variants: [_, _ | _]}}} = assigns) do
defp code_block_insert_button(assigns) when is_many(assigns.definition.variants) do
~H"""
<.submenu>
<:primary>
<button role="menuitem">
<.remix_icon icon="terminal-box-line" />
<span><%= @definition.name %></span>
</button>
</:primary>
<.menu_item :for={{variant, idx} <- Enum.with_index(@definition.requirement.variants)}>
<.menu_item :for={{variant, idx} <- Enum.with_index(@definition.variants)}>
<button
role="menuitem"
phx-click={on_smart_cell_click(@definition, idx, @section_id, @cell_id)}
phx-click={on_code_block_click(@definition, idx, @runtime, @section_id, @cell_id)}
>
<span><%= variant.name %></span>
</button>
@ -137,60 +140,84 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
"""
end
defp smart_cell_insert_button(assigns) do
defp code_block_insert_button(assigns) do
~H"""
<button role="menuitem" phx-click={on_smart_cell_click(@definition, 0, @section_id, @cell_id)}>
<button
role="menuitem"
phx-click={on_code_block_click(@definition, 0, @runtime, @section_id, @cell_id)}
>
<.remix_icon icon="terminal-box-line" />
<span><%= @definition.name %></span>
</button>
"""
end
defp on_smart_cell_click(%{requirement: nil} = definition, _variant_idx, section_id, cell_id) do
insert_smart_cell(definition, section_id, cell_id)
defp smart_cell_insert_button(assigns) when is_many(assigns.definition.requirement_presets) do
~H"""
<.submenu>
<:primary>
<button role="menuitem">
<span><%= @definition.name %></span>
</button>
</:primary>
<.menu_item :for={{preset, idx} <- Enum.with_index(@definition.requirement_presets)}>
<button
role="menuitem"
phx-click={on_smart_cell_click(@definition, idx, @section_id, @cell_id)}
>
<span><%= preset.name %></span>
</button>
</.menu_item>
</.submenu>
"""
end
defp on_smart_cell_click(%{requirement: %{}} = definition, variant_idx, section_id, cell_id) do
variant = Enum.fetch!(definition.requirement.variants, variant_idx)
defp smart_cell_insert_button(assigns) do
~H"""
<button role="menuitem" phx-click={on_smart_cell_click(@definition, @section_id, @cell_id)}>
<span><%= @definition.name %></span>
</button>
"""
end
with_confirm(
JS.push("add_smart_cell_dependencies",
value: %{kind: definition.kind, variant_idx: variant_idx}
defp on_code_block_click(definition, variant_idx, runtime, section_id, cell_id) do
if Livebook.Runtime.connected?(runtime) do
JS.push("insert_code_block_below",
value: %{
definition_name: definition.name,
variant_idx: variant_idx,
section_id: section_id,
cell_id: cell_id
}
)
|> insert_smart_cell(definition, section_id, cell_id)
|> JS.push("queue_cells_reevaluation"),
title: "Add packages",
description:
case variant.packages do
[%{name: name}] ->
~s'''
The <span class="font-semibold">#{definition.name}“</span>
smart cell requires the #{code_tag(name)} package. Do you want to add
it as a dependency and restart?
'''
else
setup_runtime_with_confirm("To insert this block, you need a connected runtime.")
end
end
packages ->
~s'''
The <span class="font-semibold">#{definition.name}“</span>
smart cell requires the #{packages |> Enum.map(&code_tag(&1.name)) |> format_items()}
packages. Do you want to add them as dependencies and restart?
'''
end,
confirm_text: "Add and restart",
confirm_icon: "add-line",
danger: false,
html: true
defp setup_runtime_with_confirm(reason) do
with_confirm(
JS.push("setup_default_runtime"),
title: "Setup runtime",
description: "#{reason} Do you want to connect and setup the default one?",
confirm_text: "Setup runtime",
confirm_icon: "play-line",
danger: false
)
end
defp code_tag(text), do: "<code>#{text}</code>"
defp on_smart_cell_click(definition, section_id, cell_id) do
preset_idx = if definition.requirement_presets == [], do: nil, else: 0
on_smart_cell_click(definition, preset_idx, section_id, cell_id)
end
defp insert_smart_cell(js \\ %JS{}, definition, section_id, cell_id) do
JS.push(js, "insert_cell_below",
defp on_smart_cell_click(definition, preset_idx, section_id, cell_id) do
JS.push("insert_smart_cell_below",
value: %{
type: "smart",
kind: definition.kind,
section_id: section_id,
cell_id: cell_id
cell_id: cell_id,
preset_idx: preset_idx
}
)
end

View file

@ -8,7 +8,7 @@ defmodule Livebook.Session.DataTest do
alias Livebook.Users.User
@eval_resp {:ok, [1, 2, 3]}
@smart_cell_definitions [%{kind: "text", name: "Text", requirement: nil}]
@smart_cell_definitions [%{kind: "text", name: "Text", requirement_presets: []}]
@cid "__anonymous__"
defp eval_meta(opts \\ []) do

View file

@ -123,7 +123,8 @@ defmodule Livebook.SessionTest do
send(
session.pid,
{:runtime_smart_cell_definitions, [%{kind: "text", name: "Text", requirement: nil}]}
{:runtime_smart_cell_definitions,
[%{kind: "text", name: "Text", requirement_presets: []}]}
)
send(
@ -851,7 +852,8 @@ defmodule Livebook.SessionTest do
send(
session.pid,
{:runtime_smart_cell_definitions, [%{kind: "text", name: "Text", requirement: nil}]}
{:runtime_smart_cell_definitions,
[%{kind: "text", name: "Text", requirement_presets: []}]}
)
Session.subscribe(session.id)
@ -878,7 +880,8 @@ defmodule Livebook.SessionTest do
send(
session.pid,
{:runtime_smart_cell_definitions, [%{kind: "text", name: "Text", requirement: nil}]}
{:runtime_smart_cell_definitions,
[%{kind: "text", name: "Text", requirement_presets: []}]}
)
server_pid = self()
@ -915,7 +918,8 @@ defmodule Livebook.SessionTest do
send(
session.pid,
{:runtime_smart_cell_definitions, [%{kind: "text", name: "Text", requirement: nil}]}
{:runtime_smart_cell_definitions,
[%{kind: "text", name: "Text", requirement_presets: []}]}
)
server_pid = self()
@ -953,7 +957,8 @@ defmodule Livebook.SessionTest do
send(
session.pid,
{:runtime_smart_cell_definitions, [%{kind: "text", name: "Text", requirement: nil}]}
{:runtime_smart_cell_definitions,
[%{kind: "text", name: "Text", requirement_presets: []}]}
)
Session.subscribe(session.id)

View file

@ -538,7 +538,7 @@ defmodule LivebookWeb.SessionLiveTest do
send(
session.pid,
{:runtime_smart_cell_definitions,
[%{kind: "dbconn", name: "Database connection", requirement: nil}]}
[%{kind: "dbconn", name: "Database connection", requirement_presets: []}]}
)
wait_for_session_update(session.pid)

View file

@ -50,6 +50,10 @@ defmodule Livebook.Runtime.NoopRuntime do
Livebook.Runtime.Dependencies.add_dependencies(code, dependencies)
end
def has_dependencies?(_runtime, _dependencies), do: true
def code_block_definitions(_runtime), do: []
def search_packages(_, _, _), do: make_ref()
def disable_dependencies_cache(_), do: :ok