Packages
livebook
0.15.0
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/hub/new_live.ex
defmodule LivebookWeb.Hub.NewLive do
use LivebookWeb, :live_view
alias Livebook.Teams
alias Livebook.Teams.Org
alias LivebookWeb.LayoutComponents
on_mount LivebookWeb.SidebarHook
@check_completion_data_interval Application.compile_env(
:livebook,
:check_completion_data_interval,
3000
)
@impl true
def mount(_params, _session, socket) do
socket =
assign(socket,
selected_option: "new-org",
page_title: "Workspace - Livebook",
requested_code: false,
org: nil,
verification_uri: nil,
form: nil,
button_label: nil,
request_code_info: nil
)
socket = assign_form(socket, "new-org")
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<LayoutComponents.layout current_page="/hub" current_user={@current_user} saved_hubs={@saved_hubs}>
<LayoutComponents.topbar :if={Livebook.Config.warn_on_live_teams_server?()} variant="warning">
<strong>Beware!</strong>
You are running Livebook in development but this page communicates with production servers.
</LayoutComponents.topbar>
<div class="flex flex-col p-4 md:px-12 md:py-7 max-w-screen-md mx-auto space-y-8">
<div>
<LayoutComponents.title text="Add organization" />
<p class="mt-4 text-gray-700">
<a
class="font-medium underline text-gray-900 hover:no-underline"
href="https://livebook.dev/teams?ref=LivebookApp"
target="_blank"
phx-no-format
>
Livebook Teams</a> enables you to deploy internal tools built with Elixir and Livebook to your own infrastructure. It is currently in closed beta.
</p>
<p class="mt-4 text-gray-700">
To create a Teams organization, you must <a
class="font-medium underline text-gray-900 hover:no-underline"
href="https://livebook.dev/teams?ref=LivebookApp"
target="_blank"
>join the beta for free early access</a>.
</p>
</div>
<!-- TABS -->
<div class="flex flex-col space-y-4">
<div class="flex flex-col justify-center sm:items-center sm:m-auto">
<div class="flex rounded-xl bg-gray-100 p-1">
<ul class="flex flex-col sm:flex-row md:flex-col lg:flex-row w-full list-none gap-1">
<!-- New Org -->
<.tab_button
id="new-org"
selected={@selected_option}
title="Create a new organization"
icon="lightbulb-flash-line"
/>
<!-- Join Org -->
<.tab_button
id="join-org"
selected={@selected_option}
title="Join an existing organization"
icon="organization-chart"
/>
</ul>
</div>
</div>
</div>
<!-- FORMS -->
<div :if={@selected_option} class="flex flex-col space-y-4">
<.form
:let={f}
for={@form}
id={"#{@selected_option}-form"}
class="flex flex-col space-y-4"
phx-submit="save"
phx-change="validate"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<.text_field field={f[:name]} label="Organization name" autofocus />
<.emoji_field field={f[:emoji]} label="Emoji" />
</div>
<.password_field
:if={@selected_option == "join-org"}
field={f[:teams_key]}
label="Livebook Teams key"
/>
<div>
<.button :if={!@requested_code} phx-disable-with="Loading...">
{@button_label}
</.button>
</div>
<div class="invisible"></div>
<div :if={@requested_code} class="flex flex-col rounded-xl bg-gray-50 px-10 py-6 mt-10">
<div class="flex flex-col items-center rounded-xl bg-gray-50">
<span class="text-base font-semibold text-center text-gray-900">
{@request_code_info}
</span>
<div class="text-center mt-4 text-gray-700">
<span class="text-sm">
1. Copy the code:
</span>
<div class="mt-3 text-center">
<.copyclip content={@org.user_code} />
</div>
</div>
<div class="text-center mt-4 text-gray-700">
<span class="text-sm">
2. Sign in to Livebook Teams and paste the code:
</span>
<div class="mt-2">
<.button color="gray" outlined href={@verification_uri} target="_blank">
Go to Teams
</.button>
</div>
</div>
</div>
</div>
</.form>
</div>
</div>
</LayoutComponents.layout>
"""
end
defp copyclip(assigns) do
~H"""
<div
id="clipboard"
class="flex items-center justify-between border rounded-lg px-4 py-2.5 bg-white"
>
<.icon_button class="invisible">
<.remix_icon icon="clipboard-line" />
</.icon_button>
<div
class="mr-4 text-brand-pink font-semibold text-xl leading-none"
id="clipboard-code"
phx-no-format
>{@content}</div>
<.icon_button phx-click={JS.dispatch("lb:clipcopy", to: "#clipboard-code")} type="button">
<.remix_icon icon="clipboard-line" />
</.icon_button>
</div>
"""
end
defp tab_button(assigns) do
~H"""
<li class="group/toggle w-full">
<button
type="button"
id={@id}
aria-haspopup="menu"
aria-expanded="false"
data-state="closed"
class="w-full"
phx-click="select_option"
phx-value-option={@id}
>
<div class={[
"group button flex w-full sm:w-72 items-center justify-center gap-1 md:gap-2 rounded-lg border py-3 md:py-2.5 px-5 transition-opacity duration-100",
selected_tab_button(@id, @selected)
]}>
<.remix_icon
icon={@icon}
class={[
"group-hover:text-blue-600 text-lg",
if @selected == @id do
"text-blue-600"
else
"text-gray-500"
end
]}
/>
<span class="truncate text-sm font-medium">
{@title}
</span>
</div>
</button>
</li>
"""
end
defp selected_tab_button(id, id),
do: "border-black/10 bg-white drop-shadow-sm hover:!opacity-100"
defp selected_tab_button(_, _), do: "border-transparent text-gray-500 hover:text-gray-800"
@impl true
def handle_event("select_option", %{"option" => option}, socket) do
{:noreply,
socket
|> assign(selected_option: option, requested_code: false, verification_uri: nil)
|> assign_form(option)}
end
def handle_event("validate", %{"org" => attrs}, socket) do
changeset =
socket.assigns.org
|> Teams.change_org(attrs)
|> Map.replace!(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
def handle_event("save", %{"org" => attrs}, socket) do
result =
case socket.assigns.selected_option do
"new-org" -> Teams.create_org(socket.assigns.org, attrs)
"join-org" -> Teams.join_org(socket.assigns.org, attrs)
end
case result do
{:ok, %{"device_code" => device_code} = response} ->
attrs = Map.merge(attrs, response)
changeset = Teams.change_org(socket.assigns.org, attrs)
org = Ecto.Changeset.apply_action!(changeset, :insert)
Process.send_after(
self(),
{:check_completion_data, device_code},
@check_completion_data_interval
)
{:noreply,
socket
|> assign(requested_code: true, org: org, verification_uri: response["verification_uri"])
|> assign_form(changeset)}
{:error, changeset} ->
{:noreply, assign_form(socket, changeset)}
{:transport_error, message} ->
{:noreply, put_flash(socket, :error, message)}
end
end
@impl true
def handle_info({:check_completion_data, device_code}, %{assigns: %{org: org}} = socket) do
case Teams.get_org_request_completion_data(org, device_code) do
{:ok, :awaiting_confirmation} ->
Process.send_after(
self(),
{:check_completion_data, device_code},
@check_completion_data_interval
)
{:noreply, socket}
{:ok, %{"id" => _id, "session_token" => _session_token} = response} ->
hub =
Teams.create_hub!(%{
org_id: response["id"],
user_id: response["user_id"],
org_key_id: response["org_key_id"],
org_public_key: response["org_public_key"],
session_token: response["session_token"],
teams_key: org.teams_key,
hub_name: org.name,
hub_emoji: org.emoji
})
{:noreply,
socket
|> put_flash(:success, "Workspace added successfully")
|> push_navigate(to: ~p"/hub/#{hub.id}?show-key=confirm")}
{:error, :expired} ->
changeset =
org
|> Teams.change_org(%{user_code: nil})
|> Map.replace!(:action, :validate)
{:noreply,
socket
|> assign(requested_code: false, org: org, verification_uri: nil)
|> put_flash(:error, "Oh no! Your org request expired, could you please try again?")
|> assign_form(changeset)}
{:transport_error, message} ->
Process.send_after(
self(),
{:check_completion_data, device_code},
@check_completion_data_interval
)
{:noreply, put_flash(socket, :error, message)}
end
end
def handle_info(_any, socket), do: {:noreply, socket}
defp assign_form(socket, "join-org") do
org = %Org{emoji: random_emoji()}
changeset = Teams.change_org(org)
socket
|> assign(
org: org,
button_label: "Join",
request_code_info: "Authenticate with your organization"
)
|> assign_form(changeset)
end
defp assign_form(socket, "new-org") do
org = %Org{emoji: random_emoji(), teams_key: Org.teams_key()}
changeset = Teams.change_org(org)
socket
|> assign(
org: org,
button_label: "Create",
request_code_info: "Verify your new organization"
)
|> assign_form(changeset)
end
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
assign(socket, form: to_form(changeset))
end
defp random_emoji do
Enum.random(~w[💡 🚀 🌈 🦄 🐱 👩💻 ⚽️ ⭐️])
end
end