From 3b41e87876fbccf355f6cfd3951e10fb0639a131 Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Thu, 9 Mar 2023 12:04:47 -0300 Subject: [PATCH] Add hub secrets list to Personal Hub page (#1763) --- lib/livebook_web/live/env_vars_component.ex | 2 +- .../live/hub/edit/personal_component.ex | 125 +++++++++++++++- lib/livebook_web/live/hub/edit_live.ex | 48 ++++++- .../live/hub/secret_form_component.ex | 104 ++++++++++++++ lib/livebook_web/live/session_live.ex | 2 + lib/livebook_web/router.ex | 2 + test/livebook_web/live/hub/edit_live_test.exs | 134 ++++++++++++++++++ 7 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 lib/livebook_web/live/hub/secret_form_component.ex diff --git a/lib/livebook_web/live/env_vars_component.ex b/lib/livebook_web/live/env_vars_component.ex index bc169b071..6638fe21c 100644 --- a/lib/livebook_web/live/env_vars_component.ex +++ b/lib/livebook_web/live/env_vars_component.ex @@ -39,7 +39,7 @@ defmodule LivebookWeb.EnvVarsComponent do
<.menu id={"env-var-#{@env_var.name}-menu"}> <:toggle> - diff --git a/lib/livebook_web/live/hub/edit/personal_component.ex b/lib/livebook_web/live/hub/edit/personal_component.ex index 1cebbc1b4..2bfee9d89 100644 --- a/lib/livebook_web/live/hub/edit/personal_component.ex +++ b/lib/livebook_web/live/hub/edit/personal_component.ex @@ -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
+ +
+

+ Secrets +

+ + <.secrets_list + id="hub-secrets-list" + new_secret_path={~p"/hub/#{@hub.id}/secrets/new"} + secrets={@secrets} + target={@myself} + /> +
+ + + <.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}"} + /> + + + """ + end + + defp secrets_list(assigns) do + ~H""" +
+
+
+ <.secret_info secret={secret} target={@target} /> +
+
+
+ <.link patch={@new_secret_path} class="button-base button-blue" id="add-secret"> + Add new secret + +
+
+ """ + end + + defp secret_info(assigns) do + ~H""" +
+
+ <.labeled_text label="Name"> + <%= @secret.name %> + +
+ +
+ <.menu id={"hub-secret-#{@secret.name}-menu"}> + <: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" /> + Edit + + + <.menu_item variant={:danger}> + + +
""" @@ -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 diff --git a/lib/livebook_web/live/hub/edit_live.ex b/lib/livebook_web/live/hub/edit_live.ex index 3ed287630..c1185e5f1 100644 --- a/lib/livebook_web/live/hub/edit_live.ex +++ b/lib/livebook_web/live/hub/edit_live.ex @@ -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 diff --git a/lib/livebook_web/live/hub/secret_form_component.ex b/lib/livebook_web/live/hub/secret_form_component.ex new file mode 100644 index 000000000..04205e279 --- /dev/null +++ b/lib/livebook_web/live/hub/secret_form_component.ex @@ -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""" +
+

+ <%= @title %> +

+
+ <.form + :let={f} + id={"#{@id}-form"} + for={@changeset} + phx-target={@myself} + phx-change="validate" + phx-submit="save" + autocomplete="off" + class="basis-1/2 grow" + > +
+ <.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} /> +
+ + <.link patch={@return_to} class="button-base button-outlined-gray"> + Cancel + +
+
+ +
+
+ """ + 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 diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index b70f0c416..ba374768d 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -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 diff --git a/lib/livebook_web/router.ex b/lib/livebook_web/router.ex index d365b580f..5cca09073 100644 --- a/lib/livebook_web/router.ex +++ b/lib/livebook_web/router.ex @@ -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 diff --git a/test/livebook_web/live/hub/edit_live_test.exs b/test/livebook_web/live/hub/edit_live_test.exs index 5d7f52ccb..6ca890b46 100644 --- a/test/livebook_web/live/hub/edit_live_test.exs +++ b/test/livebook_web/live/hub/edit_live_test.exs @@ -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()