From fb578906ecb1256a52df8c9f25be31562976cab7 Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Tue, 1 Aug 2023 14:27:44 -0300 Subject: [PATCH] Improves disconnected status on Hub's page (#2127) --- lib/livebook/hubs.ex | 5 +- lib/livebook/hubs/team_client.ex | 10 +- lib/livebook/secrets.ex | 11 - lib/livebook_web/live/home_live.ex | 11 +- .../live/hub/edit/personal_component.ex | 256 ++++++------ .../live/hub/edit/team_component.ex | 383 ++++++++++-------- lib/livebook_web/live/hub/edit_live.ex | 55 ++- .../live/hub/secret_form_component.ex | 4 +- lib/livebook_web/live/layout_helpers.ex | 32 +- .../livebook_teams/web/hub/edit_live_test.exs | 12 +- test/livebook_web/live/hub/edit_live_test.exs | 12 +- 11 files changed, 429 insertions(+), 362 deletions(-) diff --git a/lib/livebook/hubs.ex b/lib/livebook/hubs.ex index 3eb4da180..b66cbe727 100644 --- a/lib/livebook/hubs.ex +++ b/lib/livebook/hubs.ex @@ -126,7 +126,6 @@ defmodule Livebook.Hubs do Topic `hubs:connection`: * `{:hub_connected, hub_id}` - * `{:hub_disconnected, hub_id}` * `{:hub_connection_failed, hub_id, reason}` * `{:hub_server_error, hub_id, reason}` @@ -218,7 +217,9 @@ defmodule Livebook.Hubs do @spec get_secrets(Provider.t()) :: list(Secret.t()) def get_secrets(hub) do if capability?(hub, [:list_secrets]) do - Provider.get_secrets(hub) + hub + |> Provider.get_secrets() + |> Enum.sort() else [] end diff --git a/lib/livebook/hubs/team_client.ex b/lib/livebook/hubs/team_client.ex index 21763b43b..03250e446 100644 --- a/lib/livebook/hubs/team_client.ex +++ b/lib/livebook/hubs/team_client.ex @@ -165,10 +165,12 @@ defmodule Livebook.Hubs.TeamClient do end defp handle_event(:secret_deleted, secret_deleted, state) do - secret = Enum.find(state.secrets, &(&1.name == secret_deleted.name)) - Broadcasts.secret_deleted(secret) - - remove_secret(state, secret) + if secret = Enum.find(state.secrets, &(&1.name == secret_deleted.name)) do + Broadcasts.secret_deleted(secret) + remove_secret(state, secret) + else + state + end end defp handle_event(:user_connected, %{secrets: secrets}, state) do diff --git a/lib/livebook/secrets.ex b/lib/livebook/secrets.ex index afbba3ba3..a3f4033bd 100644 --- a/lib/livebook/secrets.ex +++ b/lib/livebook/secrets.ex @@ -61,17 +61,6 @@ defmodule Livebook.Secrets do |> Ecto.Changeset.apply_action(:update) end - @doc """ - Returns an `%Ecto.Changeset{}` with errors. - """ - @spec add_secret_error(Ecto.Changeset.t() | Secret.t(), atom(), String.t()) :: - Ecto.Changeset.t() - def add_secret_error(%Secret{} = secret, field, message) do - secret - |> change_secret(%{}) - |> Ecto.Changeset.add_error(field, message) - end - @doc """ Stores the given secret as is, without validation. """ diff --git a/lib/livebook_web/live/home_live.ex b/lib/livebook_web/live/home_live.ex index fe32d3524..166cf72a1 100644 --- a/lib/livebook_web/live/home_live.ex +++ b/lib/livebook_web/live/home_live.ex @@ -157,7 +157,7 @@ defmodule LivebookWeb.HomeLive do defp update_notification(assigns) do ~H""" -
+ Livebook v<%= @version %> available! <%= if @instructions_url do %> @@ -189,16 +189,13 @@ defmodule LivebookWeb.HomeLive do <% end %> 🚀 -
+ """ end defp memory_notification(assigns) do ~H""" -
+ <.remix_icon icon="alarm-warning-line" class="align-text-bottom mr-0.5" /> Less than 30 MB of memory left, consider running sessions -
+ """ end diff --git a/lib/livebook_web/live/hub/edit/personal_component.ex b/lib/livebook_web/live/hub/edit/personal_component.ex index 139971c80..10a2a669a 100644 --- a/lib/livebook_web/live/hub/edit/personal_component.ex +++ b/lib/livebook_web/live/hub/edit/personal_component.ex @@ -32,141 +32,143 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do @impl true def render(assigns) do ~H""" -
-
-
- +
+
+
+
+ -

- Your personal hub. All data is stored on your machine and only you can access it. -

+

+ Your personal hub. All data is stored on your machine and only you can access it. +

-
-
-

- General -

+
+
+

+ General +

- <.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} - > -
- <.text_field field={f[:hub_name]} label="Name" /> - <.emoji_field field={f[:hub_emoji]} label="Emoji" /> -
-
- -
- -
- -
-

- Secrets -

- -

- 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 LB_ prefix. -

- - <.live_component - module={LivebookWeb.Hub.SecretListComponent} - id="hub-secrets-list" - hub={@hub} - secrets={@secrets} - target={@myself} - /> -
- -
-

- Stamping -

- -

- Notebooks may be stamped using your secret key. - 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. -

-

- If you change the secret key, you will need - to grant access to secrets once again in previously stamped notebooks. -

- - <.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} - > -
-
- <.password_field field={f[:secret_key]} label="Secret key" /> + <.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} + > +
+ <.text_field field={f[:hub_name]} label="Name" /> + <.emoji_field field={f[:hub_emoji]} label="Emoji" />
-
- - - +
+
-
-
- -
- + +
+ +
+

+ Secrets +

+ +

+ 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 LB_ prefix. +

+ + <.live_component + module={LivebookWeb.Hub.SecretListComponent} + id="hub-secrets-list" + hub={@hub} + secrets={@secrets} + target={@myself} + /> +
+ +
+

+ Stamping +

+ +

+ Notebooks may be stamped using your secret key. + 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. +

+

+ If you change the secret key, you will need + to grant access to secrets once again in previously stamped notebooks. +

+ + <.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} + > +
+
+ <.password_field field={f[:secret_key]} label="Secret key" /> +
+
+ + + +
+
+
+ +
+ +
-
- <.modal - :if={@live_action in [:new_secret, :edit_secret]} - id="secrets-modal" - show - width={:medium} - 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 + :if={@live_action in [:new_secret, :edit_secret]} + id="secrets-modal" + show + width={:medium} + 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 @@ -201,8 +203,6 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do _ = Livebook.Hubs.delete_secret(hub, secret) socket - |> put_flash(:success, "Secret deleted successfully") - |> push_navigate(to: ~p"/hub/#{hub.id}") end {:noreply, diff --git a/lib/livebook_web/live/hub/edit/team_component.ex b/lib/livebook_web/live/hub/edit/team_component.ex index 34f2caced..9361d1339 100644 --- a/lib/livebook_web/live/hub/edit/team_component.ex +++ b/lib/livebook_web/live/hub/edit/team_component.ex @@ -1,7 +1,7 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do use LivebookWeb, :live_component - alias Livebook.Hubs.Team + alias Livebook.Hubs.{Provider, Team} alias Livebook.Teams alias LivebookWeb.LayoutHelpers alias LivebookWeb.NotFoundError @@ -26,7 +26,8 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do secrets: secrets, show_key: show_key?, secret_name: secret_name, - secret_value: secret_value + secret_value: secret_value, + hub_metadata: Provider.to_metadata(assigns.hub) ) |> assign_dockerfile() |> assign_form(changeset)} @@ -35,193 +36,222 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do @impl true def render(assigns) do ~H""" -
-
-
- +
+ + <%= Provider.connection_error(@hub) %> + -
- -
-
-
-

- A shared Teams hub. All resources here are shared with your team. Manage users and billing on livebook.dev. -

-
+
+
+
+
+
+ +
+
+ + <%= @hub.hub_emoji %> -
-
-

- General -

+
+ +
+ <%= @hub.hub_name %> +
+ - <.form - :let={f} - id={@id} - class="flex flex-col mt-4 space-y-4" - for={@form} - phx-submit="save" - phx-change="validate" - phx-target={@myself} - > -
- <.text_field - field={f[:hub_name]} - label="Name" - disabled - help="Name cannot be changed" - class="bg-gray-200/50 border-200/80 cursor-not-allowed" - /> - <.emoji_field field={f[:hub_emoji]} label="Emoji" /> +
+ +
+
-
- -
- -
+
+

+ A shared Teams hub. All resources here are shared with your team. Manage users and billing on livebook.dev. +

+
-
-

- Secrets -

+
+
+

+ General +

-

- 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 LB_ prefix. -

- - <.live_component - module={LivebookWeb.Hub.SecretListComponent} - id="hub-secrets-list" - hub={@hub} - secrets={@secrets} - target={@myself} - /> -
- -
-

- Airgapped Deployment -

- -

- It is possible to deploy notebooks that belong to this Hub in an airgapped - deployment, without connecting back to Livebook Teams server. This is done - using the Docker image template below, which encrypts all of your Hub metadata, - and taking some additional steps. -

- -
-
- Dockerfile - +
+ <.text_field + field={f[:hub_name]} + label="Name" + disabled + help="Name cannot be changed" + class="bg-gray-200/50 border-200/80 cursor-not-allowed" + /> + <.emoji_field field={f[:hub_emoji]} label="Emoji" /> +
+ +
+ +
+
- <.code_preview - source_id={"offline-deployment-#{@hub.id}-source"} - source={@dockerfile} - language="dockerfile" - /> +
+

+ Secrets +

-
    -
  1. - You must change /path/to/my/notebooks in the template above - to point to a directory with the `.livemd` files you want to deploy -
  2. -
  3. - You must set the LIVEBOOK_TEAMS_KEY environment variable - directly on your deployment platform, with the value you can find at the - top of this page -
  4. -
  5. - You may set the LIVEBOOK_PASSWORD environment variable to any - value of your choice, if you want to access and debug your deployed notebooks - in production -
  6. -
+

+ 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 LB_ prefix. +

+ + <.live_component + module={LivebookWeb.Hub.SecretListComponent} + id="hub-secrets-list" + hub={@hub} + secrets={@secrets} + target={@myself} + /> +
+ +
+

+ Airgapped Deployment +

+ +

+ It is possible to deploy notebooks that belong to this Hub in an airgapped + deployment, without connecting back to Livebook Teams server. This is done + using the Docker image template below, which encrypts all of your Hub metadata, + and taking some additional steps. +

+ +
+
+ Dockerfile + +
+ + <.code_preview + source_id={"offline-deployment-#{@hub.id}-source"} + source={@dockerfile} + language="dockerfile" + /> + +
    +
  1. + You must change /path/to/my/notebooks in the template above + to point to a directory with the `.livemd` files you want to deploy +
  2. +
  3. + You must set the LIVEBOOK_TEAMS_KEY environment variable + directly on your deployment platform, with the value you can find at the + top of this page +
  4. +
  5. + You may set the LIVEBOOK_PASSWORD environment variable to any + value of your choice, if you want to access and debug your deployed notebooks + in production +
  6. +
+
+
+ +
+

+ Danger Zone +

+ +
+
+

+ Delete this hub +

+

Once deleted, you won’t be able to access its features unless you rejoin.

+
+ +
+
-
-

- Danger Zone -

+ <.modal show={@show_key} id="show-key-modal" width={:medium} patch={~p"/hub/#{@hub.id}"}> + <.teams_key_modal teams_key={@hub.teams_key} /> + -
-
-

- Delete this hub -

-

Once deleted, you won’t be able to access its features unless you rejoin.

-
- -
-
+ <.modal + :if={@live_action in [:new_secret, :edit_secret]} + id="secrets-modal" + show + width={:medium} + 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 show={@show_key} id="show-key-modal" width={:medium} patch={~p"/hub/#{@hub.id}"}> - <.teams_key_modal teams_key={@hub.teams_key} /> - - - <.modal - :if={@live_action in [:new_secret, :edit_secret]} - id="secrets-modal" - show - width={:medium} - 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 @@ -337,13 +367,8 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do {:ok, secret} = Livebook.Secrets.update_secret(%Livebook.Secrets.Secret{}, attrs) case Livebook.Hubs.delete_secret(hub, secret) do - :ok -> - socket - |> put_flash(:success, "Secret deleted successfully") - |> push_navigate(to: ~p"/hub/#{hub.id}") - - {:transport_error, reason} -> - put_flash(socket, :error, reason) + :ok -> socket + {:transport_error, reason} -> put_flash(socket, :error, reason) end end diff --git a/lib/livebook_web/live/hub/edit_live.ex b/lib/livebook_web/live/hub/edit_live.ex index 7e05ed329..d280b6a92 100644 --- a/lib/livebook_web/live/hub/edit_live.ex +++ b/lib/livebook_web/live/hub/edit_live.ex @@ -14,7 +14,7 @@ defmodule LivebookWeb.Hub.EditLive do @impl true def handle_params(params, _url, socket) do - Hubs.subscribe([:secrets]) + Hubs.subscribe([:connection, :secrets]) hub = Hubs.fetch_hub!(params["id"]) type = Provider.type(hub) @@ -29,15 +29,13 @@ defmodule LivebookWeb.Hub.EditLive do current_user={@current_user} saved_hubs={@saved_hubs} > -
- <.hub_component - type={@type} - hub={@hub} - live_action={@live_action} - params={@params} - counter={@counter} - /> -
+ <.hub_component + type={@type} + hub={@hub} + live_action={@live_action} + params={@params} + counter={@counter} + /> """ end @@ -87,30 +85,45 @@ defmodule LivebookWeb.Hub.EditLive do end @impl true - def handle_info({:secret_created, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do + def handle_info( + {:secret_created, %{name: name, hub_id: id}}, + %{assigns: %{hub: %{id: id}}} = socket + ) do {:noreply, socket - |> increment_counter() - |> put_flash(:success, "Secret created successfully")} + |> push_navigate(to: ~p"/hub/#{id}") + |> put_flash(:success, "Secret #{name} created successfully")} end - def handle_info({:secret_updated, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do + def handle_info( + {:secret_updated, %{name: name, hub_id: id}}, + %{assigns: %{hub: %{id: id}}} = socket + ) do {:noreply, socket - |> increment_counter() - |> put_flash(:success, "Secret updated successfully")} + |> push_navigate(to: ~p"/hub/#{id}") + |> put_flash(:success, "Secret #{name} updated successfully")} end - def handle_info({:secret_deleted, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do + def handle_info( + {:secret_deleted, %{name: name, hub_id: id}}, + %{assigns: %{hub: %{id: id}}} = socket + ) do {:noreply, socket - |> increment_counter() - |> put_flash(:success, "Secret deleted successfully")} + |> push_navigate(to: ~p"/hub/#{id}") + |> put_flash(:success, "Secret #{name} deleted successfully")} + end + + def handle_info({:hub_connected, id}, %{assigns: %{hub: %{id: id}}} = socket) do + {:noreply, push_navigate(socket, to: ~p"/hub/#{id}")} + end + + def handle_info({_event, id, _reason}, %{assigns: %{hub: %{id: id}}} = socket) do + {:noreply, push_navigate(socket, to: ~p"/hub/#{id}")} end def handle_info(_message, socket) do {:noreply, socket} end - - defp increment_counter(socket), do: assign(socket, counter: socket.assigns.counter + 1) end diff --git a/lib/livebook_web/live/hub/secret_form_component.ex b/lib/livebook_web/live/hub/secret_form_component.ex index 2b2fd6550..3239996a8 100644 --- a/lib/livebook_web/live/hub/secret_form_component.ex +++ b/lib/livebook_web/live/hub/secret_form_component.ex @@ -77,8 +77,8 @@ defmodule LivebookWeb.Hub.SecretFormComponent do :ok <- set_secret(socket, secret) do message = if socket.assigns.secret_name, - do: "Secret updated successfully", - else: "Secret created successfully" + do: "Secret #{secret.name} updated successfully", + else: "Secret #{secret.name} created successfully" {:noreply, socket diff --git a/lib/livebook_web/live/layout_helpers.ex b/lib/livebook_web/live/layout_helpers.ex index 1d7dd366d..b77546b67 100644 --- a/lib/livebook_web/live/layout_helpers.ex +++ b/lib/livebook_web/live/layout_helpers.ex @@ -259,10 +259,16 @@ defmodule LivebookWeb.LayoutHelpers do <.title text="Learn" /> """ - attr :text, :string, required: true + attr :text, :string, default: nil attr :back_navigate, :string, default: nil + slot :inner_block + def title(assigns) do + if assigns.text == nil and assigns.inner_block == [] do + raise ArgumentError, "should pass at least text attribute or an inner block" + end + ~H"""

- <%= @text %> + <%= if @inner_block != [] do %> + <%= render_slot(@inner_block) %> + <% else %> + <%= @text %> + <% end %>

""" end + + @doc """ + Topbar for showing pinned, page-specific messages. + """ + attr :variant, :atom, default: :info, values: [:warning, :info, :error] + slot :inner_block, required: true + + def topbar(assigns) do + ~H""" +
+ <%= render_slot(@inner_block) %> +
+ """ + end + + defp topbar_class(:warning), do: "bg-yellow-200 text-gray-900" + defp topbar_class(:info), do: "bg-blue-200 text-gray-900" + defp topbar_class(:error), do: "bg-red-200 text-gray-900" end diff --git a/test/livebook_teams/web/hub/edit_live_test.exs b/test/livebook_teams/web/hub/edit_live_test.exs index 4351b54b1..9f43bc0ec 100644 --- a/test/livebook_teams/web/hub/edit_live_test.exs +++ b/test/livebook_teams/web/hub/edit_live_test.exs @@ -98,7 +98,9 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do |> render_submit(attrs) assert_receive {:secret_created, ^secret} - %{"success" => "Secret created successfully"} = assert_redirect(view, "/hub/#{hub.id}") + + %{"success" => "Secret TEAM_ADD_SECRET created successfully"} = + assert_redirect(view, "/hub/#{hub.id}") {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") @@ -144,7 +146,9 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do updated_secret = %{secret | value: new_value} assert_receive {:secret_updated, ^updated_secret} - %{"success" => "Secret updated successfully"} = assert_redirect(view, "/hub/#{hub.id}") + + %{"success" => "Secret TEAM_EDIT_SECRET updated successfully"} = + assert_redirect(view, "/hub/#{hub.id}") {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") assert render(element(view, "#hub-secrets-list")) =~ secret.name @@ -168,7 +172,9 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do render_confirm(view) assert_receive {:secret_deleted, ^secret} - %{"success" => "Secret deleted successfully"} = assert_redirect(view, "/hub/#{hub.id}") + + %{"success" => "Secret TEAM_DELETE_SECRET deleted successfully"} = + assert_redirect(view, "/hub/#{hub.id}") {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") refute render(element(view, "#hub-secrets-list")) =~ secret.name diff --git a/test/livebook_web/live/hub/edit_live_test.exs b/test/livebook_web/live/hub/edit_live_test.exs index 162fa8d46..5e3d91e19 100644 --- a/test/livebook_web/live/hub/edit_live_test.exs +++ b/test/livebook_web/live/hub/edit_live_test.exs @@ -81,7 +81,9 @@ defmodule LivebookWeb.Hub.EditLiveTest do |> render_submit(attrs) assert_receive {:secret_created, ^secret} - %{"success" => "Secret created successfully"} = assert_redirect(view, "/hub/#{hub.id}") + + %{"success" => "Secret PERSONAL_ADD_SECRET created successfully"} = + assert_redirect(view, "/hub/#{hub.id}") {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") @@ -126,7 +128,9 @@ defmodule LivebookWeb.Hub.EditLiveTest do updated_secret = %{secret | value: new_value} assert_receive {:secret_updated, ^updated_secret} - %{"success" => "Secret updated successfully"} = assert_redirect(view, "/hub/#{hub.id}") + + %{"success" => "Secret PERSONAL_EDIT_SECRET updated successfully"} = + assert_redirect(view, "/hub/#{hub.id}") {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") assert render(element(view, "#hub-secrets-list")) =~ secret.name @@ -149,7 +153,9 @@ defmodule LivebookWeb.Hub.EditLiveTest do render_confirm(view) assert_receive {:secret_deleted, ^secret} - %{"success" => "Secret deleted successfully"} = assert_redirect(view, "/hub/#{hub.id}") + + %{"success" => "Secret PERSONAL_DELETE_SECRET deleted successfully"} = + assert_redirect(view, "/hub/#{hub.id}") {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") refute render(element(view, "#hub-secrets-list")) =~ secret.name