livebook/lib/livebook_web/live/hub/edit/personal_component.ex
2023-05-10 18:23:08 +02:00

294 lines
9.1 KiB
Elixir

defmodule LivebookWeb.Hub.Edit.PersonalComponent do
use LivebookWeb, :live_component
alias Livebook.Hubs.Personal
alias LivebookWeb.LayoutHelpers
@impl true
def update(assigns, socket) do
changeset = Personal.change_hub(assigns.hub)
secret_value =
if assigns.live_action == :edit_secret do
secret = Enum.find(assigns.secrets, &(&1.name == assigns.secret_name))
secret.value
end
{:ok,
socket
|> assign(assigns)
|> assign(changeset: changeset, stamp_changeset: changeset, secret_value: secret_value)}
end
@impl true
def render(assigns) do
~H"""
<div id={"#{@id}-component"} class="space-y-8 pb-8">
<div class="space-y-4">
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
<p class="text-gray-700">
Your personal hub. All data is stored on your machine and only you can access it.
</p>
</div>
<div class="flex flex-col space-y-10">
<div class="flex flex-col space-y-2">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
General
</h2>
<.form
:let={f}
id={@id}
class="flex flex-col mt-4 space-y-4"
for={@changeset}
phx-submit="save"
phx-change="validate"
phx-target={@myself}
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<.text_field field={f[:hub_name]} label="Name" />
<.emoji_field field={f[:hub_emoji]} label="Emoji" />
</div>
<div>
<button
class="button-base button-blue"
type="submit"
phx-disable-with="Updating..."
disable={not @changeset.valid?}
>
Save
</button>
</div>
</.form>
</div>
<div class="flex flex-col space-y-4">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
Secrets
</h2>
<p class="text-gray-700">
Secrets are a safe way to share credentials and tokens with notebooks.
They are often shared with Smart cells and can be read as
environment variables using the <code>LB_</code> prefix.
</p>
<.secrets_list
id="hub-secrets-list"
new_secret_path={~p"/hub/#{@hub.id}/secrets/new"}
secrets={@secrets}
target={@myself}
/>
</div>
<div class="flex flex-col space-y-4">
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
Stamping
</h2>
<p class="text-gray-700">
Notebooks may be stamped using your <span class="font-medium text-gray-800">secret key</span>.
A stamp allows to securely store information such as the names of the secrets that you granted access to.
You must not share your secret key with others. But you may copy the secret key between
different machines you own.
</p>
<p class="text-gray-700">
If you change the <span class="font-medium text-gray-800">secret key</span>, you will need
to grant access to secrets once again in previously stamped notebooks.
</p>
<.form
:let={f}
id={"#{@id}-stamp"}
class="flex flex-col mt-4 space-y-4"
for={@stamp_changeset}
phx-submit="stamp_save"
phx-change="stamp_validate"
phx-target={@myself}
>
<div class="flex space-x-2">
<div class="grow">
<.password_field field={f[:secret_key]} label="Secret key" />
</div>
<div class="mt-6">
<span class="tooltip top" data-tooltip="Generate">
<button
class="button-base button-outlined-gray button-square-icon"
type="button"
phx-click="generate_secret_key"
phx-target={@myself}
>
<.remix_icon icon="refresh-line" class="text-xl" />
</button>
</span>
</div>
</div>
<div>
<button
class="button-base button-blue"
type="submit"
phx-disable-with="Updating..."
disable={not @stamp_changeset.valid?}
>
Save
</button>
</div>
</.form>
</div>
</div>
<.modal
:if={@live_action in [:new_secret, :edit_secret]}
id="secrets-modal"
show
width={:big}
patch={~p"/hub/#{@hub.id}"}
>
<.live_component
module={LivebookWeb.Hub.SecretFormComponent}
id="secrets"
hub={@hub}
secret_name={@secret_name}
secret_value={@secret_value}
return_to={~p"/hub/#{@hub.id}"}
/>
</.modal>
</div>
"""
end
defp secrets_list(assigns) do
~H"""
<div id={@id} class="flex flex-col space-y-4">
<div class="flex flex-col space-y-4">
<.no_entries :if={@secrets == []}>
No secrets in this Hub yet.
</.no_entries>
<div
:for={secret <- @secrets}
class="flex items-center justify-between border border-gray-200 rounded-lg p-4"
>
<.secret_info secret={secret} target={@target} />
</div>
</div>
<div class="flex">
<.link patch={@new_secret_path} class="button-base button-blue" id="add-secret">
Add secret
</.link>
</div>
</div>
"""
end
defp secret_info(assigns) do
~H"""
<div class="grid grid-cols-1 md:grid-cols-2 w-full">
<div class="place-content-start">
<.labeled_text label="Name">
<%= @secret.name %>
</.labeled_text>
</div>
<div class="flex items-center place-content-end">
<.menu id={"hub-secret-#{@secret.name}-menu"}>
<:toggle>
<button class="icon-button" aria-label="open environment variable menu" type="button">
<.remix_icon icon="more-2-fill" class="text-xl" />
</button>
</:toggle>
<.menu_item>
<.link
id={"hub-secret-#{@secret.name}-edit"}
patch={~p"/hub/#{@secret.hub_id}/secrets/edit/#{@secret.name}"}
type="button"
role="menuitem"
>
<.remix_icon icon="file-edit-line" />
<span>Edit</span>
</.link>
</.menu_item>
<.menu_item variant={:danger}>
<button
id={"hub-secret-#{@secret.name}-delete"}
type="button"
phx-click={
JS.push("delete_hub_secret",
value: %{
name: @secret.name,
value: @secret.value,
hub_id: @secret.hub_id
},
target: @target
)
}
phx-target={@target}
role="menuitem"
>
<.remix_icon icon="delete-bin-line" />
<span>Delete</span>
</button>
</.menu_item>
</.menu>
</div>
</div>
"""
end
@impl true
def handle_event("save", %{"personal" => params}, socket) do
{:noreply, save(params, :changeset, socket)}
end
def handle_event("validate", %{"personal" => params}, socket) do
{:noreply, validate(params, :changeset, socket)}
end
def handle_event("stamp_save", %{"personal" => params}, socket) do
{:noreply, save(params, :stamp_changeset, socket)}
end
def handle_event("stamp_validate", %{"personal" => params}, socket) do
{:noreply, validate(params, :stamp_changeset, socket)}
end
def handle_event("generate_secret_key", %{}, socket) do
params = %{"secret_key" => Personal.generate_secret_key()}
{:noreply, validate(params, :stamp_changeset, socket)}
end
def handle_event("delete_hub_secret", attrs, socket) do
%{hub: hub} = socket.assigns
on_confirm = fn socket ->
{:ok, secret} = Livebook.Secrets.update_secret(%Livebook.Secrets.Secret{}, attrs)
:ok = Livebook.Hubs.delete_secret(hub, secret)
socket
end
{:noreply,
confirm(socket, on_confirm,
title: "Delete hub secret - #{attrs["name"]}",
description: "Are you sure you want to delete this hub secret?",
confirm_text: "Delete",
confirm_icon: "delete-bin-6-line"
)}
end
defp save(params, changeset_name, socket) do
case Personal.update_hub(socket.assigns.hub, params) do
{:ok, hub} ->
socket
|> put_flash(:success, "Hub updated successfully")
|> push_navigate(to: ~p"/hub/#{hub.id}")
{:error, changeset} ->
assign(socket, changeset_name, changeset)
end
end
defp validate(params, changeset_name, socket) do
assign(socket, changeset_name, Personal.validate_hub(socket.assigns.hub, params))
end
end