mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-16 21:28:03 +08:00
Add Offline Deployment section on the Hub Teams page (#2086)
This commit is contained in:
parent
5a79137390
commit
07caffaecb
8 changed files with 143 additions and 86 deletions
|
|
@ -94,6 +94,7 @@ import "monaco-editor/esm/vs/basic-languages/sql/sql.contribution";
|
||||||
import "monaco-editor/esm/vs/basic-languages/css/css.contribution";
|
import "monaco-editor/esm/vs/basic-languages/css/css.contribution";
|
||||||
import "monaco-editor/esm/vs/basic-languages/html/html.contribution";
|
import "monaco-editor/esm/vs/basic-languages/html/html.contribution";
|
||||||
import "monaco-editor/esm/vs/basic-languages/xml/xml.contribution";
|
import "monaco-editor/esm/vs/basic-languages/xml/xml.contribution";
|
||||||
|
import "monaco-editor/esm/vs/basic-languages/dockerfile/dockerfile.contribution";
|
||||||
|
|
||||||
// === Configuration ===
|
// === Configuration ===
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ config :mime, :types, %{
|
||||||
config :plug_cowboy, :log_exceptions_with_status_code, [407..599]
|
config :plug_cowboy, :log_exceptions_with_status_code, [407..599]
|
||||||
|
|
||||||
config :livebook,
|
config :livebook,
|
||||||
teams_url: nil,
|
teams_url: "http://localhost:4100",
|
||||||
app_service_name: nil,
|
app_service_name: nil,
|
||||||
app_service_url: nil,
|
app_service_url: nil,
|
||||||
authentication_mode: :token,
|
authentication_mode: :token,
|
||||||
|
|
|
||||||
|
|
@ -207,10 +207,15 @@ defmodule Livebook.Application do
|
||||||
|
|
||||||
def create_offline_hub() do
|
def create_offline_hub() do
|
||||||
name = System.get_env("LIVEBOOK_TEAMS_NAME")
|
name = System.get_env("LIVEBOOK_TEAMS_NAME")
|
||||||
teams_key = System.get_env("LIVEBOOK_TEAMS_KEY")
|
|
||||||
public_key = System.get_env("LIVEBOOK_TEAMS_OFFLINE_KEY")
|
public_key = System.get_env("LIVEBOOK_TEAMS_OFFLINE_KEY")
|
||||||
|
|
||||||
if name && teams_key && public_key do
|
if name && public_key do
|
||||||
|
teams_key =
|
||||||
|
System.get_env("LIVEBOOK_TEAMS_KEY") ||
|
||||||
|
Livebook.Config.abort!(
|
||||||
|
"You specified LIVEBOOK_TEAMS_NAME, but LIVEBOOK_TEAMS_KEY is missing."
|
||||||
|
)
|
||||||
|
|
||||||
Livebook.Hubs.set_offline_hub(%Livebook.Hubs.Team{
|
Livebook.Hubs.set_offline_hub(%Livebook.Hubs.Team{
|
||||||
id: "team-#{name}",
|
id: "team-#{name}",
|
||||||
hub_name: name,
|
hub_name: name,
|
||||||
|
|
|
||||||
4
lib/livebook_web/errors/not_found_error.ex
Normal file
4
lib/livebook_web/errors/not_found_error.ex
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule LivebookWeb.NotFoundError do
|
||||||
|
@moduledoc false
|
||||||
|
defexception [:message, plug_status: 404]
|
||||||
|
end
|
||||||
|
|
@ -4,11 +4,7 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do
|
||||||
alias Livebook.Hubs
|
alias Livebook.Hubs
|
||||||
alias Livebook.Hubs.Personal
|
alias Livebook.Hubs.Personal
|
||||||
alias LivebookWeb.LayoutHelpers
|
alias LivebookWeb.LayoutHelpers
|
||||||
|
alias LivebookWeb.NotFoundError
|
||||||
defmodule NotFoundError do
|
|
||||||
@moduledoc false
|
|
||||||
defexception [:message, plug_status: 404]
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
||||||
alias Livebook.Hubs.Team
|
alias Livebook.Hubs.Team
|
||||||
alias Livebook.Teams
|
alias Livebook.Teams
|
||||||
alias LivebookWeb.LayoutHelpers
|
alias LivebookWeb.LayoutHelpers
|
||||||
|
alias LivebookWeb.NotFoundError
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(assigns, socket) do
|
||||||
|
|
@ -15,7 +16,8 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
||||||
|
|
||||||
secret_value =
|
secret_value =
|
||||||
if assigns.live_action == :edit_secret do
|
if assigns.live_action == :edit_secret do
|
||||||
Enum.find_value(secrets, &(&1.name == secret_name && &1.value))
|
Enum.find_value(secrets, &(&1.name == secret_name and &1.value)) ||
|
||||||
|
raise(NotFoundError, "could not find secret matching #{inspect(secret_name)}")
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
|
|
@ -26,6 +28,7 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
||||||
secret_name: secret_name,
|
secret_name: secret_name,
|
||||||
secret_value: secret_value
|
secret_value: secret_value
|
||||||
)
|
)
|
||||||
|
|> assign_dockerfile()
|
||||||
|> assign_form(changeset)}
|
|> assign_form(changeset)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -101,86 +104,29 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
||||||
target={@myself}
|
target={@myself}
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
|
Offline Deployment
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Deploy your stamped notebooks with your Hub
|
||||||
|
using an instance of the Hub using
|
||||||
|
environment variables.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<.code_preview
|
||||||
|
source_id={"offline-deployment-#{@hub.id}"}
|
||||||
|
source={@dockerfile}
|
||||||
|
language="dockerfile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.modal show={@show_key} id="show-key-modal" width={:medium} patch={~p"/hub/#{@hub.id}"}>
|
<.modal show={@show_key} id="show-key-modal" width={:medium} patch={~p"/hub/#{@hub.id}"}>
|
||||||
<div class="p-6 flex flex-col space-y-5">
|
<.teams_key_modal teams_key={@hub.teams_key} />
|
||||||
<h3 class="text-2xl font-semibold text-gray-800">
|
|
||||||
Teams Key
|
|
||||||
</h3>
|
|
||||||
<div class="justify-center">
|
|
||||||
This is your <strong>Teams Key</strong>. If you want to join or invite others
|
|
||||||
to your organization, you will need to share your Teams Key with them. We
|
|
||||||
recommend storing it somewhere safe:
|
|
||||||
</div>
|
|
||||||
<div class=" w-full">
|
|
||||||
<div id="teams-key-toggle" class="relative flex">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="teams-key"
|
|
||||||
readonly
|
|
||||||
value={@hub.teams_key}
|
|
||||||
class="input font-mono w-full border-neutral-200 bg-neutral-100 py-2 border-2 pr-8"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex items-center absolute inset-y-0 right-1">
|
|
||||||
<button
|
|
||||||
class="icon-button"
|
|
||||||
data-copy
|
|
||||||
data-tooltip="Copied to clipboard"
|
|
||||||
type="button"
|
|
||||||
aria-label="copy to clipboard"
|
|
||||||
phx-click={
|
|
||||||
JS.dispatch("lb:clipcopy", to: "#teams-key")
|
|
||||||
|> JS.add_class(
|
|
||||||
"tooltip top",
|
|
||||||
to: "#teams-key-toggle [data-copy]",
|
|
||||||
transition: {"ease-out duration-200", "opacity-0", "opacity-100"}
|
|
||||||
)
|
|
||||||
|> JS.remove_class(
|
|
||||||
"tooltip top",
|
|
||||||
to: "#teams-key-toggle [data-copy]",
|
|
||||||
transition: {"ease-out duration-200", "opacity-0", "opacity-100"},
|
|
||||||
time: 2000
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<.remix_icon icon="clipboard-line" class="text-xl" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="icon-button"
|
|
||||||
data-show
|
|
||||||
type="button"
|
|
||||||
aria-label="show password"
|
|
||||||
phx-click={
|
|
||||||
JS.remove_attribute("type", to: "#teams-key-toggle input")
|
|
||||||
|> JS.set_attribute({"type", "text"}, to: "#teams-key-toggle input")
|
|
||||||
|> toggle_class("hidden", to: "#teams-key-toggle [data-show]")
|
|
||||||
|> toggle_class("hidden", to: "#teams-key-toggle [data-hide]")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<.remix_icon icon="eye-line" class="text-xl" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="icon-button hidden"
|
|
||||||
data-hide
|
|
||||||
type="button"
|
|
||||||
aria-label="hide password"
|
|
||||||
phx-click={
|
|
||||||
JS.remove_attribute("type", to: "#teams-key-toggle input")
|
|
||||||
|> JS.set_attribute({"type", "password"}, to: "#teams-key-toggle input")
|
|
||||||
|> toggle_class("hidden", to: "#teams-key-toggle [data-show]")
|
|
||||||
|> toggle_class("hidden", to: "#teams-key-toggle [data-hide]")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<.remix_icon icon="eye-off-line" class="text-xl" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.modal>
|
</.modal>
|
||||||
|
|
||||||
<.modal
|
<.modal
|
||||||
|
|
@ -203,6 +149,87 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp teams_key_modal(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="p-6 flex flex-col space-y-5">
|
||||||
|
<h3 class="text-2xl font-semibold text-gray-800">
|
||||||
|
Teams Key
|
||||||
|
</h3>
|
||||||
|
<div class="justify-center">
|
||||||
|
This is your <strong>Teams Key</strong>. If you want to join or invite others
|
||||||
|
to your organization, you will need to share your Teams Key with them. We
|
||||||
|
recommend storing it somewhere safe:
|
||||||
|
</div>
|
||||||
|
<div class=" w-full">
|
||||||
|
<div id="teams-key-toggle" class="relative flex">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="teams-key"
|
||||||
|
readonly
|
||||||
|
value={@teams_key}
|
||||||
|
class="input font-mono w-full border-neutral-200 bg-neutral-100 py-2 border-2 pr-8"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center absolute inset-y-0 right-1">
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
data-copy
|
||||||
|
data-tooltip="Copied to clipboard"
|
||||||
|
type="button"
|
||||||
|
aria-label="copy to clipboard"
|
||||||
|
phx-click={
|
||||||
|
JS.dispatch("lb:clipcopy", to: "#teams-key")
|
||||||
|
|> JS.add_class(
|
||||||
|
"tooltip top",
|
||||||
|
to: "#teams-key-toggle [data-copy]",
|
||||||
|
transition: {"ease-out duration-200", "opacity-0", "opacity-100"}
|
||||||
|
)
|
||||||
|
|> JS.remove_class(
|
||||||
|
"tooltip top",
|
||||||
|
to: "#teams-key-toggle [data-copy]",
|
||||||
|
transition: {"ease-out duration-200", "opacity-0", "opacity-100"},
|
||||||
|
time: 2000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<.remix_icon icon="clipboard-line" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
data-show
|
||||||
|
type="button"
|
||||||
|
aria-label="show password"
|
||||||
|
phx-click={
|
||||||
|
JS.remove_attribute("type", to: "#teams-key-toggle input")
|
||||||
|
|> JS.set_attribute({"type", "text"}, to: "#teams-key-toggle input")
|
||||||
|
|> toggle_class("hidden", to: "#teams-key-toggle [data-show]")
|
||||||
|
|> toggle_class("hidden", to: "#teams-key-toggle [data-hide]")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<.remix_icon icon="eye-line" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="icon-button hidden"
|
||||||
|
data-hide
|
||||||
|
type="button"
|
||||||
|
aria-label="hide password"
|
||||||
|
phx-click={
|
||||||
|
JS.remove_attribute("type", to: "#teams-key-toggle input")
|
||||||
|
|> JS.set_attribute({"type", "password"}, to: "#teams-key-toggle input")
|
||||||
|
|> toggle_class("hidden", to: "#teams-key-toggle [data-show]")
|
||||||
|
|> toggle_class("hidden", to: "#teams-key-toggle [data-hide]")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<.remix_icon icon="eye-off-line" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("save", %{"team" => params}, socket) do
|
def handle_event("save", %{"team" => params}, socket) do
|
||||||
case Teams.update_hub(socket.assigns.hub, params) do
|
case Teams.update_hub(socket.assigns.hub, params) do
|
||||||
|
|
@ -255,4 +282,22 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
||||||
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
||||||
assign(socket, form: to_form(changeset))
|
assign(socket, form: to_form(changeset))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assign_dockerfile(socket) do
|
||||||
|
version = to_string(Application.spec(:livebook, :vsn))
|
||||||
|
version = if version =~ "dev", do: "edge", else: version
|
||||||
|
|
||||||
|
assign(socket, :dockerfile, """
|
||||||
|
FROM livebook/livebook:#{version}
|
||||||
|
|
||||||
|
COPY /path/to/my/notebooks /data
|
||||||
|
ENV LIVEBOOK_APPS_PATH "/data"
|
||||||
|
|
||||||
|
ENV LIVEBOOK_APPS_PATH_HUB_ID "#{socket.assigns.hub.id}"
|
||||||
|
ENV LIVEBOOK_TEAMS_NAME "#{socket.assigns.hub.hub_name}"
|
||||||
|
ENV LIVEBOOK_TEAMS_OFFLINE_KEY "#{socket.assigns.hub.org_public_key}"
|
||||||
|
|
||||||
|
CMD [ "/app/bin/livebook", "start" ]\
|
||||||
|
""")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -175,5 +175,11 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do
|
||||||
refute render(element(view, "#hub-secrets-list")) =~ secret.name
|
refute render(element(view, "#hub-secrets-list")) =~ secret.name
|
||||||
refute secret in Livebook.Hubs.get_secrets(hub)
|
refute secret in Livebook.Hubs.get_secrets(hub)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "raises an error if does not exist secret", %{conn: conn, hub: hub} do
|
||||||
|
assert_raise LivebookWeb.NotFoundError, fn ->
|
||||||
|
live(conn, ~p"/hub/#{hub.id}/secrets/edit/HELLO")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "raises an error if does not exist secret", %{conn: conn, hub: hub} do
|
test "raises an error if does not exist secret", %{conn: conn, hub: hub} do
|
||||||
assert_raise LivebookWeb.Hub.Edit.PersonalComponent.NotFoundError, fn ->
|
assert_raise LivebookWeb.NotFoundError, fn ->
|
||||||
live(conn, ~p"/hub/#{hub.id}/secrets/edit/HELLO")
|
live(conn, ~p"/hub/#{hub.id}/secrets/edit/HELLO")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue