Packages
livebook
0.19.0
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/session_live/secrets_component.ex
defmodule LivebookWeb.SessionLive.SecretsComponent do
use LivebookWeb, :live_component
alias Livebook.Hubs
alias Livebook.Secrets
alias Livebook.Secrets.Secret
alias Livebook.Session
@impl true
def mount(socket) do
{:ok, socket}
end
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
secret_name = socket.assigns.prefill_secret_name
socket =
socket
|> assign(title: title(socket))
|> assign_new(:changeset, fn ->
attrs = %{name: secret_name, value: nil, hub_id: nil}
Secrets.change_secret(%Secret{}, attrs)
end)
|> assign_new(:grant_access_secret, fn ->
Enum.find(
socket.assigns.hub_secrets,
&(&1.name == secret_name and not is_map_key(socket.assigns.secrets, secret_name))
)
end)
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div class="flex flex-col space-y-5">
<h3 class="text-2xl font-semibold text-gray-800">
{@title}
</h3>
<p class="text-gray-700">
The notebook can read the secret value as a LB_ prefixed environment variable.
</p>
<.grant_access_message
:if={@grant_access_secret}
secret={@grant_access_secret}
target={@myself}
hub={@hub}
/>
<div class="flex flex-columns gap-4">
<div :if={@select_secret_metadata} class="basis-1/2 grow-0 pr-4 border-r">
<div class="flex flex-col space-y-4">
<p class="text-gray-800">
Choose a secret
</p>
<div class="flex flex-wrap">
<.secret_with_badge
:for={
secret <-
@secrets |> Session.Data.session_secrets(@hub.id) |> Enum.sort_by(& &1.name)
}
secret_name={secret.name}
hub?={false}
stored="Session"
active={secret.name == @prefill_secret_name}
target={@myself}
/>
<.secret_with_badge
:for={secret <- Enum.sort_by(@hub_secrets, & &1.name)}
secret_name={secret.name}
hub?={true}
stored={hub_label(@hub)}
active={
secret.name == @prefill_secret_name and
Session.Data.secret_toggled?(secret, @secrets)
}
target={@myself}
/>
<div
:if={@secrets == %{} and @hub_secrets == []}
class="w-full text-center text-gray-400 border rounded-lg p-8"
>
<.remix_icon icon="folder-lock-line" class="align-middle text-2xl" />
<span class="mt-1 block text-sm text-gray-700">
Secrets not found. <br /> Add to see them here.
</span>
</div>
</div>
</div>
</div>
<.form
:let={f}
for={@changeset}
phx-target={@myself}
phx-change="validate"
phx-submit="save"
autocomplete="off"
class="basis-1/2 grow"
>
<div class="flex flex-col space-y-4">
<p :if={@select_secret_metadata} class="text-gray-700">
Add new secret
</p>
<.text_field
field={f[:name]}
label="Name (alphanumeric and underscore)"
outer_prefix="LB_"
autofocus={@prefill_secret_name == nil}
spellcheck="false"
autocomplete="off"
phx-debounce
class="uppercase"
/>
<.password_field
field={f[:value]}
label="Value"
autofocus={@prefill_secret_name != nil}
spellcheck="false"
autocomplete="off"
phx-debounce
/>
<.radio_field
field={f[:hub_id]}
options={[
{"", "only this session"},
{@hub.id, "in #{@hub.hub_emoji} #{@hub.hub_name}"}
]}
/>
<div class="flex space-x-2">
<.button type="submit" disabled={not @changeset.valid?}>
<.remix_icon icon="add-line" />
<span class="font-normal">Add</span>
</.button>
<.button color="gray" outlined patch={@return_to}>
Cancel
</.button>
</div>
</div>
</.form>
</div>
</div>
"""
end
defp secret_with_badge(assigns) do
~H"""
<div
role="button"
class={[
"flex justify-between w-full font-mono text-sm p-2 border-b cursor-pointer",
if @active do
"bg-blue-100 text-blue-700"
else
"text-gray-700 hover:bg-gray-100"
end
]}
phx-click="select_secret"
phx-value-name={@secret_name}
phx-value-hub={@hub?}
phx-target={@target}
>
{@secret_name}
<span class={[
"inline-flex items-center font-sans rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-100",
if @active do
"bg-indigo-100 text-blue-800"
else
"bg-gray-100 text-gray-800"
end
]}>
<svg
:if={@active}
class="-ml-0.5 mr-1.5 h-2 w-2 text-blue-400"
fill="currentColor"
viewBox="0 0 8 8"
>
<circle cx="4" cy="4" r="3" />
</svg>
{@stored}
</span>
</div>
"""
end
defp grant_access_message(assigns) do
~H"""
<div>
<div class="mx-auto">
<div class="rounded-lg bg-blue-600 py-1 px-4 shadow-sm">
<div class="flex flex-wrap items-center justify-between">
<div class="flex w-0 flex-1 items-center pr-1">
<.remix_icon
icon="error-warning-fill"
class="align-middle text-2xl flex text-gray-100 rounded-lg py-2"
/>
<span class="ml-2 text-sm font-normal text-gray-100">
There is a secret named <span class="font-semibold text-white">{@secret.name}</span>
in the {hub_label(@hub)} workspace. Allow this notebook to access it?
</span>
</div>
<.button
color="gray"
phx-click="select_secret"
phx-value-name={@secret.name}
phx-value-hub={true}
phx-target={@target}
>
Grant access
</.button>
</div>
</div>
</div>
</div>
"""
end
@impl true
def handle_event("save", %{"secret" => attrs}, socket) do
changeset = Secrets.change_secret(%Secret{}, attrs)
with {:ok, secret} <- Ecto.Changeset.apply_action(changeset, :insert),
:ok <- save_secret(socket, secret, changeset) do
Session.set_secret(socket.assigns.session.pid, secret)
{:noreply,
socket
|> push_patch(to: socket.assigns.return_to)
|> push_secret_selected(secret.name)}
else
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
{:transport_error, error} ->
{:noreply,
socket
|> push_patch(to: ~p"/sessions/#{socket.assigns.session.id}/secrets")
|> put_flash(:error, error)}
end
end
def handle_event("validate", %{"secret" => attrs}, socket) do
changeset =
%Secret{}
|> Secrets.change_secret(attrs)
|> Map.replace!(:action, :validate)
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("select_secret", %{"name" => secret_name} = attrs, socket) do
if attrs["hub"] do
secret = Enum.find(socket.assigns.hub_secrets, &(&1.name == secret_name))
if secret && !Session.Data.secret_toggled?(secret, socket.assigns.secrets) do
Session.set_secret(socket.assigns.session.pid, secret)
end
end
{:noreply,
socket
|> push_patch(to: socket.assigns.return_to)
|> push_secret_selected(secret_name)}
end
defp push_secret_selected(%{assigns: %{select_secret_metadata: nil}} = socket, _), do: socket
defp push_secret_selected(
%{assigns: %{select_secret_metadata: %{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_metadata: nil}}), do: "Add secret"
defp title(%{assigns: %{select_secret_metadata: %{options: %{"title" => title}}}}), do: title
defp title(_), do: "Select secret"
defp save_secret(socket, secret, changeset) do
if secret.hub_id do
with {:error, errors} <- Hubs.create_secret(socket.assigns.hub, secret) do
{:error,
changeset
|> Livebook.Utils.put_changeset_errors(errors)
|> Map.replace!(:action, :validate)}
end
else
:ok
end
end
defp hub_label(hub), do: "#{hub.hub_emoji} #{hub.hub_name}"
end