Skip to content

Instantly share code, notes, and snippets.

@noizu
Created March 18, 2024 01:07
Show Gist options
  • Save noizu/41629ab2631d6aa1362c98fa49b85fe0 to your computer and use it in GitHub Desktop.
Save noizu/41629ab2631d6aa1362c98fa49b85fe0 to your computer and use it in GitHub Desktop.
smart-cell with secret value input pop up.

SmartCell Secret Input Modal Example

Mix.install([
  {:kino, "~> 0.12.0"}
])

Setup Frame

Quick hack to expose frame to smart cell demo.

frame = Kino.Frame.new()
:persistent_term.put({:demo, :frame}, frame)
:persistent_term.put({:demo, :log}, true)

Break Down

Now here is a quick break down of our of the steps needed to hook up to the LiveBook secret and secret selector modal pop up in our own smart cell provider.

First: we add a secret_key field to our context

  @impl true
  def init(attrs, ctx) do    
    ctx =
      assign(ctx,
        secret_key: attrs["secret_key"]
      )
    {:ok, ctx, []}
  end

Second: Event Handler for secret update

We listen for incoming updates to this key and push back a field updated message to inform the frontend of success.

  @impl true
  def handle_event("set_secret_key" = event, variable, ctx) do
    ctx = assign(ctx, secret_key: variable["value"])
    broadcast_event(ctx, "update_secret_key", ctx.assigns.secret_key)
    {:noreply, ctx}
  end

Third: Evaluate output code gen

Once our smart cell is evaluated we fetch and return the provided key from livebooks secret storage system or return a not set tuple. You could alternatively in this section check if all required inputs are set and return a blank string response wehn unavailable as the Kino DB connection_cell does.

  @impl true
  def to_source(attrs) do
    secret = attrs["secret_key"]
    if is_bitstring(secret) and secret != "" do      
      quote do
        {:all_your_scrests_belong_to_me, System.fetch_env!(unquote("LB_#{secret}"))}
      end
    else
      quote do         
        {:oh, :you_havent_set_the_secret_yet}
      end
    end
    |> Kino.SmartCell.quoted_to_string()
  end

Front End

Finally we define a basic html form to accept the required params. We wrap the label and input for the secret input field with a on click event that invokes the javascript's ctx.selectSecret method. It pops the input modal and once selected the provided secret hanlde is pushed back to our live cell's set_secret_key event handler.

     ctx.selectSecret(
          (secretName) => {
            ctx.pushEvent("set_secret_key", {
              field: "if_you_wanted_to_impelment_a_handler_method_supporting_multiple_inputs",
              value: secretName,
            });
          },
          "YOUR_SECRET_KEY_DEFAULT",
          { title: "TELL ME YOUR SECRET" }
        );

To indicate the value has been assigned we add a listener for the handle_event update_secret_key broadcast event.

  const el = ctx.root.querySelector(`.secret-modal`);   
      const el_input = ctx.root.querySelector(`.secret-modal [name="secret"]`);   
      ctx.handleEvent("update_secret_key", (text) => {
        alert("hey")
        el_input.value = text;
      });

Logging

To assist with inspecting the internal state of our smart cell I've added jury rigged (qiocl amd dirty) persistent_term backed toggler for logs and log output statements at the top of our smart cell methods. method for passing our output frame and log on/off toggle to our smart cell. An obvious improvement here for a longer term solution would be to pass in the frame as an input and, and track the log on/flag in the per instance ctx context state. And add the frame as the output of a cell on our live book.

frame = Kino.Frame.new()
:persistent_term.put({:demo, :frame}, frame)
:persistent_term.put({:demo, :log}, false)
   content = Kino.Markdown.new("- [x] #{inspect __ENV__.function}\n\n```elixir\n#{inspect [attrs: attrs, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity}\n\n```")
    :persistent_term.get({:demo, :log}) && Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)
@impl true
def handle_event("toggle_log", _args, ctx) do
  :persistent_term.put({:demo, :log}, !:persistent_term.get({:demo, :log}))
  {:noreply, ctx}
end
defmodule TheRobotLives.AddingSecretsToKinoSmartCell do
  use Kino.JS
  use Kino.JS.Live
  use Kino.SmartCell, name: "Smart Cell Secret Selection Demo"

  @impl true
  def init(attrs, ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([attrs: attrs, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    ctx =
      assign(ctx,
        secret_key: attrs["secret_key"]
      )

    {:ok, ctx, []}
  end

  @impl true
  def handle_connect(ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([actx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    {:ok, %{secret_key: ctx.assigns.secret_key}, ctx}
  end

  @impl true
  def handle_event("set_secret_key" = event, variable, ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([event: event, variable: variable, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    ctx = assign(ctx, secret_key: variable["value"])
    broadcast_event(ctx, "update_secret_key", ctx.assigns.secret_key)
    {:noreply, ctx}
  end

  @impl true
  def handle_event("toggle_log" = event, args, ctx) do
    s = :persistent_term.get({:demo, :log})

    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)} - Set To #{inspect(!s)}\n\n```elixir\n#{inspect([event: event, args: args, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    :persistent_term.put({:demo, :log}, !s)
    {:noreply, ctx}
  end

  @impl true
  def to_attrs(ctx) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    %{"secret_key" => ctx.assigns.secret_key}
  end

  @impl true
  def to_source(attrs) do
    content =
      Kino.Markdown.new(
        "- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([attrs: attrs], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
      )

    :persistent_term.get({:demo, :log}) &&
      Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)

    secret = attrs["secret_key"]

    if is_bitstring(secret) and secret != "" do
      quote do
        {:all_your_scrests_belong_to_me, System.fetch_env!(unquote("LB_#{secret}"))}
      end
    else
      quote do
        {:oh, :you_havent_set_the_secret_yet}
      end
    end
    |> Kino.SmartCell.quoted_to_string()
  end

  asset "main.js" do
    """
    export function init(ctx, payload) {
      ctx.importCSS("main.css");
      ctx.importCSS("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap");
      root.innerHTML = `
        <div class="app">
          <div class="secret-modal">
            <label class="label">Secret</label>
            <input class="input" type="password" name="secret" />
          </div> 
          <label class="label">Enable Frame Logs</label>
          <input class="input" type="checkbox" name="log" />
        </div>
      `;
      
      const el = ctx.root.querySelector(`.secret-modal`);   
      const el_input = ctx.root.querySelector(`.secret-modal [name="secret"]`);   
      ctx.handleEvent("update_secret_key", (text) => {
        el_input.value = text;
      });
      


      el.addEventListener("click", (event) => {
        const preselectName = "DEFAULT_ENV_VAR";
        ctx.selectSecret(
          (secretName) => {
            ctx.pushEvent("set_secret_key", {
              field: "if_you_wanted_to_impelment_a_handler_method_supporting_multiple_inputs",
              value: secretName,
            });
          },
          preselectName,
          { title: "TELL ME YOUR SECRET" }
        );
      });

      const el2 = ctx.root.querySelector(`[name="log"]`);   

      el2.addEventListener("click", (event) => {
      ctx.pushEvent("toggle_log", {});
      });


      ctx.handleSync(() => {
        // Synchronously invokes change listeners
        document.activeElement &&
          document.activeElement.dispatchEvent(new Event("change"));
      });
    }
    """
  end

  asset "main.css" do
    """
    // make it pretty
    """
  end
end

Kino.SmartCell.register(TheRobotLives.AddingSecretsToKinoSmartCell)

Frame Log Output

frame
{:all_your_scrests_belong_to_me, System.fetch_env!("LB_DEFAULT_ENV_VAR2")}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment