Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API for setting smart cell editor source and intellisense node #390

Merged
merged 1 commit into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions assets/remote_execution_cell/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,55 +233,17 @@ export function init(ctx, payload) {
const field = event.target.name;
const value = this.fields[field];
ctx.pushEvent("update_field", { field, value });
field !== "assign_to" && this.updateNodeInfo();
},
updateNodeInfo() {
const node = this.fields["use_node_secret"]
? this.fields["node_secret_value"]
: this.fields["node"];
const cookie = this.fields["use_cookie_secret"]
? this.fields["cookie_secret_value"]
: this.fields["cookie"];
ctx.setSmartCellEditorIntellisenseNode(node, cookie);
},
},

mounted() {
this.updateNodeInfo();
},
}).mount(ctx.root);

ctx.handleEvent("update_field", ({ fields }) => {
setFields(fields);
});

ctx.handleEvent("update_node_info", (secret_value) => {
const node =
"node_secret" in secret_value ? secret_value.node_secret : getNode();
const cookie =
"cookie_secret" in secret_value
? secret_value.cookie_secret
: getCookie();
ctx.setSmartCellEditorIntellisenseNode(node, cookie);
});

function setFields(fields) {
for (const field in fields) {
app.fields[field] = fields[field];
}
}

function getNode() {
const node = app.fields["use_node_secret"]
? app.fields["node_secret_value"]
: app.fields["node"];
return node;
}

function getCookie() {
const cookie = app.fields["use_cookie_secret"]
? app.fields["cookie_secret_value"]
: app.fields["cookie"];
return cookie;
}
}
4 changes: 2 additions & 2 deletions lib/assets/remote_execution_cell/build/main.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions lib/kino/js/live/context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,24 @@ defmodule Kino.JS.Live.Context do
def emit_event(%__MODULE__{} = ctx, event) do
Kino.JS.Live.Server.emit_event(ctx, event)
end

@doc """
Updates smart cell configuration.

This function allows for re-configuring some of the options that can
be specified in smart cell's `c:Kino.JS.Live.init/2`.

Note that this function returns the new context, which you should
return from the given handler.

## Options

* `:editor` - note that the smart cell must be initialized with an
editor during on init. Supported options: `:source`, `:intellisense_node`.

"""
@spec reconfigure_smart_cell(t(), keyword()) :: t()
def reconfigure_smart_cell(%__MODULE__{} = ctx, opts) do
Kino.SmartCell.Server.reconfigure_smart_cell(ctx, opts)
end
end
81 changes: 49 additions & 32 deletions lib/kino/remote_execution_cell.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ defmodule Kino.RemoteExecutionCell do
@default_code ":ok"
@global_key __MODULE__
@global_attrs ["node", "cookie", "cookie_secret", "node_secret"]
@secret_attrs ["cookie_secret", "node_secret"]
@distribution_attrs [
"node",
"use_node_secret",
"node_secret",
"cookie",
"use_cookie_secret",
"cookie_secret"
]

@impl true
def init(attrs, ctx) do
Expand All @@ -20,28 +27,49 @@ defmodule Kino.RemoteExecutionCell do
{shared_node, shared_node_secret} =
AttributeStore.get_attribute({@global_key, :node}, {nil, nil})

node_secret = attrs["node_secret"] || shared_node_secret
node_secret_value = node_secret && System.get_env("LB_#{node_secret}")
cookie_secret = attrs["cookie_secret"] || shared_cookie_secret
cookie_secret_value = cookie_secret && System.get_env("LB_#{cookie_secret}")

fields = %{
"assign_to" => attrs["assign_to"] || "",
"node" => attrs["node"] || shared_node || "",
"node_secret" => node_secret || "",
"node_secret" => attrs["node_secret"] || shared_node_secret || "",
"cookie" => attrs["cookie"] || shared_cookie || "",
"cookie_secret" => cookie_secret || "",
"cookie_secret" => attrs["cookie_secret"] || shared_cookie_secret || "",
"use_node_secret" =>
if(shared_node_secret, do: true, else: Map.get(attrs, "use_node_secret", false)),
"use_cookie_secret" =>
if(shared_cookie, do: false, else: Map.get(attrs, "use_cookie_secret", true)),
"cookie_secret_value" => cookie_secret_value,
"node_secret_value" => node_secret_value
if(shared_cookie, do: false, else: Map.get(attrs, "use_cookie_secret", true))
}

intellisense_node = intellisense_node(fields)

ctx = assign(ctx, fields: fields)

{:ok, ctx, editor: [attribute: "code", language: "elixir", default_source: @default_code]}
{:ok, ctx,
editor: [
attribute: "code",
language: "elixir",
default_source: @default_code,
intellisense_node: intellisense_node
]}
end

defp intellisense_node(fields) do
node =
if fields["use_node_secret"] do
System.get_env("LB_#{fields["node_secret"]}")
else
fields["node"]
end

cookie =
if fields["use_cookie_secret"] do
System.get_env("LB_#{fields["cookie_secret"]}")
else
fields["cookie"]
end

if is_binary(node) and node =~ "@" and is_binary(cookie) and cookie != "" do
{String.to_atom(node), String.to_atom(cookie)}
end
end

@impl true
Expand All @@ -54,21 +82,24 @@ defmodule Kino.RemoteExecutionCell do
def handle_event("update_field", %{"field" => field, "value" => value}, ctx) do
ctx = update(ctx, :fields, &Map.put(&1, field, value))
if field in @global_attrs, do: put_shared_attr(field, value)
fields = update_fields(field, value)

if field in @secret_attrs,
do: send_event(ctx, ctx.origin, "update_node_info", %{field => fields["#{field}_value"]})
ctx =
if field in @distribution_attrs do
reconfigure_smart_cell(ctx,
editor: [intellisense_node: intellisense_node(ctx.assigns.fields)]
)
else
ctx
end

broadcast_event(ctx, "update_field", %{"fields" => fields})
broadcast_event(ctx, "update_field", %{"fields" => update_fields(field, value)})

{:noreply, ctx}
end

@impl true
def to_attrs(ctx) do
ctx.assigns.fields
|> Map.delete("node_secret_value")
|> Map.delete("cookie_secret_value")
end

@impl true
Expand Down Expand Up @@ -151,19 +182,5 @@ defmodule Kino.RemoteExecutionCell do
AttributeStore.put_attribute({@global_key, :node}, {nil, value})
end

defp update_fields("cookie_secret", cookie_secret) do
%{
"cookie_secret" => cookie_secret,
"cookie_secret_value" => System.get_env("LB_#{cookie_secret}")
}
end

defp update_fields("node_secret", node_secret) do
%{
"node_secret" => node_secret,
"node_secret_value" => System.get_env("LB_#{node_secret}")
}
end

defp update_fields(field, value), do: %{field => value}
end
15 changes: 15 additions & 0 deletions lib/kino/smart_cell.ex
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ defmodule Kino.SmartCell do

* `:default_source` - the initial editor source. Defaults to `""`

* `:intellisense_node` - a `{node, cookie}` atom tuple specifying
a remote node that should be introspected for editor intellisense.
This is only applicable when `:language` is Elixir. Defaults to
`nil`

## Other options

Other than the editor configuration, the following options are
Expand Down Expand Up @@ -258,6 +263,16 @@ defmodule Kino.SmartCell do

@smart_opts opts

import Kino.JS.Live.Context,
only: [
assign: 2,
update: 3,
broadcast_event: 3,
send_event: 4,
emit_event: 2,
reconfigure_smart_cell: 2
]

@before_compile Kino.SmartCell
end
end
Expand Down
58 changes: 54 additions & 4 deletions lib/kino/smart_cell/server.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Kino.SmartCell.Server do
@moduledoc false

@behaviour GenServer

require Logger

import Kino.Utils, only: [has_function?: 3]
Expand All @@ -9,7 +11,7 @@ defmodule Kino.SmartCell.Server do
@chunk_joiner_size byte_size(@chunk_joiner)

def start_link(module, ref, attrs, target_pid) do
case :proc_lib.start_link(__MODULE__, :init, [module, ref, attrs, target_pid]) do
case :proc_lib.start_link(__MODULE__, :init, [{module, ref, attrs, target_pid}]) do
{:error, error} ->
{:error, error}

Expand All @@ -21,7 +23,8 @@ defmodule Kino.SmartCell.Server do
%{
language: editor_opts[:language],
placement: editor_opts[:placement],
source: source
source: source,
intellisense_node: editor_opts[:intellisense_node]
}
end

Expand All @@ -42,7 +45,29 @@ defmodule Kino.SmartCell.Server do
end
end

def init(module, ref, initial_attrs, target_pid) do
def reconfigure_smart_cell(ctx, opts) do
unless ctx.__private__.smart_cell do
raise ArgumentError,
"configure_smart_cell/2 can only be called in smart cell handlers"
end

opts = Keyword.validate!(opts, [:editor])

if editor_opts = opts[:editor] do
unless ctx.__private__.smart_cell.editor? do
raise ArgumentError,
"configure_smart_cell/2 called with :editor, but the editor is not enabled." <>
" Make sure to enable smart cell editor during init"
end

Keyword.validate!(editor_opts, [:source, :intellisense_node])
end

put_in(ctx.__private__.smart_cell[:reconfigure_options], opts)
end

@impl true
def init({module, ref, initial_attrs, target_pid}) do
{:ok, ctx, init_opts} = Kino.JS.Live.Server.call_init(module, initial_attrs, ref)
init_opts = validate_init_opts!(init_opts)

Expand All @@ -61,6 +86,9 @@ defmodule Kino.SmartCell.Server do

{source, chunks} = to_source(module, attrs)

editor? = editor_source_attr != nil
ctx = put_in(ctx.__private__[:smart_cell], %{editor?: editor?})

:proc_lib.init_ack({:ok, self(), source, chunks, init_opts})

state = %{
Expand All @@ -83,6 +111,7 @@ defmodule Kino.SmartCell.Server do
Keyword.validate!(editor_opts, [
:attribute,
:language,
:intellisense_node,
placement: :bottom,
default_source: ""
])
Expand All @@ -100,18 +129,39 @@ defmodule Kino.SmartCell.Server do
end)
end

defp handle_reconfigure(state) do
case pop_in(state.ctx.__private__.smart_cell[:reconfigure_options]) do
{nil, state} ->
state

{options, state} ->
if editor_options = options[:editor] do
options = Map.new(editor_options)

send(
state.target_pid,
{:runtime_smart_cell_editor_update, state.ctx.__private__.ref, options}
)
end

state
end
end

@impl true
def handle_info({:editor_source, source}, state) do
attrs = Map.put(state.attrs, state.editor_source_attr, source)
{:noreply, set_attrs(state, attrs)}
end

def handle_info(msg, state) do
case Kino.JS.Live.Server.call_handle_info(msg, state.module, state.ctx) do
{:ok, ctx} -> {:noreply, recompute_attrs(%{state | ctx: ctx})}
{:ok, ctx} -> {:noreply, %{state | ctx: ctx} |> handle_reconfigure() |> recompute_attrs()}
:error -> {:noreply, state}
end
end

@impl true
def terminate(reason, state) do
Kino.JS.Live.Server.call_terminate(reason, state.module, state.ctx)
end
Expand Down
Loading