diff --git a/assets/js/hooks/confirm_modal.js b/assets/js/hooks/confirm_modal.js index 705a69e24..f24ad62d3 100644 --- a/assets/js/hooks/confirm_modal.js +++ b/assets/js/hooks/confirm_modal.js @@ -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 + ); }, }; diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index dbda87be4..6814387e2 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -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. diff --git a/lib/livebook/runtime/attached.ex b/lib/livebook/runtime/attached.ex index 1ed4b2bd9..ed2577467 100644 --- a/lib/livebook/runtime/attached.ex +++ b/lib/livebook/runtime/attached.ex @@ -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 diff --git a/lib/livebook/runtime/definitions.ex b/lib/livebook/runtime/definitions.ex new file mode 100644 index 000000000..f00c8c3fe --- /dev/null +++ b/lib/livebook/runtime/definitions.ex @@ -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 diff --git a/lib/livebook/runtime/elixir_standalone.ex b/lib/livebook/runtime/elixir_standalone.ex index b58dd6b5a..2a27274b8 100644 --- a/lib/livebook/runtime/elixir_standalone.ex +++ b/lib/livebook/runtime/elixir_standalone.ex @@ -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 diff --git a/lib/livebook/runtime/embedded.ex b/lib/livebook/runtime/embedded.ex index 1f2d21a64..dfe98a224 100644 --- a/lib/livebook/runtime/embedded.ex +++ b/lib/livebook/runtime/embedded.ex @@ -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) diff --git a/lib/livebook/runtime/erl_dist.ex b/lib/livebook/runtime/erl_dist.ex index b0dac1147..96a5de2b4 100644 --- a/lib/livebook/runtime/erl_dist.ex +++ b/lib/livebook/runtime/erl_dist.ex @@ -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, diff --git a/lib/livebook/runtime/erl_dist/runtime_server.ex b/lib/livebook/runtime/erl_dist/runtime_server.ex index aa283f85a..e7e474bd5 100644 --- a/lib/livebook/runtime/erl_dist/runtime_server.ex +++ b/lib/livebook/runtime/erl_dist/runtime_server.ex @@ -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 diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex index ea69a6ecc..30e5f11b0 100644 --- a/lib/livebook/session/data.ex +++ b/lib/livebook/session/data.ex @@ -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) diff --git a/lib/livebook_web/components/core_components.ex b/lib/livebook_web/components/core_components.ex index d63edc62d..a092da177 100644 --- a/lib/livebook_web/components/core_components.ex +++ b/lib/livebook_web/components/core_components.ex @@ -276,8 +276,23 @@ defmodule LivebookWeb.CoreComponents do }> Delete + """ 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 """ diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index 2ea3ab2f5..b66feee5d 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -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 `` tag. + """ + @spec code_tag(String.t()) :: String.t() + def code_tag(text), do: "#{text}" end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 2d437bc6e..7e1b8b6b2 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -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 = """ @@ -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 “#{target_name}“ #{target_type} requires + the #{code_tag(package.name)} package. Do you want to add it as a dependency + and restart? + ''' + + packages -> + ~s''' + The “#{target_name}“ #{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 diff --git a/lib/livebook_web/live/session_live/insert_buttons_component.ex b/lib/livebook_web/live/session_live/insert_buttons_component.ex index 52c131bb2..494fc7d73 100644 --- a/lib/livebook_web/live/session_live/insert_buttons_component.ex +++ b/lib/livebook_web/live/session_live/insert_buttons_component.ex @@ -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"""
Image + <.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} + /> + <%= cond do %> <% not Livebook.Runtime.connected?(@runtime) -> %> - <.menu_item :for={{variant, idx} <- Enum.with_index(@definition.requirement.variants)}> + <.menu_item :for={{variant, idx} <- Enum.with_index(@definition.variants)}> @@ -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""" - """ 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> + + + <.menu_item :for={{preset, idx} <- Enum.with_index(@definition.requirement_presets)}> + + + + """ 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""" + + """ + 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 “#{definition.name}“ - 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 “#{definition.name}“ - 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: "#{text}" + 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 diff --git a/test/livebook/session/data_test.exs b/test/livebook/session/data_test.exs index a61b6b511..a72d44f2b 100644 --- a/test/livebook/session/data_test.exs +++ b/test/livebook/session/data_test.exs @@ -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 diff --git a/test/livebook/session_test.exs b/test/livebook/session_test.exs index 451e42d86..160601b43 100644 --- a/test/livebook/session_test.exs +++ b/test/livebook/session_test.exs @@ -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) diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs index 95b7fa924..02a5260de 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -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) diff --git a/test/support/noop_runtime.ex b/test/support/noop_runtime.ex index 6f8f6f806..b14323ad3 100644 --- a/test/support/noop_runtime.ex +++ b/test/support/noop_runtime.ex @@ -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