mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 21:14:26 +08:00
Improves disconnected status on Hub's page (#2127)
This commit is contained in:
parent
7c40ab22e3
commit
fb578906ec
11 changed files with 429 additions and 362 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 won’t 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 won’t 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue