defmodule LivebookWeb.SessionLive.SecretsComponent do use LivebookWeb, :live_component alias Livebook.Secrets.Secret @impl true def update(assigns, socket) do socket = socket |> assign(assigns) |> assign(hubs: Livebook.Hubs.get_hubs([:secrets])) {:ok, assign(socket, prefill_assigns(socket))} end @impl true def render(assigns) do ~H"""

<%= @title %>

<%= if @grant_access_name do %> <.grant_access_message secret_name={@grant_access_name} secret_origin={@grant_access_origin} 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} secret_origin="session" stored="Session" action="select_secret" active={secret_name == @prefill_secret_name} target={@myself} /> <% end %> <%= for secret <- @saved_secrets do %> <.secret_with_badge secret_name={secret.name} secret_store={store(secret)} secret_origin={origin(secret)} stored={stored(secret)} action="select_secret" active={false} target={@myself} /> <% end %> <%= if @secrets == %{} and @saved_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, "app", checked: @data["store"] == "app") %> in the Livebook app <% end %> <%= if Livebook.Config.feature_flag_enabled?(:hub) do %> <%= label class: "flex items-center gap-2 text-gray-600" do %> <%= radio_button(f, :store, "hub", disabled: @hubs == [], checked: @data["store"] == "hub" ) %> in the Hub <% end %> <%= if @data["store"] == "hub" do %> <%= select(f, :hub_id, hubs_options(@hubs, @data["hub_id"]), class: "input") %> <% end %> <% end %>
<%= live_patch("Cancel", to: @return_to, class: "button-base button-outlined-gray") %>
""" end defp secret_with_badge(%{secret_store: "hub"} = assigns) do ~H"""
<%= @secret_name %> <%= if @active do %> <% end %> <%= @stored %>
""" 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" /> <%= 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 %>
<%= if @secret_origin in ["app", "startup"] do %> <% else %> <% end %>
""" end defp prefill_assigns(socket) do secret_name = socket.assigns[:prefill_secret_name] assigns = %{ data: %{"name" => secret_name, "value" => "", "store" => "session"}, errors: [{"value", {"can't be blank", []}}], title: title(socket), grant_access_name: nil, grant_access_origin: "app", has_prefill: !is_nil(secret_name) } case Enum.find(socket.assigns.saved_secrets, &(&1.name == secret_name)) do %Secret{name: name, origin: {:hub, id}} -> %{assigns | grant_access_name: name, grant_access_origin: id} %Secret{name: name, origin: origin} -> %{assigns | grant_access_name: name, grant_access_origin: to_string(origin)} nil -> assigns end end defp store(%{origin: {:hub, _id}}), do: "hub" defp store(%{origin: origin}), do: to_string(origin) defp origin(%{origin: {:hub, id}}), do: id defp origin(%{origin: origin}), do: to_string(origin) defp stored(%{origin: {:hub, _}}), do: "Hub" defp stored(%{origin: origin}) when origin in [:app, :startup], do: "Livebook" @impl true def handle_event("save", %{"data" => data}, socket) do with attrs <- build_attrs(data), {:ok, secret} <- Livebook.Secrets.validate_secret(attrs), :ok <- set_secret(socket, secret) do {:noreply, socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret.name)} else {:error, %{errors: errors}} -> {:noreply, assign(socket, errors: errors)} end end def handle_event("select_secret", %{"name" => secret_name} = attrs, socket) do grant_access(socket.assigns.saved_secrets, secret_name, build_origin(attrs), 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, _secret} -> {:noreply, assign(socket, errors: [])} {:error, changeset} -> {:noreply, assign(socket, errors: changeset.errors)} end end def handle_event("grant_access", %{"name" => secret_name} = attrs, socket) do grant_access(socket.assigns.saved_secrets, secret_name, build_origin(attrs), 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 build_origin(%{"store" => "hub", "hub_id" => id}), do: {:hub, id} defp build_origin(%{"store" => store}), do: String.to_existing_atom(store) defp build_attrs(%{"name" => name, "value" => value} = attrs) do %{name: name, value: value, origin: build_origin(attrs)} end defp set_secret(socket, %Secret{origin: :session} = secret) do Livebook.Session.set_secret(socket.assigns.session.pid, secret) end defp set_secret(socket, %Secret{origin: :app} = secret) do Livebook.Secrets.set_secret(secret) Livebook.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 <- Livebook.Hubs.create_secret(secret) do Livebook.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 defp hubs_options(hubs, hub_id) do [[key: "Select one Hub", value: "", selected: true, disabled: true]] ++ for hub <- hubs do [key: hub.hub_name, value: hub.id, selected: hub.id == hub_id] end end end