Add hub secrets list to Personal Hub page (#1763)

This commit is contained in:
Alexandre de Souza 2023-03-09 12:04:47 -03:00 committed by GitHub
parent 84fccd16ea
commit 3b41e87876
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 413 additions and 4 deletions

View file

@ -39,7 +39,7 @@ defmodule LivebookWeb.EnvVarsComponent do
<div class="flex items-center place-content-end">
<.menu id={"env-var-#{@env_var.name}-menu"}>
<:toggle>
<button class="icon-button" aria-label="open session menu" type="button">
<button class="icon-button" aria-label="open environment variable menu" type="button">
<.remix_icon icon="more-2-fill" class="text-xl" />
</button>
</:toggle>

View file

@ -8,10 +8,16 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
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)}
|> assign(changeset: changeset, secret_value: secret_value)}
end
@impl true
@ -57,6 +63,116 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
</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>
<.secrets_list
id="hub-secrets-list"
new_secret_path={~p"/hub/#{@hub.id}/secrets/new"}
secrets={@secrets}
target={@myself}
/>
</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">
<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 new 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={
with_confirm(
JS.push("delete_hub_secret",
value: %{
name: @secret.name,
value: @secret.value,
hub_id: @secret.hub_id
},
target: @target
),
title: "Delete hub secret - #{@secret.name}",
description: "Are you sure you want to delete this hub secret?",
confirm_text: "Delete",
confirm_icon: "delete-bin-6-line"
)
}
phx-target={@target}
role="menuitem"
>
<.remix_icon icon="delete-bin-line" />
<span>Delete</span>
</button>
</.menu_item>
</.menu>
</div>
</div>
"""
@ -79,4 +195,11 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
def handle_event("validate", %{"personal" => attrs}, socket) do
{:noreply, assign(socket, changeset: Personal.validate_hub(socket.assigns.hub, attrs))}
end
def handle_event("delete_hub_secret", attrs, socket) do
{:ok, secret} = Livebook.Secrets.update_secret(%Livebook.Secrets.Secret{}, attrs)
:ok = Livebook.Hubs.delete_secret(socket.assigns.hub, secret)
{:noreply, socket}
end
end

View file

@ -9,11 +9,20 @@ defmodule LivebookWeb.Hub.EditLive do
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, hub: nil, type: nil, page_title: "Livebook - Hub", env_var_id: nil)}
{:ok,
assign(socket,
hub: nil,
secrets: [],
type: nil,
page_title: "Livebook - Hub",
env_var_id: nil,
secret_name: nil
)}
end
@impl true
def handle_params(params, _url, socket) do
Livebook.Hubs.subscribe([:secrets])
hub = Hubs.fetch_hub!(params["id"])
type = Provider.type(hub)
@ -21,9 +30,11 @@ defmodule LivebookWeb.Hub.EditLive do
assign(socket,
hub: hub,
type: type,
secrets: Hubs.get_secrets(hub),
page_title: "Livebook - Hub",
params: params,
env_var_id: params["env_var_id"]
env_var_id: params["env_var_id"],
secret_name: params["secret_name"]
)}
end
@ -49,6 +60,9 @@ defmodule LivebookWeb.Hub.EditLive do
<.live_component
module={LivebookWeb.Hub.Edit.PersonalComponent}
hub={@hub}
secrets={@secrets}
live_action={@live_action}
secret_name={@secret_name}
id="personal-form"
/>
<% "enterprise" -> %>
@ -72,4 +86,34 @@ defmodule LivebookWeb.Hub.EditLive do
|> put_flash(:success, "Hub deleted successfully")
|> push_navigate(to: "/")}
end
@impl true
def handle_info({:secret_created, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do
{:noreply,
socket
|> refresh_secrets()
|> put_flash(:success, "Secret created successfully")}
end
def handle_info({:secret_updated, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do
{:noreply,
socket
|> refresh_secrets()
|> put_flash(:success, "Secret updated successfully")}
end
def handle_info({:secret_deleted, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do
{:noreply,
socket
|> refresh_secrets()
|> put_flash(:success, "Secret deleted successfully")}
end
def handle_info(_message, socket) do
{:noreply, socket}
end
defp refresh_secrets(socket) do
assign(socket, secrets: Livebook.Hubs.get_secrets(socket.assigns.hub))
end
end

View file

@ -0,0 +1,104 @@
defmodule LivebookWeb.Hub.SecretFormComponent do
use LivebookWeb, :live_component
alias Livebook.Hubs
alias Livebook.Secrets
alias Livebook.Secrets.Secret
@impl true
def update(assigns, socket) do
changeset =
Secrets.change_secret(%Secret{}, %{
name: assigns.secret_name,
value: assigns.secret_value
})
socket = assign(socket, assigns)
{:ok, assign(socket, title: title(socket), changeset: changeset)}
end
@impl true
def render(assigns) do
~H"""
<div class="p-6 max-w-4xl flex flex-col space-y-5">
<h3 class="text-2xl font-semibold text-gray-800">
<%= @title %>
</h3>
<div class="flex flex-columns gap-4">
<.form
:let={f}
id={"#{@id}-form"}
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">
<.text_field
field={f[:name]}
label="Name (alphanumeric and underscore)"
autofocus={@secret_name == nil}
spellcheck="false"
autocomplete="off"
phx-debounce="blur"
class="uppercase"
/>
<.text_field
field={f[:value]}
label="Value"
autofocus={@secret_name != nil}
spellcheck="false"
autocomplete="off"
phx-debounce="blur"
/>
<.hidden_field field={f[:hub_id]} value={@hub.id} />
<div class="flex space-x-2">
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
<.remix_icon icon="add-line" class="align-middle" />
<span class="font-normal">Add</span>
</button>
<.link patch={@return_to} class="button-base button-outlined-gray">
Cancel
</.link>
</div>
</div>
</.form>
</div>
</div>
"""
end
@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, push_patch(socket, to: socket.assigns.return_to)}
else
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
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
defp title(%{assigns: %{secret_name: nil}}), do: "Add secret"
defp title(_), do: "Edit secret"
defp set_secret(%{assigns: %{secret_name: nil}} = socket, %Secret{} = secret) do
Hubs.create_secret(socket.assigns.hub, secret)
end
defp set_secret(socket, %Secret{} = secret) do
Hubs.update_secret(socket.assigns.hub, secret)
end
end

View file

@ -1237,6 +1237,8 @@ defmodule LivebookWeb.SessionLive do
end
def handle_info(:hub_changed, socket) do
Session.set_notebook_hub(socket.assigns.session.pid, socket.private.data.hub.id)
{:noreply, refresh_secrets(socket)}
end

View file

@ -78,6 +78,8 @@ defmodule LivebookWeb.Router do
live "/hub/:id", Hub.EditLive, :edit, as: :hub
live "/hub/:id/env-var/new", Hub.EditLive, :add_env_var, as: :hub
live "/hub/:id/env-var/edit/:env_var_id", Hub.EditLive, :edit_env_var, as: :hub
live "/hub/:id/secrets/new", Hub.EditLive, :new_secret, as: :hub
live "/hub/:id/secrets/edit/:secret_name", Hub.EditLive, :edit_secret, as: :hub
live "/sessions/:id", SessionLive, :page
live "/sessions/:id/shortcuts", SessionLive, :shortcuts

View file

@ -233,6 +233,140 @@ defmodule LivebookWeb.Hub.EditLiveTest do
end
end
describe "personal" do
setup do
Livebook.Hubs.subscribe([:secrets])
{:ok, hub: Hubs.fetch_hub!(Hubs.Personal.id())}
end
test "updates the hub", %{conn: conn, hub: hub} do
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
attrs = %{"hub_emoji" => "🐈"}
view
|> element("#personal-form")
|> render_change(%{"personal" => attrs})
refute view
|> element("#enterprise-form .invalid-feedback")
|> has_element?()
assert {:ok, view, _html} =
view
|> element("#personal-form")
|> render_submit(%{"personal" => attrs})
|> follow_redirect(conn)
assert render(view) =~ "Hub updated successfully"
assert_hub(view, %{hub | hub_emoji: attrs["hub_emoji"]})
refute Hubs.fetch_hub!(hub.id) == hub
end
test "creates secret", %{conn: conn, hub: hub} do
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
secret = build(:secret, name: "PERSONAL_ADD_SECRET")
attrs = %{
secret: %{
name: secret.name,
value: secret.value,
hub_id: secret.hub_id
}
}
refute render(view) =~ secret.name
view
|> element("#add-secret")
|> render_click(%{})
assert_patch(view, ~p"/hub/#{hub.id}/secrets/new")
assert render(view) =~ "Add new secret"
view
|> element("#secrets-form")
|> render_change(attrs)
refute view
|> element("#secrets-form button[disabled]")
|> has_element?()
view
|> element("#secrets-form")
|> render_submit(attrs)
assert_receive {:secret_created, ^secret}
assert render(view) =~ "Secret created successfully"
assert render(view) =~ secret.name
assert secret in Livebook.Hubs.get_secrets(hub)
end
test "updates secret", %{conn: conn, hub: hub} do
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
secret = insert_secret(name: "PERSONAL_EDIT_SECRET", value: "GetTheBonk")
attrs = %{
secret: %{
name: secret.name,
value: secret.value,
hub_id: secret.hub_id
}
}
new_value = "new_value"
view
|> element("#hub-secret-#{secret.name}-edit")
|> render_click(%{"secret_name" => secret.name})
assert_patch(view, ~p"/hub/#{hub.id}/secrets/edit/#{secret.name}")
assert render(view) =~ "Edit secret"
view
|> element("#secrets-form")
|> render_change(attrs)
refute view
|> element("#secrets-form button[disabled]")
|> has_element?()
view
|> element("#secrets-form")
|> render_submit(put_in(attrs.secret.value, new_value))
updated_secret = %{secret | value: new_value}
assert_receive {:secret_updated, ^updated_secret}
assert render(view) =~ "Secret updated successfully"
assert render(view) =~ secret.name
assert updated_secret in Livebook.Hubs.get_secrets(hub)
end
test "deletes secret", %{conn: conn, hub: hub} do
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
secret = insert_secret(name: "PERSONAL_DELETE_SECRET", value: "GetTheBonk")
refute view
|> element("#secrets-form button[disabled]")
|> has_element?()
view
|> with_target("#personal-form-component")
|> render_click("delete_hub_secret", %{
name: secret.name,
value: secret.value,
hub_id: secret.hub_id
})
assert_receive {:secret_deleted, ^secret}
assert render(view) =~ "Secret deleted successfully"
refute render(view) =~ secret.name
refute secret in Livebook.Hubs.get_secrets(hub)
end
end
defp assert_hub(view, hub) do
hubs_html = view |> element("#hubs") |> render()