mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-02-25 07:16:45 +08:00
349 lines
11 KiB
Elixir
349 lines
11 KiB
Elixir
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: "Hub - 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> amplifies Livebook with features designed for teams and businesses. 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
|
|
id={"#{@selected_option}-form"}
|
|
class="flex flex-col space-y-4"
|
|
for={@form}
|
|
phx-submit="save"
|
|
phx-change="validate"
|
|
>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<.text_field field={@form[:name]} label="Organization name" />
|
|
<.emoji_field field={@form[:emoji]} label="Emoji" />
|
|
</div>
|
|
|
|
<.password_field
|
|
:if={@selected_option == "join-org"}
|
|
field={@form[: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} ->
|
|
changeset = Map.replace!(changeset, :action, :validate)
|
|
|
|
{: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, "Hub 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
|