defmodule LivebookWeb.SessionLive.SecretsComponent do use LivebookWeb, :live_component @impl true def update(assigns, socket) do socket = assign(socket, assigns) prefill_form = prefill_secret_name(socket) socket = if socket.assigns[:data] do socket else assign(socket, data: %{"name" => prefill_form, "value" => "", "store" => "session"}, errors: [{"value", {"can't be blank", []}}], title: title(socket), grant_access: must_grant_access(socket), has_prefill: prefill_form != "" ) end {:ok, socket} end @impl true def render(assigns) do ~H"""

<%= @title %>

<%= if @grant_access do %> <.grant_access_message grant_access={@grant_access} target={@myself} /> <% end %>
<%= if @select_secret_ref do %>

Choose a secret

<%= for {secret_name, _} <- Enum.sort(@secrets) do %> <.secret_with_badge secret_name={secret_name} stored="Session" action="select_secret" active={secret_name == @prefill_secret_name} target={@myself} /> <% end %> <%= for {secret_name, _} <- livebook_only_secrets(@secrets, @livebook_secrets) do %> <.secret_with_badge secret_name={secret_name} stored="Livebook" action="select_livebook_secret" active={false} target={@myself} /> <% end %> <%= if @secrets == %{} and @livebook_secrets == %{} do %>
<.remix_icon icon="folder-lock-line" class="align-middle text-2xl" /> Secrets not found.
Add to see them here.
<% end %>
<% end %> <.form :let={f} for={:data} phx-submit="save" phx-change="validate" autocomplete="off" phx-target={@myself} errors={@errors} class="basis-1/2 grow" >
<%= if @select_secret_ref do %>

Add new secret

<% end %> <.input_wrapper form={f} field={:name}>
Name (alphanumeric and underscore)
<%= text_input(f, :name, value: @data["name"], class: "input", autofocus: !@has_prefill, spellcheck: "false" ) %> <.input_wrapper form={f} field={:value}>
Value
<%= text_input(f, :value, value: @data["value"], class: "input", autofocus: @has_prefill, spellcheck: "false" ) %>
Storage
<%= label class: "flex items-center gap-2 text-gray-600" do %> <%= radio_button(f, :store, "session", checked: @data["store"] == "session") %> only this session <% end %> <%= label class: "flex items-center gap-2 text-gray-600" do %> <%= radio_button(f, :store, "livebook", checked: @data["store"] == "livebook") %> in the Livebook app <% end %>
<%= live_patch("Cancel", to: @return_to, class: "button-base button-outlined-gray") %>
""" end defp secret_with_badge(assigns) do ~H"""
<%= @secret_name %> <%= if @active do %> <% end %> <%= @stored %>
""" end defp grant_access_message(assigns) do ~H"""
<.remix_icon icon="error-warning-fill" class="align-middle text-2xl flex text-gray-100 rounded-lg py-2" /> There is a secret named <%= @grant_access %> in your Livebook app. Allow this session to access it?
""" end @impl true def handle_event("save", %{"data" => data}, socket) do assigns = socket.assigns case Livebook.Secrets.validate_secret(data) do {:ok, secret} -> store = data["store"] set_secret(assigns.session.pid, secret, store) {:noreply, socket |> push_patch(to: assigns.return_to) |> push_secret_selected(secret.name)} {:error, changeset} -> {:noreply, assign(socket, errors: changeset.errors)} end end def handle_event("select_secret", %{"secret_name" => secret_name}, socket) do {:noreply, socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret_name)} end def handle_event("select_livebook_secret", %{"secret_name" => secret_name}, socket) do grant_access(secret_name, socket) {:noreply, socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret_name)} end def handle_event("validate", %{"data" => data}, socket) do socket = assign(socket, data: data) case Livebook.Secrets.validate_secret(data) do {:ok, _} -> {:noreply, assign(socket, errors: [])} {:error, changeset} -> {:noreply, assign(socket, errors: changeset.errors)} end end def handle_event("grant_access", %{"secret_name" => secret_name}, socket) do grant_access(secret_name, socket) {:noreply, socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret_name)} end defp push_secret_selected(%{assigns: %{select_secret_ref: nil}} = socket, _), do: socket defp push_secret_selected(%{assigns: %{select_secret_ref: ref}} = socket, secret_name) do push_event(socket, "secret_selected", %{select_secret_ref: ref, secret_name: secret_name}) end defp prefill_secret_name(socket) do if unavailable_secret?( socket.assigns.prefill_secret_name, socket.assigns.secrets, socket.assigns.livebook_secrets ), do: socket.assigns.prefill_secret_name, else: "" end defp unavailable_secret?(nil, _, _), do: false defp unavailable_secret?("", _, _), do: false defp unavailable_secret?(preselect_name, secrets, livebook_secrets) do not Map.has_key?(secrets, preselect_name) and not Map.has_key?(livebook_secrets, preselect_name) end defp title(%{assigns: %{select_secret_ref: nil}}), do: "Add secret" defp title(%{assigns: %{select_secret_options: %{"title" => title}}}), do: title defp title(_), do: "Select secret" defp set_secret(pid, secret, "session") do Livebook.Session.set_secret(pid, secret) end defp set_secret(pid, secret, "livebook") do Livebook.Secrets.set_secret(secret) Livebook.Session.set_secret(pid, secret) end defp grant_access(secret_name, socket) do secret_value = socket.assigns.livebook_secrets[secret_name] secret = %{name: secret_name, value: secret_value} set_secret(socket.assigns.session.pid, secret, "session") end defp livebook_only_secrets(secrets, livebook_secrets) do Enum.reject(livebook_secrets, &(&1 in secrets)) |> Enum.sort() end defp must_grant_access(%{assigns: %{prefill_secret_name: prefill_secret_name}} = socket) do if not Map.has_key?(socket.assigns.secrets, prefill_secret_name) and Map.has_key?(socket.assigns.livebook_secrets, prefill_secret_name) do prefill_secret_name end end end