Skip to content

Commit

Permalink
Add API for setting smart cell editor source and intellisense node (#390
Browse files Browse the repository at this point in the history
)
  • Loading branch information
jonatanklosko authored Jan 31, 2024
1 parent f8802d0 commit 9f5d073
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 76 deletions.
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

0 comments on commit 9f5d073

Please sign in to comment.