mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
List predefined smart cells and automatically add their dependencies (#1078)
* List predefined smart cells and automatically add their dependencies * Generalize dependency insertion to multiple entries * Add runtime setup modal * Don't evaluate setup when restarting the runtime fails * Automatically add vega_lite for chart builder * Improve confirmation modal actions when the action is not destructive
This commit is contained in:
parent
108b40651d
commit
df765f2dd3
|
@ -10,6 +10,8 @@ const ConfirmModal = {
|
|||
const descriptionEl = this.el.querySelector("[data-description]");
|
||||
const confirmIconEl = this.el.querySelector("[data-confirm-icon]");
|
||||
const confirmTextEl = this.el.querySelector("[data-confirm-text]");
|
||||
const confirmButtonEl = this.el.querySelector("[data-confirm-button]");
|
||||
const actionsEl = this.el.querySelector("[data-actions]");
|
||||
const optOutEl = this.el.querySelector("[data-opt-out]");
|
||||
const optOutElCheckbox = optOutEl.querySelector("input");
|
||||
|
||||
|
@ -18,8 +20,14 @@ const ConfirmModal = {
|
|||
this.handleConfirmRequest = (event) => {
|
||||
confirmEvent = event;
|
||||
|
||||
const { title, description, confirm_text, confirm_icon, opt_out_id } =
|
||||
event.detail;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
confirm_text,
|
||||
confirm_icon,
|
||||
danger,
|
||||
opt_out_id,
|
||||
} = event.detail;
|
||||
|
||||
if (opt_out_id && optedOutIds.includes(opt_out_id)) {
|
||||
liveSocket.execJS(event.target, event.detail.on_confirm);
|
||||
|
@ -34,6 +42,11 @@ const ConfirmModal = {
|
|||
confirmIconEl.className = "hidden";
|
||||
}
|
||||
|
||||
confirmButtonEl.classList.toggle("button-red", danger);
|
||||
confirmButtonEl.classList.toggle("button-blue", !danger);
|
||||
actionsEl.classList.toggle("flex-row-reverse", danger);
|
||||
actionsEl.classList.toggle("space-x-reverse", danger);
|
||||
|
||||
optOutElCheckbox.checked = false;
|
||||
optOutEl.classList.toggle("hidden", !opt_out_id);
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ defmodule Livebook.Notebook do
|
|||
"""
|
||||
@spec put_setup_cell(t(), Cell.Code.t()) :: t()
|
||||
def put_setup_cell(notebook, %Cell.Code{} = cell) do
|
||||
put_in(notebook.setup_section.cells, [%{cell | id: "setup"}])
|
||||
put_in(notebook.setup_section.cells, [%{cell | id: Cell.setup_cell_id()}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -68,12 +68,20 @@ defmodule Livebook.Notebook.Cell do
|
|||
|
||||
def find_inputs_in_output(_output), do: []
|
||||
|
||||
@setup_cell_id "setup"
|
||||
|
||||
@doc """
|
||||
Checks if the given cell is the setup code cell.
|
||||
"""
|
||||
@spec setup?(t()) :: boolean()
|
||||
def setup?(cell)
|
||||
|
||||
def setup?(%Cell.Code{id: "setup"}), do: true
|
||||
def setup?(%Cell.Code{id: @setup_cell_id}), do: true
|
||||
def setup?(_cell), do: false
|
||||
|
||||
@doc """
|
||||
The fixed identifier of the setup cell.
|
||||
"""
|
||||
@spec setup_cell_id() :: id()
|
||||
def setup_cell_id(), do: @setup_cell_id
|
||||
end
|
||||
|
|
|
@ -187,12 +187,19 @@ defprotocol Livebook.Runtime do
|
|||
the updated list as
|
||||
|
||||
* `{: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`.
|
||||
"""
|
||||
@type smart_cell_definition :: %{
|
||||
kind: String.t(),
|
||||
name: String.t()
|
||||
name: String.t(),
|
||||
requirement: nil | %{name: String.t(), dependencies: list(dependency())}
|
||||
}
|
||||
|
||||
@type dependency :: term()
|
||||
|
||||
@typedoc """
|
||||
A JavaScript view definition.
|
||||
|
||||
|
@ -397,4 +404,11 @@ defprotocol Livebook.Runtime do
|
|||
"""
|
||||
@spec stop_smart_cell(t(), smart_cell_ref()) :: :ok
|
||||
def stop_smart_cell(runtime, ref)
|
||||
|
||||
@doc """
|
||||
Updates the given source code to install the given dependencies.
|
||||
"""
|
||||
@spec add_dependencies(t(), String.t(), list(dependency())) ::
|
||||
{:ok, String.t()} | {:error, String.t()}
|
||||
def add_dependencies(runtime, code, dependencies)
|
||||
end
|
||||
|
|
|
@ -28,8 +28,11 @@ defmodule Livebook.Runtime.Attached do
|
|||
|
||||
case Node.ping(node) do
|
||||
:pong ->
|
||||
opts = [parent_node: node()]
|
||||
server_pid = Livebook.Runtime.ErlDist.initialize(node, opts)
|
||||
server_pid =
|
||||
Livebook.Runtime.ErlDist.initialize(node,
|
||||
node_manager_opts: [parent_node: node()]
|
||||
)
|
||||
|
||||
{:ok, %__MODULE__{node: node, cookie: cookie, server_pid: server_pid}}
|
||||
|
||||
:pang ->
|
||||
|
@ -96,4 +99,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
|
|||
def stop_smart_cell(runtime, ref) do
|
||||
ErlDist.RuntimeServer.stop_smart_cell(runtime.server_pid, ref)
|
||||
end
|
||||
|
||||
def add_dependencies(_runtime, code, dependencies) do
|
||||
Livebook.Runtime.Code.add_mix_deps(code, dependencies)
|
||||
end
|
||||
end
|
||||
|
|
99
lib/livebook/runtime/code.ex
Normal file
99
lib/livebook/runtime/code.ex
Normal file
|
@ -0,0 +1,99 @@
|
|||
defmodule Livebook.Runtime.Code do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Finds or adds a `Mix.install/2` call to `code` and modifies it to
|
||||
include the given Mix deps.
|
||||
"""
|
||||
@spec add_mix_deps(String.t(), list(tuple())) :: {:ok, String.t()} | {:error, String.t()}
|
||||
def add_mix_deps(code, deps) do
|
||||
with {:ok, ast, comments} <- string_to_quoted_with_comments(code),
|
||||
{:ok, ast} <- insert_deps(ast, deps),
|
||||
do: {:ok, format(ast, comments)}
|
||||
end
|
||||
|
||||
defp string_to_quoted_with_comments(code) do
|
||||
try do
|
||||
to_quoted_opts = [
|
||||
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
|
||||
token_metadata: true,
|
||||
unescape: false
|
||||
]
|
||||
|
||||
{ast, comments} = Code.string_to_quoted_with_comments!(code, to_quoted_opts)
|
||||
{:ok, ast, comments}
|
||||
rescue
|
||||
error -> {:error, Exception.format(:error, error)}
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_deps(ast, deps) do
|
||||
with :error <- update_install(ast, deps) do
|
||||
dep_nodes = Enum.map(deps, &dep_node/1)
|
||||
|
||||
install_node =
|
||||
{{:., [], [{:__aliases__, [], [:Mix]}, :install]}, [],
|
||||
[{:__block__, [newlines: 1], [dep_nodes]}]}
|
||||
|
||||
{:ok, prepend_node(ast, install_node)}
|
||||
end
|
||||
end
|
||||
|
||||
defp format(ast, comments) do
|
||||
ast
|
||||
|> Code.quoted_to_algebra(comments: comments)
|
||||
|> Inspect.Algebra.format(90)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp prepend_node({:__block__, meta, nodes}, node) do
|
||||
{:__block__, meta, [node | nodes]}
|
||||
end
|
||||
|
||||
defp prepend_node(ast, node) do
|
||||
{:__block__, [], [node, ast]}
|
||||
end
|
||||
|
||||
defp update_install(
|
||||
{{:., _, [{:__aliases__, _, [:Mix]}, :install]} = target, meta1,
|
||||
[{:__block__, meta2, [dep_nodes]} | args]},
|
||||
deps
|
||||
) do
|
||||
new_dep_nodes = for dep <- deps, not has_dep?(dep_nodes, dep), do: dep_node(dep)
|
||||
{:ok, {target, meta1, [{:__block__, meta2, [dep_nodes ++ new_dep_nodes]} | args]}}
|
||||
end
|
||||
|
||||
defp update_install({:__block__, meta, nodes}, deps) do
|
||||
{nodes, found} =
|
||||
Enum.map_reduce(nodes, _found = false, fn
|
||||
node, false ->
|
||||
case update_install(node, deps) do
|
||||
{:ok, node} -> {node, true}
|
||||
_ -> {node, false}
|
||||
end
|
||||
|
||||
node, true ->
|
||||
{node, true}
|
||||
end)
|
||||
|
||||
if found do
|
||||
{:ok, {:__block__, meta, nodes}}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp update_install(_node, _deps), do: :error
|
||||
|
||||
defp has_dep?(deps, dep) do
|
||||
name = elem(dep, 0)
|
||||
|
||||
Enum.any?(deps, fn
|
||||
{:__block__, _, [{{:__block__, _, [^name]}, _}]} -> true
|
||||
{:{}, _, [{:__block__, _, [^name]} | _]} -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp dep_node(dep), do: {:__block__, [], [Macro.escape(dep)]}
|
||||
end
|
|
@ -16,6 +16,27 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
|||
server_pid: pid()
|
||||
}
|
||||
|
||||
kino_dep = {:kino, github: "livebook-dev/kino"}
|
||||
vega_lite_dep = {:vega_lite, "~> 0.1.3"}
|
||||
|
||||
@extra_smart_cell_definitions [
|
||||
%{
|
||||
kind: "Elixir.Kino.SmartCell.DBConnection",
|
||||
name: "Database connection",
|
||||
requirement: %{name: "Kino", dependencies: [kino_dep]}
|
||||
},
|
||||
%{
|
||||
kind: "Elixir.Kino.SmartCell.SQL",
|
||||
name: "SQL query",
|
||||
requirement: %{name: "Kino", dependencies: [kino_dep]}
|
||||
},
|
||||
%{
|
||||
kind: "Elixir.Kino.SmartCell.ChartBuilder",
|
||||
name: "Chart builder",
|
||||
requirement: %{name: "Kino", dependencies: [kino_dep, vega_lite_dep]}
|
||||
}
|
||||
]
|
||||
|
||||
@doc """
|
||||
Starts a new Elixir node (i.e. a system process) and initializes
|
||||
it with Livebook-specific modules and processes.
|
||||
|
@ -36,9 +57,13 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
|||
Utils.temporarily_register(self(), child_node, fn ->
|
||||
argv = [parent_node]
|
||||
|
||||
init_opts = [
|
||||
runtime_server_opts: [extra_smart_cell_definitions: @extra_smart_cell_definitions]
|
||||
]
|
||||
|
||||
with {:ok, elixir_path} <- find_elixir_executable(),
|
||||
port = start_elixir_node(elixir_path, child_node, child_node_eval_string(), argv),
|
||||
{:ok, server_pid} <- parent_init_sequence(child_node, port) do
|
||||
{:ok, server_pid} <- parent_init_sequence(child_node, port, init_opts: init_opts) do
|
||||
runtime = %__MODULE__{
|
||||
node: child_node,
|
||||
server_pid: server_pid
|
||||
|
@ -121,4 +146,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
|||
def stop_smart_cell(runtime, ref) do
|
||||
ErlDist.RuntimeServer.stop_smart_cell(runtime.server_pid, ref)
|
||||
end
|
||||
|
||||
def add_dependencies(_runtime, code, dependencies) do
|
||||
Livebook.Runtime.Code.add_mix_deps(code, dependencies)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,7 +34,11 @@ defmodule Livebook.Runtime.Embedded do
|
|||
# as we already do it for the Livebook application globally
|
||||
# (see Livebook.Application.start/2).
|
||||
|
||||
server_pid = ErlDist.initialize(node(), unload_modules_on_termination: false)
|
||||
server_pid =
|
||||
ErlDist.initialize(node(),
|
||||
node_manager_opts: [unload_modules_on_termination: false]
|
||||
)
|
||||
|
||||
{:ok, %__MODULE__{node: node(), server_pid: server_pid}}
|
||||
end
|
||||
end
|
||||
|
@ -94,4 +98,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
|
|||
def stop_smart_cell(runtime, ref) do
|
||||
ErlDist.RuntimeServer.stop_smart_cell(runtime.server_pid, ref)
|
||||
end
|
||||
|
||||
def add_dependencies(_runtime, code, dependencies) do
|
||||
Livebook.Runtime.Code.add_mix_deps(code, dependencies)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,18 +46,24 @@ defmodule Livebook.Runtime.ErlDist do
|
|||
If necessary, the required modules are loaded
|
||||
into the given node and the node manager process
|
||||
is started with `node_manager_opts`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:node_manager_opts` - see `Livebook.Runtime.ErlDist.NodeManager.start/1`
|
||||
|
||||
* `:runtime_server_opts` - see `Livebook.Runtime.ErlDist.RuntimeServer.start_link/1`
|
||||
"""
|
||||
@spec initialize(node(), keyword()) :: pid()
|
||||
def initialize(node, node_manager_opts \\ []) do
|
||||
def initialize(node, opts \\ []) do
|
||||
unless modules_loaded?(node) do
|
||||
load_required_modules(node)
|
||||
end
|
||||
|
||||
unless node_manager_started?(node) do
|
||||
start_node_manager(node, node_manager_opts)
|
||||
start_node_manager(node, opts[:node_manager_opts] || [])
|
||||
end
|
||||
|
||||
start_runtime_server(node)
|
||||
start_runtime_server(node, opts[:runtime_server_opts] || [])
|
||||
end
|
||||
|
||||
defp load_required_modules(node) do
|
||||
|
@ -87,8 +93,8 @@ defmodule Livebook.Runtime.ErlDist do
|
|||
:rpc.call(node, Livebook.Runtime.ErlDist.NodeManager, :start, [opts])
|
||||
end
|
||||
|
||||
defp start_runtime_server(node) do
|
||||
Livebook.Runtime.ErlDist.NodeManager.start_runtime_server(node)
|
||||
defp start_runtime_server(node, opts) do
|
||||
Livebook.Runtime.ErlDist.NodeManager.start_runtime_server(node, opts)
|
||||
end
|
||||
|
||||
defp modules_loaded?(node) do
|
||||
|
|
|
@ -35,6 +35,10 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
* `:smart_cell_definitions_module` - the module to read smart
|
||||
cell definitions from, it needs to export a `definitions/0`
|
||||
function. Defaults to `Kino.SmartCell`
|
||||
|
||||
* `:extra_smart_cell_definitions` - a list of predefined smart
|
||||
cell definitions, that may be currently be unavailable, but
|
||||
should be reported together with their requirements
|
||||
"""
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts)
|
||||
|
@ -192,9 +196,10 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
smart_cell_supervisor: nil,
|
||||
smart_cell_gl: nil,
|
||||
smart_cells: %{},
|
||||
smart_cell_definitions: [],
|
||||
smart_cell_definitions: nil,
|
||||
smart_cell_definitions_module:
|
||||
Keyword.get(opts, :smart_cell_definitions_module, Kino.SmartCell),
|
||||
extra_smart_cell_definitions: Keyword.get(opts, :extra_smart_cell_definitions, []),
|
||||
memory_timer_ref: nil
|
||||
}}
|
||||
end
|
||||
|
@ -495,8 +500,16 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
if smart_cell_definitions == state.smart_cell_definitions do
|
||||
state
|
||||
else
|
||||
defs = Enum.map(smart_cell_definitions, &Map.take(&1, [:kind, :name]))
|
||||
send(state.owner, {:runtime_smart_cell_definitions, defs})
|
||||
available_defs =
|
||||
for definition <- smart_cell_definitions,
|
||||
do: %{kind: definition.kind, name: definition.name, requirement: nil}
|
||||
|
||||
defs = Enum.uniq_by(available_defs ++ state.extra_smart_cell_definitions, & &1.kind)
|
||||
|
||||
if defs != [] do
|
||||
send(state.owner, {:runtime_smart_cell_definitions, defs})
|
||||
end
|
||||
|
||||
%{state | smart_cell_definitions: smart_cell_definitions}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -56,7 +56,7 @@ defmodule Livebook.Runtime.MixStandalone do
|
|||
:ok <- run_mix_task("compile", project_path, output_emitter),
|
||||
eval = child_node_eval_string(),
|
||||
port = start_elixir_mix_node(elixir_path, child_node, eval, argv, project_path),
|
||||
{:ok, server_pid} <- parent_init_sequence(child_node, port, output_emitter) do
|
||||
{:ok, server_pid} <- parent_init_sequence(child_node, port, emitter: output_emitter) do
|
||||
runtime = %__MODULE__{
|
||||
node: child_node,
|
||||
server_pid: server_pid,
|
||||
|
@ -188,4 +188,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.MixStandalone do
|
|||
def stop_smart_cell(runtime, ref) do
|
||||
ErlDist.RuntimeServer.stop_smart_cell(runtime.server_pid, ref)
|
||||
end
|
||||
|
||||
def add_dependencies(_runtime, code, dependencies) do
|
||||
Livebook.Runtime.Code.add_mix_deps(code, dependencies)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -80,18 +80,25 @@ defmodule Livebook.Runtime.StandaloneInit do
|
|||
Performs the parent side of the initialization contract.
|
||||
|
||||
Should be called by the initializing process on the parent node.
|
||||
|
||||
## Options
|
||||
|
||||
* `:emitter` - an emitter through which all child outpt is passed
|
||||
|
||||
* `:init_opts` - see `Livebook.Runtime.ErlDist.initialize/2`
|
||||
"""
|
||||
@spec parent_init_sequence(node(), port(), Emitter.t() | nil) ::
|
||||
{:ok, pid()} | {:error, String.t()}
|
||||
def parent_init_sequence(child_node, port, emitter \\ nil) do
|
||||
@spec parent_init_sequence(node(), port(), keyword()) :: {:ok, pid()} | {:error, String.t()}
|
||||
def parent_init_sequence(child_node, port, opts \\ []) do
|
||||
port_ref = Port.monitor(port)
|
||||
|
||||
emitter = opts[:emitter]
|
||||
|
||||
loop = fn loop ->
|
||||
receive do
|
||||
{:node_started, init_ref, ^child_node, primary_pid} ->
|
||||
Port.demonitor(port_ref)
|
||||
|
||||
server_pid = Livebook.Runtime.ErlDist.initialize(child_node)
|
||||
server_pid = Livebook.Runtime.ErlDist.initialize(child_node, opts[:init_opts] || [])
|
||||
|
||||
send(primary_pid, {:node_initialized, init_ref})
|
||||
|
||||
|
|
|
@ -310,6 +310,14 @@ defmodule Livebook.Session do
|
|||
GenServer.cast(pid, {:convert_smart_cell, self(), cell_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends smart cell dependency addition request to the server.
|
||||
"""
|
||||
@spec add_smart_cell_dependencies(pid(), String.t()) :: :ok
|
||||
def add_smart_cell_dependencies(pid, kind) do
|
||||
GenServer.cast(pid, {:add_smart_cell_dependencies, self(), kind})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends cell evaluation request to the server.
|
||||
"""
|
||||
|
@ -728,6 +736,16 @@ defmodule Livebook.Session do
|
|||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:add_smart_cell_dependencies, _client_pid, kind}, state) do
|
||||
state =
|
||||
case Enum.find(state.data.smart_cell_definitions, &(&1.kind == kind)) do
|
||||
%{requirement: %{dependencies: dependencies}} -> add_dependencies(state, dependencies)
|
||||
_ -> state
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:queue_cell_evaluation, client_pid, cell_id}, state) do
|
||||
operation = {:queue_cells_evaluation, client_pid, [cell_id]}
|
||||
{:noreply, handle_operation(state, operation)}
|
||||
|
@ -1088,6 +1106,35 @@ defmodule Livebook.Session do
|
|||
%{state | runtime_monitor_ref: runtime_monitor_ref}
|
||||
end
|
||||
|
||||
defp add_dependencies(%{data: %{runtime: nil}} = state, _dependencies), do: state
|
||||
|
||||
defp add_dependencies(state, dependencies) do
|
||||
{:ok, cell, _} = Notebook.fetch_cell_and_section(state.data.notebook, Cell.setup_cell_id())
|
||||
source = cell.source
|
||||
|
||||
case Runtime.add_dependencies(state.data.runtime, source, dependencies) do
|
||||
{:ok, ^source} ->
|
||||
state
|
||||
|
||||
{:ok, new_source} ->
|
||||
delta = Livebook.JSInterop.diff(cell.source, new_source)
|
||||
revision = state.data.cell_infos[cell.id].sources.primary.revision + 1
|
||||
|
||||
handle_operation(
|
||||
state,
|
||||
{:apply_cell_delta, self(), cell.id, :primary, delta, revision}
|
||||
)
|
||||
|
||||
{:error, message} ->
|
||||
broadcast_error(
|
||||
state.session_id,
|
||||
"failed to add dependencies to the setup cell, reason:\n\n#{message}"
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
# Given any operation on `Livebook.Session.Data`, the process
|
||||
# does the following:
|
||||
#
|
||||
|
|
|
@ -641,7 +641,13 @@ defmodule Livebook.Session.Data do
|
|||
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
||||
source_info <- data.cell_infos[cell_id].sources[tag],
|
||||
true <- 0 < revision and revision <= source_info.revision + 1,
|
||||
true <- Map.has_key?(data.clients_map, client_pid) do
|
||||
# We either need to know the client, so that we can transform
|
||||
# the delta, or the delta must apply to the latest revision,
|
||||
# in which case no transformation is necessary. The latter is
|
||||
# useful when we want to apply changes programatically
|
||||
true <-
|
||||
Map.has_key?(data.clients_map, client_pid) or
|
||||
revision == source_info.revision + 1 do
|
||||
data
|
||||
|> with_actions()
|
||||
|> apply_delta(client_pid, cell, tag, delta, revision)
|
||||
|
@ -1375,12 +1381,16 @@ defmodule Livebook.Session.Data do
|
|||
|> Map.update!(:deltas, &(&1 ++ [transformed_new_delta]))
|
||||
|> Map.update!(:revision, &(&1 + 1))
|
||||
|
||||
# Before receiving acknowledgement, the client receives all
|
||||
# the other deltas, so we can assume they are in sync with
|
||||
# the server and have the same revision.
|
||||
source_info =
|
||||
put_in(source_info.revision_by_client_pid[client_pid], source_info.revision)
|
||||
|> purge_deltas()
|
||||
if Map.has_key?(source_info.revision_by_client_pid, client_pid) do
|
||||
# Before receiving acknowledgement, the client receives all
|
||||
# the other deltas, so we can assume they are in sync with
|
||||
# the server and have the same revision.
|
||||
put_in(source_info.revision_by_client_pid[client_pid], source_info.revision)
|
||||
|> purge_deltas()
|
||||
else
|
||||
source_info
|
||||
end
|
||||
|
||||
updated_cell =
|
||||
update_in(cell, source_access(cell, tag), fn
|
||||
|
@ -1446,7 +1456,12 @@ defmodule Livebook.Session.Data do
|
|||
defp maybe_start_smart_cells({data, _} = data_actions) do
|
||||
if data.runtime do
|
||||
dead_cells = dead_smart_cells_with_section(data)
|
||||
kinds = Enum.map(data.smart_cell_definitions, & &1.kind)
|
||||
|
||||
kinds =
|
||||
for definition <- data.smart_cell_definitions,
|
||||
definition.requirement == nil,
|
||||
do: definition.kind
|
||||
|
||||
cells_ready_to_start = Enum.filter(dead_cells, fn {cell, _} -> cell.kind in kinds end)
|
||||
|
||||
reduce(data_actions, cells_ready_to_start, fn data_actions, {cell, section} ->
|
||||
|
|
|
@ -99,16 +99,19 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
Don't show this message again
|
||||
</span>
|
||||
</label>
|
||||
<div class="mt-8 flex justify-end space-x-2">
|
||||
<button class="button-base button-red"
|
||||
phx-click={hide_modal(@id) |> JS.dispatch("lb:confirm", to: "##{@id}")}>
|
||||
<i aria-hidden="true" data-confirm-icon></i>
|
||||
<span data-confirm-text></span>
|
||||
</button>
|
||||
<button class="button-base button-outlined-gray"
|
||||
phx-click={hide_modal(@id)}>
|
||||
Cancel
|
||||
</button>
|
||||
<div class="mt-8 flex justify-end">
|
||||
<div class="flex space-x-2" data-actions>
|
||||
<button class="button-base button-outlined-gray"
|
||||
phx-click={hide_modal(@id)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="button-base"
|
||||
phx-click={hide_modal(@id) |> JS.dispatch("lb:confirm", to: "##{@id}")}
|
||||
data-confirm-button>
|
||||
<i aria-hidden="true" data-confirm-icon></i>
|
||||
<span data-confirm-text></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.modal>
|
||||
|
@ -130,6 +133,8 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
|
||||
* `:confirm_icon` - icon in the confirm button. Optional
|
||||
|
||||
* `:danger` - whether the action is destructive or regular. Defaults to `true`
|
||||
|
||||
* `:opt_out_id` - enables the "Don't show this message again"
|
||||
checkbox. Once checked by the user, the confirmation with this
|
||||
id is never shown again. Optional
|
||||
|
@ -154,7 +159,14 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
opts =
|
||||
Keyword.validate!(
|
||||
opts,
|
||||
[:confirm_icon, :description, :opt_out_id, title: "Are you sure?", confirm_text: "Yes"]
|
||||
[
|
||||
:confirm_icon,
|
||||
:description,
|
||||
:opt_out_id,
|
||||
title: "Are you sure?",
|
||||
confirm_text: "Yes",
|
||||
danger: true
|
||||
]
|
||||
)
|
||||
|
||||
JS.dispatch(js, "lb:confirm_request",
|
||||
|
@ -164,6 +176,7 @@ defmodule LivebookWeb.LiveHelpers do
|
|||
description: Keyword.fetch!(opts, :description),
|
||||
confirm_text: opts[:confirm_text],
|
||||
confirm_icon: opts[:confirm_icon],
|
||||
danger: opts[:danger],
|
||||
opt_out_id: opts[:opt_out_id]
|
||||
}
|
||||
)
|
||||
|
|
|
@ -435,7 +435,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
<.labeled_text label="Type" text={runtime_type_label(@empty_default_runtime)} />
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button class="button-base button-blue" phx-click="connect_default_runtime">
|
||||
<button class="button-base button-blue" phx-click="start_default_runtime">
|
||||
<.remix_icon icon="wireless-charging-line" class="align-middle mr-1" />
|
||||
<span>Connect</span>
|
||||
</button>
|
||||
|
@ -721,19 +721,33 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("add_smart_cell_dependencies", %{"kind" => kind}, socket) do
|
||||
Session.add_smart_cell_dependencies(socket.assigns.session.pid, kind)
|
||||
|
||||
{status, socket} = maybe_restart_runtime(socket)
|
||||
|
||||
if status == :ok do
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||
data = socket.private.data
|
||||
|
||||
socket =
|
||||
{status, socket} =
|
||||
with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
||||
true <- Cell.setup?(cell),
|
||||
false <- data.cell_infos[cell.id].eval.validity == :fresh do
|
||||
maybe_restart_runtime(socket)
|
||||
else
|
||||
_ -> socket
|
||||
_ -> {:ok, socket}
|
||||
end
|
||||
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, cell_id)
|
||||
if status == :ok do
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, cell_id)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
@ -779,21 +793,21 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
def handle_event("restart_runtime", %{}, socket) do
|
||||
{:noreply, maybe_restart_runtime(socket)}
|
||||
{_, socket} = maybe_restart_runtime(socket)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("connect_default_runtime", %{}, socket) do
|
||||
{runtime_module, args} = Livebook.Config.default_runtime()
|
||||
def handle_event("start_default_runtime", %{}, socket) do
|
||||
{_, socket} = start_default_runtime(socket)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
socket =
|
||||
case apply(runtime_module, :init, args) do
|
||||
{:ok, runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
socket
|
||||
def handle_event("setup_default_runtime", %{}, socket) do
|
||||
{status, socket} = start_default_runtime(socket)
|
||||
|
||||
{:error, message} ->
|
||||
put_flash(socket, :error, "Failed to setup runtime - #{message}")
|
||||
end
|
||||
if status == :ok do
|
||||
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
@ -1316,16 +1330,29 @@ defmodule LivebookWeb.SessionLive do
|
|||
defp autofocus_cell_id(%Notebook{sections: [%{cells: [%{id: id, source: ""}]}]}), do: id
|
||||
defp autofocus_cell_id(_notebook), do: nil
|
||||
|
||||
defp maybe_restart_runtime(%{private: %{data: %{runtime: nil}}} = socket), do: socket
|
||||
defp start_default_runtime(socket) do
|
||||
{runtime_module, args} = Livebook.Config.default_runtime()
|
||||
|
||||
case apply(runtime_module, :init, args) do
|
||||
{:ok, runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session.pid, runtime)
|
||||
{:ok, socket}
|
||||
|
||||
{:error, message} ->
|
||||
{:error, put_flash(socket, :error, "Failed to start runtime - #{message}")}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_restart_runtime(%{private: %{data: %{runtime: nil}}} = socket), do: {:ok, socket}
|
||||
|
||||
defp maybe_restart_runtime(%{private: %{data: data}} = socket) do
|
||||
case Runtime.duplicate(data.runtime) do
|
||||
{:ok, new_runtime} ->
|
||||
Session.connect_runtime(socket.assigns.session.pid, new_runtime)
|
||||
clear_flash(socket, :error)
|
||||
{:ok, clear_flash(socket, :error)}
|
||||
|
||||
{:error, message} ->
|
||||
put_flash(socket, :error, "Failed to setup runtime - #{message}")
|
||||
{:error, put_flash(socket, :error, "Failed to start runtime - #{message}")}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -38,28 +38,76 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
|||
</button>
|
||||
</:content>
|
||||
</.menu>
|
||||
<%= if @smart_cell_definitions != [] do %>
|
||||
<.menu id={"#{@id}-smart-menu"} position="left">
|
||||
<:toggle>
|
||||
<button class="button-base button-small">+ Smart</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= for smart_cell_definition <- Enum.sort_by(@smart_cell_definitions, & &1.name) do %>
|
||||
<button class="menu-item text-gray-500"
|
||||
role="menuitem"
|
||||
phx-click="insert_cell_below"
|
||||
phx-value-type="smart"
|
||||
phx-value-kind={smart_cell_definition.kind}
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-cell_id={@cell_id}>
|
||||
<span class="font-medium"><%= smart_cell_definition.name %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
</:content>
|
||||
</.menu>
|
||||
<%= cond do %>
|
||||
<% @runtime == nil -> %>
|
||||
<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 to start a runtime.
|
||||
Do you want to start and setup the default one?
|
||||
''',
|
||||
confirm_text: "Setup runtime",
|
||||
confirm_icon: "play-line",
|
||||
danger: false
|
||||
)
|
||||
}>+ Smart</button>
|
||||
|
||||
<% @smart_cell_definitions == [] -> %>
|
||||
<span class="tooltip right" data-tooltip="No smart cells available">
|
||||
<button class="button-base button-small" disabled>+ Smart</button>
|
||||
</span>
|
||||
|
||||
<% true -> %>
|
||||
<.menu id={"#{@id}-smart-menu"} position="left">
|
||||
<:toggle>
|
||||
<button class="button-base button-small">+ Smart</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= for definition <- Enum.sort_by(@smart_cell_definitions, & &1.name) do %>
|
||||
<button class="menu-item text-gray-500"
|
||||
role="menuitem"
|
||||
phx-click={on_smart_cell_click(definition, @section_id, @cell_id)}>
|
||||
<span class="font-medium"><%= definition.name %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
</:content>
|
||||
</.menu>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp on_smart_cell_click(%{requirement: nil} = definition, section_id, cell_id) do
|
||||
insert_smart_cell(definition, section_id, cell_id)
|
||||
end
|
||||
|
||||
defp on_smart_cell_click(%{requirement: %{}} = definition, section_id, cell_id) do
|
||||
with_confirm(
|
||||
JS.push("add_smart_cell_dependencies", value: %{kind: definition.kind})
|
||||
|> insert_smart_cell(definition, section_id, cell_id),
|
||||
title: "Add package",
|
||||
description: ~s'''
|
||||
The “#{definition.name}“ smart cell requires #{definition.requirement.name}.
|
||||
Do you want to add it as a dependency and restart the runtime?
|
||||
''',
|
||||
confirm_text: "Add and restart",
|
||||
confirm_icon: "add-line",
|
||||
danger: false
|
||||
)
|
||||
end
|
||||
|
||||
defp insert_smart_cell(js \\ %JS{}, definition, section_id, cell_id) do
|
||||
JS.push(js, "insert_cell_below",
|
||||
value: %{
|
||||
type: "smart",
|
||||
kind: definition.kind,
|
||||
section_id: section_id,
|
||||
cell_id: cell_id
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -102,6 +102,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
id={"insert-buttons-#{@section_view.id}-first"}
|
||||
persistent={@section_view.cell_views == []}
|
||||
smart_cell_definitions={@smart_cell_definitions}
|
||||
runtime={@runtime}
|
||||
section_id={@section_view.id}
|
||||
cell_id={nil} />
|
||||
<%= for {cell_view, index} <- Enum.with_index(@section_view.cell_views) do %>
|
||||
|
@ -114,6 +115,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
id={"insert-buttons-#{@section_view.id}-#{index}"}
|
||||
persistent={false}
|
||||
smart_cell_definitions={@smart_cell_definitions}
|
||||
runtime={@runtime}
|
||||
section_id={@section_view.id}
|
||||
cell_id={cell_view.id} />
|
||||
<% end %>
|
||||
|
|
164
test/livebook/runtime/code_test.exs
Normal file
164
test/livebook/runtime/code_test.exs
Normal file
|
@ -0,0 +1,164 @@
|
|||
defmodule Livebook.Runtime.CodeTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Livebook.Runtime
|
||||
|
||||
@kino {:kino, "~> 0.5.0"}
|
||||
|
||||
describe "add_mix_deps/2" do
|
||||
test "prepends Mix.install/2 call if there is none" do
|
||||
assert Runtime.Code.add_mix_deps("", [@kino]) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
])\
|
||||
"""}
|
||||
|
||||
assert Runtime.Code.add_mix_deps("# Comment", [@kino]) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
])
|
||||
|
||||
# Comment\
|
||||
"""}
|
||||
|
||||
assert Runtime.Code.add_mix_deps(
|
||||
"""
|
||||
# Outer comment
|
||||
for key <- [:key1, :key2] do
|
||||
# Inner comment
|
||||
Application.put_env(:app, key, :value)
|
||||
end
|
||||
|
||||
# Final comment\
|
||||
""",
|
||||
[@kino]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
])
|
||||
|
||||
# Outer comment
|
||||
for key <- [:key1, :key2] do
|
||||
# Inner comment
|
||||
Application.put_env(:app, key, :value)
|
||||
end
|
||||
|
||||
# Final comment\
|
||||
"""}
|
||||
end
|
||||
|
||||
test "appends dependency to an existing Mix.install/2 call" do
|
||||
assert Runtime.Code.add_mix_deps(
|
||||
"""
|
||||
Mix.install([
|
||||
{:req, "~> 0.2.0"}
|
||||
])\
|
||||
""",
|
||||
[@kino]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:req, "~> 0.2.0"},
|
||||
{:kino, "~> 0.5.0"}
|
||||
])\
|
||||
"""}
|
||||
|
||||
assert Runtime.Code.add_mix_deps(
|
||||
"""
|
||||
# Outer comment
|
||||
Mix.install([
|
||||
# Inner comment leading
|
||||
{:req, "~> 0.2.0"}
|
||||
# Inner comment trailing
|
||||
])
|
||||
|
||||
# Result
|
||||
:ok\
|
||||
""",
|
||||
[@kino]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
# Outer comment
|
||||
Mix.install([
|
||||
# Inner comment leading
|
||||
{:req, "~> 0.2.0"},
|
||||
{:kino, "~> 0.5.0"}
|
||||
# Inner comment trailing
|
||||
])
|
||||
|
||||
# Result
|
||||
:ok\
|
||||
"""}
|
||||
end
|
||||
|
||||
test "does not add the dependency if it already exists" do
|
||||
code = """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2"}
|
||||
])\
|
||||
"""
|
||||
|
||||
assert Runtime.Code.add_mix_deps(code, [@kino]) == {:ok, code}
|
||||
|
||||
code = """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2", runtime: false}
|
||||
])\
|
||||
"""
|
||||
|
||||
assert Runtime.Code.add_mix_deps(code, [@kino]) == {:ok, code}
|
||||
end
|
||||
|
||||
test "given multiple dependencies adds the missing ones" do
|
||||
assert Runtime.Code.add_mix_deps(
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2"}
|
||||
])\
|
||||
""",
|
||||
[{:vega_lite, "~> 0.1.3"}, {:kino, "~> 0.5.0"}, {:req, "~> 0.2.0"}]
|
||||
) ==
|
||||
{:ok,
|
||||
"""
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2"},
|
||||
{:vega_lite, "~> 0.1.3"},
|
||||
{:req, "~> 0.2.0"}
|
||||
])\
|
||||
"""}
|
||||
|
||||
code = """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.2", runtime: false}
|
||||
])\
|
||||
"""
|
||||
|
||||
assert Runtime.Code.add_mix_deps(code, [@kino]) == {:ok, code}
|
||||
end
|
||||
|
||||
test "returns an error if the code has a syntax error" do
|
||||
assert Runtime.Code.add_mix_deps(
|
||||
"""
|
||||
# Comment
|
||||
[,1]
|
||||
""",
|
||||
[@kino]
|
||||
) ==
|
||||
{:error,
|
||||
"""
|
||||
** (SyntaxError) nofile:2:2: syntax error before: ','
|
||||
|
|
||||
2 | [,1]
|
||||
| ^\
|
||||
"""}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ defmodule Livebook.Session.DataTest do
|
|||
|
||||
@eval_resp {:ok, [1, 2, 3]}
|
||||
@eval_meta %{evaluation_time_ms: 10}
|
||||
@smart_cell_definitions [%{kind: "text", name: "Text", requirement: nil}]
|
||||
|
||||
describe "new/1" do
|
||||
test "called with no arguments defaults to a blank notebook" do
|
||||
|
@ -440,7 +441,7 @@ defmodule Livebook.Session.DataTest do
|
|||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]}
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions}
|
||||
])
|
||||
|
||||
operation = {:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}}
|
||||
|
@ -457,7 +458,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil}
|
||||
])
|
||||
|
||||
|
@ -796,7 +797,7 @@ defmodule Livebook.Session.DataTest do
|
|||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
||||
{:smart_cell_started, self(), "c1", Delta.new(), %{}, nil}
|
||||
])
|
||||
|
@ -815,7 +816,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
evaluate_cells_operations(["setup"]),
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil},
|
||||
{:queue_cells_evaluation, self(), ["c1"]},
|
||||
{:add_cell_evaluation_response, self(), "c1", @eval_resp, @eval_meta}
|
||||
|
@ -907,7 +908,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
||||
{:delete_cell, self(), "c1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]}
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions}
|
||||
])
|
||||
|
||||
operation = {:restore_cell, self(), "c1"}
|
||||
|
@ -2324,7 +2325,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_cell, self(), "s1", 1, :smart, "c2", %{kind: "text"}},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
evaluate_cells_operations(["setup"]),
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:smart_cell_started, self(), "c2", Delta.new(), %{}, nil},
|
||||
{:queue_cells_evaluation, self(), ["c1"]}
|
||||
])
|
||||
|
@ -2646,7 +2647,7 @@ defmodule Livebook.Session.DataTest do
|
|||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}}
|
||||
])
|
||||
|
||||
|
@ -2665,7 +2666,7 @@ defmodule Livebook.Session.DataTest do
|
|||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}}
|
||||
])
|
||||
|
||||
|
@ -2692,7 +2693,7 @@ defmodule Livebook.Session.DataTest do
|
|||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
||||
{:smart_cell_started, self(), "c1", delta1, %{}, nil}
|
||||
])
|
||||
|
@ -2719,7 +2720,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_section, self(), 0, "s1"},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
evaluate_cells_operations(["setup"]),
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:insert_cell, self(), "s1", 0, :smart, "c1", %{kind: "text"}},
|
||||
{:smart_cell_started, self(), "c1", Delta.new(), %{}, nil},
|
||||
{:queue_cells_evaluation, self(), ["c1"]},
|
||||
|
@ -2992,18 +2993,6 @@ defmodule Livebook.Session.DataTest do
|
|||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "returns an error given non-joined client pid" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}}
|
||||
])
|
||||
|
||||
delta = Delta.new() |> Delta.insert("cats")
|
||||
operation = {:apply_cell_delta, self(), "c1", :primary, delta, 1}
|
||||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "returns an error given invalid revision" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
|
@ -3018,10 +3007,27 @@ defmodule Livebook.Session.DataTest do
|
|||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "returns an error given non-joined client pid and older revision" do
|
||||
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
||||
|
||||
delta1 = Delta.new() |> Delta.insert("cats")
|
||||
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:client_join, client1_pid, User.new()},
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}},
|
||||
{:apply_cell_delta, client1_pid, "c1", :primary, delta1, 1}
|
||||
])
|
||||
|
||||
delta = Delta.new() |> Delta.insert("cats")
|
||||
operation = {:apply_cell_delta, self(), "c1", :primary, delta, 1}
|
||||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "updates cell source according to the given delta" do
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:client_join, self(), User.new()},
|
||||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 0, :code, "c1", %{}}
|
||||
])
|
||||
|
@ -3141,7 +3147,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:insert_section, self(), 0, "s1"},
|
||||
{:insert_cell, self(), "s1", 1, :smart, "c1", %{kind: "text"}},
|
||||
{:set_runtime, self(), NoopRuntime.new()},
|
||||
{:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]},
|
||||
{:set_smart_cell_definitions, self(), @smart_cell_definitions},
|
||||
{:smart_cell_started, self(), "c1", Delta.new(), %{},
|
||||
%{language: "text", placement: :bottom, source: ""}}
|
||||
])
|
||||
|
@ -3436,7 +3442,7 @@ defmodule Livebook.Session.DataTest do
|
|||
{:set_runtime, self(), NoopRuntime.new()}
|
||||
])
|
||||
|
||||
operation = {:set_smart_cell_definitions, self(), [%{kind: "text", name: "Text"}]}
|
||||
operation = {:set_smart_cell_definitions, self(), @smart_cell_definitions}
|
||||
|
||||
assert {:ok, %{cell_infos: %{"c1" => %{status: :starting}}}, _actions} =
|
||||
Data.apply_operation(data, operation)
|
||||
|
|
|
@ -134,6 +134,75 @@ defmodule Livebook.SessionTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "add_smart_cell_dependencies/2" do
|
||||
test "applies source change to the setup cell to include the smart cell dependency",
|
||||
%{session: session} do
|
||||
{:ok, runtime} = Livebook.Runtime.Embedded.init()
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
|
||||
send(
|
||||
session.pid,
|
||||
{:runtime_smart_cell_definitions,
|
||||
[
|
||||
%{
|
||||
kind: "text",
|
||||
name: "Text",
|
||||
requirement: %{name: "Kino", dependencies: [{:kino, "~> 0.5.0"}]}
|
||||
}
|
||||
]}
|
||||
)
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
Session.add_smart_cell_dependencies(session.pid, "text")
|
||||
|
||||
session_pid = session.pid
|
||||
assert_receive {:operation, {:apply_cell_delta, ^session_pid, "setup", :primary, _delta, 1}}
|
||||
|
||||
assert %{
|
||||
notebook: %{
|
||||
setup_section: %{
|
||||
cells: [
|
||||
%{
|
||||
source: """
|
||||
Mix.install([
|
||||
{:kino, "~> 0.5.0"}
|
||||
])\
|
||||
"""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} = Session.get_data(session.pid)
|
||||
end
|
||||
|
||||
test "broadcasts an error if modifying the setup source fails" do
|
||||
notebook = Notebook.new() |> Notebook.update_cell("setup", &%{&1 | source: "[,]"})
|
||||
session = start_session(notebook: notebook)
|
||||
|
||||
{:ok, runtime} = Livebook.Runtime.Embedded.init()
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
|
||||
send(
|
||||
session.pid,
|
||||
{:runtime_smart_cell_definitions,
|
||||
[
|
||||
%{
|
||||
kind: "text",
|
||||
name: "Text",
|
||||
requirement: %{name: "Kino", dependencies: [{:kino, "~> 0.5.0"}]}
|
||||
}
|
||||
]}
|
||||
)
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
Session.add_smart_cell_dependencies(session.pid, "text")
|
||||
|
||||
assert_receive {:error, "failed to add dependencies to the setup cell, reason:" <> _}
|
||||
end
|
||||
end
|
||||
|
||||
describe "queue_cell_evaluation/2" do
|
||||
test "triggers evaluation and sends update operation once it finishes",
|
||||
%{session: session} do
|
||||
|
@ -577,7 +646,10 @@ defmodule Livebook.SessionTest do
|
|||
runtime = Livebook.Runtime.NoopRuntime.new()
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
|
||||
send(session.pid, {:runtime_smart_cell_definitions, [%{kind: "text", name: "Text"}]})
|
||||
send(
|
||||
session.pid,
|
||||
{:runtime_smart_cell_definitions, [%{kind: "text", name: "Text", requirement: nil}]}
|
||||
)
|
||||
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}")
|
||||
|
||||
|
@ -601,7 +673,10 @@ defmodule Livebook.SessionTest do
|
|||
runtime = Livebook.Runtime.NoopRuntime.new()
|
||||
Session.connect_runtime(session.pid, runtime)
|
||||
|
||||
send(session.pid, {:runtime_smart_cell_definitions, [%{kind: "text", name: "Text"}]})
|
||||
send(
|
||||
session.pid,
|
||||
{:runtime_smart_cell_definitions, [%{kind: "text", name: "Text", requirement: nil}]}
|
||||
)
|
||||
|
||||
server_pid = self()
|
||||
|
||||
|
|
|
@ -399,7 +399,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
send(
|
||||
session.pid,
|
||||
{:runtime_smart_cell_definitions, [%{kind: "dbconn", name: "Database connection"}]}
|
||||
{:runtime_smart_cell_definitions,
|
||||
[%{kind: "dbconn", name: "Database connection", requirement: nil}]}
|
||||
)
|
||||
|
||||
wait_for_session_update(session.pid)
|
||||
|
|
|
@ -28,5 +28,6 @@ defmodule Livebook.Runtime.NoopRuntime do
|
|||
def start_smart_cell(_, _, _, _, _), do: :ok
|
||||
def set_smart_cell_base_locator(_, _, _), do: :ok
|
||||
def stop_smart_cell(_, _), do: :ok
|
||||
def add_dependencies(_, code, _), do: {:ok, code}
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue