defmodule LivebookWeb.Hub.NewLive do use LivebookWeb, :live_view alias Livebook.Teams alias Livebook.Teams.Org alias LivebookWeb.LayoutHelpers 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 enabled? = Livebook.Config.feature_flag_enabled?(:create_hub) socket = assign(socket, selected_option: "new-org", page_title: "Hub - Livebook", enabled?: enabled?, 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(%{enabled?: false} = assigns) do ~H"""

Deploy applications, share secrets, templates, and more with Livebook Hubs.

Each Livebook user has their own personal Hub and soon they will be able to deploy their personal notebooks to Fly.io and Hugging Face.

We are also working on Livebook Teams, which were designed from the ground up to deploy notebooks within your organization. Livebook Teams runs on your own infrastructure to provide essential features for secure collaboration between team members, such as digital signing of notebooks, safe sharing of secrets, and more. To learn more, get in touch!

- The Livebook crew

""" end def render(assigns) do ~H"""

Manage your Livebooks in the cloud with Hubs.

    <.tab_button id="new-org" selected={@selected_option} title="Create a new organization" icon="lightbulb-flash-line" /> <.tab_button id="join-org" selected={@selected_option} title="Join an existing organization" icon="organization-chart" />
<.form :let={f} id={"#{@selected_option}-form"} class="flex flex-col space-y-4" for={@form} phx-submit="save" phx-change="validate" >
<.text_field field={f[:name]} label="Name" /> <.emoji_field field={f[:emoji]} label="Emoji" />
<.password_field :if={@selected_option == "join-org"} field={f[:teams_key]} label="Livebook Teams Key" />
<%= @request_code_info %> <.link>
Visit Livebook Teams and paste the code below
<.copyclip content={@org.user_code} />
""" end defp copyclip(assigns) do ~H"""
<%= @content %>
""" end defp tab_button(assigns) do ~H"""
  • """ 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"], session_token: response["session_token"], teams_key: org.teams_key, hub_name: org.name, hub_emoji: org.emoji }) {:noreply, socket |> put_flash(:success, "Hub added successfully") |> push_navigate(to: ~p"/hub/#{hub.id}?show-key=true")} {:error, :expired} -> changeset = Teams.change_org(org, %{user_code: nil}) {: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: "💡"} 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: "⭐️", 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 end