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.
<%= 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