defmodule LivebookWeb.SessionLive.SecretsComponent do use LivebookWeb, :live_component alias Livebook.Hubs alias Livebook.Secrets alias Livebook.Secrets.Secret alias Livebook.Session alias Livebook.EctoTypes.SecretOrigin @impl true def mount(socket) do {:ok, assign(socket, title: title(socket), hubs: Livebook.Hubs.get_hubs([:secrets]))} end @impl true def update(assigns, socket) do socket = assign(socket, assigns) secret_name = socket.assigns[:prefill_secret_name] socket = socket |> assign_new(:changeset, fn -> attrs = %{name: secret_name, value: nil, origin: :session} Secrets.change_secret(%Secret{}, attrs) end) |> assign_new(:grant_access_secret, fn -> Enum.find(socket.assigns.saved_secrets, &(&1.name == secret_name)) end) {:ok, socket} end @impl true def render(assigns) do ~H"""

<%= @title %>

<.grant_access_message :if={@grant_access_secret} secret={@grant_access_secret} target={@myself} />

Choose a secret

<.secret_with_badge :for={{secret_name, _} <- Enum.sort(@secrets)} secret_name={secret_name} secret_origin={:session} stored="Session" active={secret_name == @prefill_secret_name} target={@myself} /> <.secret_with_badge :for={secret <- @saved_secrets} secret_name={secret.name} secret_origin={secret.origin} stored={stored(secret)} active={false} target={@myself} />
<.remix_icon icon="folder-lock-line" class="align-middle text-2xl" /> Secrets not found.
Add to see them here.
<.form :let={f} for={@changeset} phx-target={@myself} phx-change="validate" phx-submit="save" autocomplete="off" class="basis-1/2 grow" >

Add new secret

<.text_field field={f[:name]} label="Name (alphanumeric and underscore)" autofocus={@prefill_secret_name == nil} spellcheck="false" autocomplete="off" phx-debounce="blur" /> <.text_field field={f[:value]} label="Value" autofocus={@prefill_secret_name != nil} spellcheck="false" autocomplete="off" phx-debounce="blur" /> <.radio_field field={f[:origin]} label="Storage" options={ [{"session", "only this session"}, {"app", "in the Livebook app"}] ++ if Livebook.Config.feature_flag_enabled?(:hub) do for hub <- @hubs, do: {"hub-#{hub.id}", "in #{hub.hub_emoji} #{hub.hub_name}"} else [] end } />
<.link patch={@return_to} class="button-base button-outlined-gray"> Cancel
""" end defp secret_with_badge(assigns) do ~H"""
<%= @secret_name %> <%= @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" /> <%= if @secret.origin in [:app, :startup] do %> There is a secret named <%= @secret.name %> in your Livebook app. Allow this session to access it? <% else %> There is a secret named <%= @secret.name %> in your Livebook Hub. Allow this session to access it? <% end %>
""" end defp stored(%{origin: {:hub, _}}), do: "Hub" defp stored(%{origin: origin}) when origin in [:app, :startup], do: "Livebook" @impl true def handle_event("save", %{"secret" => attrs}, socket) do with {:ok, secret} <- Secrets.update_secret(%Secret{}, attrs), :ok <- set_secret(socket, secret) do {:noreply, socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret.name)} else {:error, changeset} -> {:noreply, assign(socket, changeset: changeset)} end end def handle_event("select_secret", %{"name" => secret_name, "origin" => origin}, socket) do {:ok, origin} = SecretOrigin.decode(origin) grant_access(socket.assigns.saved_secrets, secret_name, origin, socket) {:noreply, socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret_name)} end def handle_event("validate", %{"secret" => attrs}, socket) do changeset = %Secret{} |> Secrets.change_secret(attrs) |> Map.put(:action, :validate) {:noreply, assign(socket, changeset: changeset)} end def handle_event("grant_access", %{"name" => secret_name, "origin" => origin}, socket) do {:ok, origin} = SecretOrigin.decode(origin) grant_access(socket.assigns.saved_secrets, secret_name, origin, 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 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(socket, %Secret{origin: :session} = secret) do Session.set_secret(socket.assigns.session.pid, secret) end defp set_secret(socket, %Secret{origin: :app} = secret) do Secrets.set_secret(secret) Session.set_secret(socket.assigns.session.pid, secret) end defp set_secret(socket, %Secret{origin: {:hub, id}} = secret) when is_binary(id) do with :ok <- Hubs.create_secret(secret) do Session.set_secret(socket.assigns.session.pid, secret) end end defp grant_access(secrets, secret_name, origin, socket) do secret = Enum.find(secrets, &(&1.name == secret_name and &1.origin == origin)) if secret, do: Livebook.Session.set_secret(socket.assigns.session.pid, secret) end end