Improves disconnected status on Hub's page (#2127)

This commit is contained in:
Alexandre de Souza 2023-08-01 14:27:44 -03:00 committed by GitHub
parent 7c40ab22e3
commit fb578906ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 429 additions and 362 deletions

View file

@ -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

View file

@ -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

View file

@ -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.
"""

View file

@ -157,7 +157,7 @@ defmodule LivebookWeb.HomeLive do
defp update_notification(assigns) do
~H"""
<div class="px-2 py-2 bg-blue-200 text-gray-900 text-sm text-center">
<LayoutHelpers.topbar>
<span>
Livebook v<%= @version %> available!
<%= if @instructions_url do %>
@ -189,16 +189,13 @@ defmodule LivebookWeb.HomeLive do
<% end %>
🚀
</span>
</div>
</LayoutHelpers.topbar>
"""
end
defp memory_notification(assigns) do
~H"""
<div
:if={@app_service_url && @memory.free < 30_000_000}
class="px-2 py-2 bg-red-200 text-gray-900 text-sm text-center"
>
<LayoutHelpers.topbar :if={@app_service_url && @memory.free < 30_000_000} variant={:error}>
<.remix_icon icon="alarm-warning-line" class="align-text-bottom mr-0.5" />
Less than 30 MB of memory left, consider
<a
@ -215,7 +212,7 @@ defmodule LivebookWeb.HomeLive do
>
running sessions
</a>
</div>
</LayoutHelpers.topbar>
"""
end

View file

@ -32,141 +32,143 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
@impl true
def render(assigns) do
~H"""
<div id={"#{@id}-component"}>
<div class="mb-8">
<div class="space-y-8">
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
<div class="p-4 md:px-12 md:py-7 max-w-screen-md mx-auto">
<div id={"#{@id}-component"}>
<div class="mb-8">
<div class="space-y-8">
<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>
<p class="text-gray-700">
Your personal hub. All data is stored on your machine and only you can access it.
</p>
<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>
<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..."
disabled={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>
<.live_component
module={LivebookWeb.Hub.SecretListComponent}
id="hub-secrets-list"
hub={@hub}
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" />
<.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 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>
<button
class="button-base button-blue"
type="submit"
phx-disable-with="Updating..."
disabled={not @changeset.valid?}
>
Save
</button>
</div>
</div>
<div>
<button
class="button-base button-blue"
type="submit"
phx-disable-with="Updating..."
disabled={not @stamp_changeset.valid?}
>
Save
</button>
</div>
</.form>
</.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>
<.live_component
module={LivebookWeb.Hub.SecretListComponent}
id="hub-secrets-list"
hub={@hub}
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..."
disabled={not @stamp_changeset.valid?}
>
Save
</button>
</div>
</.form>
</div>
</div>
</div>
</div>
</div>
<.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>
<.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>
</div>
</div>
"""
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,

View file

@ -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"""
<div id={"#{@id}-component"}>
<div class="mb-8 flex flex-col space-y-8">
<div class="flex justify-between">
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
<div>
<LayoutHelpers.topbar :if={not @hub_metadata.connected?} variant={:warning}>
<%= Provider.connection_error(@hub) %>
</LayoutHelpers.topbar>
<div class="flex justify-end gap-2">
<button
phx-click={show_modal("show-key-modal")}
phx-target={@myself}
class="button-base button-outlined-gray"
>
<span class="hidden sm:block">Teams key</span>
<.remix_icon icon="key-2-fill" class="text-xl sm:hidden" />
</button>
</div>
</div>
<div>
<p class="text-gray-700">
A shared Teams hub. All resources here are shared with your team. Manage users and billing on livebook.dev.
</p>
</div>
<div class="p-4 md:px-12 md:py-7 max-w-screen-md mx-auto">
<div id={"#{@id}-component"}>
<div class="mb-8 flex flex-col space-y-8">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<LayoutHelpers.title>
<div class="flex gap-2">
<div class="flex justify-center">
<span class="relative">
<%= @hub.hub_emoji %>
<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>
<div class={[
"absolute w-[10px] h-[10px] border-gray-900 border-2 rounded-full right-0 bottom-1",
if(@hub_metadata.connected?, do: "bg-green-400", else: "bg-red-400")
]} />
</span>
</div>
<%= @hub.hub_name %>
</div>
</LayoutHelpers.title>
<.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}
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-2">
<.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" />
<div class="flex justify-end gap-2">
<button
phx-click={show_modal("show-key-modal")}
phx-target={@myself}
class="button-base button-outlined-gray"
>
<span class="hidden sm:block">Teams key</span>
<.remix_icon icon="key-2-fill" class="text-xl sm:hidden" />
</button>
</div>
</div>
</div>
<div>
<button class="button-base button-blue" type="submit" phx-disable-with="Updating...">
Update Hub
</button>
</div>
</.form>
</div>
<div>
<p class="text-gray-700">
A shared Teams hub. All resources here are shared with your team. Manage users and billing on livebook.dev.
</p>
</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>
<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>
<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>
<.live_component
module={LivebookWeb.Hub.SecretListComponent}
id="hub-secrets-list"
hub={@hub}
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">
Airgapped Deployment
</h2>
<p class="text-gray-700">
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.
</p>
<div id="env-code">
<div class="flex justify-between items-end mb-1">
<span class="text-sm text-gray-700 font-semibold"> Dockerfile </span>
<button
class="button-base button-gray whitespace-nowrap py-1 px-2"
data-copy
data-tooltip="Copied to clipboard"
type="button"
aria-label="copy to clipboard"
phx-click={
JS.dispatch("lb:clipcopy", to: "#offline-deployment-#{@hub.id}-source")
|> JS.add_class(
"tooltip top",
to: "#env-code [data-copy]",
transition: {"ease-out duration-200", "opacity-0", "opacity-100"}
)
|> JS.remove_class(
"tooltip top",
to: "#env-code [data-copy]",
transition: {"ease-out duration-200", "opacity-0", "opacity-100"},
time: 2000
)
}
<.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}
>
<.remix_icon icon="clipboard-line" class="align-middle mr-1 text-xs" />
<span class="font-normal text-xs">Copy source</span>
</button>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-2">
<.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" />
</div>
<div>
<button
class="button-base button-blue"
type="submit"
phx-disable-with="Updating..."
>
Update Hub
</button>
</div>
</.form>
</div>
<.code_preview
source_id={"offline-deployment-#{@hub.id}-source"}
source={@dockerfile}
language="dockerfile"
/>
<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>
<ol class="text-gray-700 mt-4 space-y-2 list-disc list-inside">
<li>
You must change <code>/path/to/my/notebooks</code> in the template above
to point to a directory with the `.livemd` files you want to deploy
</li>
<li>
You must set the <code>LIVEBOOK_TEAMS_KEY</code> environment variable
directly on your deployment platform, with the value you can find at the
top of this page
</li>
<li>
You may set the <code>LIVEBOOK_PASSWORD</code> environment variable to any
value of your choice, if you want to access and debug your deployed notebooks
in production
</li>
</ol>
<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>
<.live_component
module={LivebookWeb.Hub.SecretListComponent}
id="hub-secrets-list"
hub={@hub}
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">
Airgapped Deployment
</h2>
<p class="text-gray-700">
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.
</p>
<div id="env-code">
<div class="flex justify-between items-end mb-1">
<span class="text-sm text-gray-700 font-semibold"> Dockerfile </span>
<button
class="button-base button-gray whitespace-nowrap py-1 px-2"
data-copy
data-tooltip="Copied to clipboard"
type="button"
aria-label="copy to clipboard"
phx-click={
JS.dispatch("lb:clipcopy", to: "#offline-deployment-#{@hub.id}-source")
|> JS.add_class(
"tooltip top",
to: "#env-code [data-copy]",
transition: {"ease-out duration-200", "opacity-0", "opacity-100"}
)
|> JS.remove_class(
"tooltip top",
to: "#env-code [data-copy]",
transition: {"ease-out duration-200", "opacity-0", "opacity-100"},
time: 2000
)
}
>
<.remix_icon icon="clipboard-line" class="align-middle mr-1 text-xs" />
<span class="font-normal text-xs">Copy source</span>
</button>
</div>
<.code_preview
source_id={"offline-deployment-#{@hub.id}-source"}
source={@dockerfile}
language="dockerfile"
/>
<ol class="text-gray-700 mt-4 space-y-2 list-disc list-inside">
<li>
You must change <code>/path/to/my/notebooks</code> in the template above
to point to a directory with the `.livemd` files you want to deploy
</li>
<li>
You must set the <code>LIVEBOOK_TEAMS_KEY</code> environment variable
directly on your deployment platform, with the value you can find at the
top of this page
</li>
<li>
You may set the <code>LIVEBOOK_PASSWORD</code> environment variable to any
value of your choice, if you want to access and debug your deployed notebooks
in production
</li>
</ol>
</div>
</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">
Danger Zone
</h2>
<div class="flex items-center justify-between gap-4 text-gray-700">
<div class="flex flex-col">
<h3 class="font-semibold">
Delete this hub
</h3>
<p>Once deleted, you wont be able to access its features unless you rejoin.</p>
</div>
<button
id="delete-hub"
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
class="button-base button-outlined-red"
>
<span class="hidden sm:block">Delete hub</span>
<.remix_icon icon="delete-bin-line" class="text-lg sm:hidden" />
</button>
</div>
</div>
</div>
</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">
Danger Zone
</h2>
<.modal show={@show_key} id="show-key-modal" width={:medium} patch={~p"/hub/#{@hub.id}"}>
<.teams_key_modal teams_key={@hub.teams_key} />
</.modal>
<div class="flex items-center justify-between gap-4 text-gray-700">
<div class="flex flex-col">
<h3 class="font-semibold">
Delete this hub
</h3>
<p>Once deleted, you wont be able to access its features unless you rejoin.</p>
</div>
<button
id="delete-hub"
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
class="button-base button-outlined-red"
>
<span class="hidden sm:block">Delete hub</span>
<.remix_icon icon="delete-bin-line" class="text-lg sm:hidden" />
</button>
</div>
</div>
<.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>
</div>
</div>
<.modal show={@show_key} id="show-key-modal" width={:medium} patch={~p"/hub/#{@hub.id}"}>
<.teams_key_modal teams_key={@hub.teams_key} />
</.modal>
<.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>
</div>
"""
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

View file

@ -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}
>
<div class="p-4 md:px-12 md:py-7 max-w-screen-md mx-auto">
<.hub_component
type={@type}
hub={@hub}
live_action={@live_action}
params={@params}
counter={@counter}
/>
</div>
<.hub_component
type={@type}
hub={@hub}
live_action={@live_action}
params={@params}
counter={@counter}
/>
</LayoutHelpers.layout>
"""
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

View file

@ -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

View file

@ -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"""
<div class="relative">
<div
@ -274,9 +280,31 @@ defmodule LivebookWeb.LayoutHelpers do
</.link>
</div>
<h1 class="text-2xl text-gray-800 font-medium">
<%= @text %>
<%= if @inner_block != [] do %>
<%= render_slot(@inner_block) %>
<% else %>
<%= @text %>
<% end %>
</h1>
</div>
"""
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"""
<div class={["px-2 py-2 text-sm text-center", topbar_class(@variant)]}>
<%= render_slot(@inner_block) %>
</div>
"""
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

View file

@ -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

View file

@ -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