mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-17 14:19:53 +08:00
Add Form to "+ Block" (#1750)
Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
parent
6061901ee9
commit
6dd19a8dc9
17 changed files with 536 additions and 267 deletions
|
|
@ -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
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
192
lib/livebook/runtime/definitions.ex
Normal file
192
lib/livebook/runtime/definitions.ex
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 """
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue