mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-12 07:54:49 +08:00
Implement new Add Hub page (#1909)
This commit is contained in:
parent
08199336b3
commit
3a63e30e89
13 changed files with 514 additions and 523 deletions
|
@ -2,7 +2,7 @@ defmodule Livebook.Hubs do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
alias Livebook.Storage
|
alias Livebook.Storage
|
||||||
alias Livebook.Hubs.{Broadcasts, Enterprise, Fly, Metadata, Personal, Provider}
|
alias Livebook.Hubs.{Broadcasts, Enterprise, Fly, Metadata, Personal, Provider, Team}
|
||||||
alias Livebook.Secrets.Secret
|
alias Livebook.Secrets.Secret
|
||||||
|
|
||||||
@namespace :hubs
|
@namespace :hubs
|
||||||
|
@ -169,6 +169,10 @@ defmodule Livebook.Hubs do
|
||||||
Provider.load(%Personal{}, fields)
|
Provider.load(%Personal{}, fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp to_struct(%{id: "team-" <> _} = fields) do
|
||||||
|
Provider.load(%Team{}, fields)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Connects to the all available and connectable hubs.
|
Connects to the all available and connectable hubs.
|
||||||
|
|
||||||
|
|
|
@ -8,25 +8,31 @@ defmodule Livebook.Hubs.Enterprise do
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
id: String.t() | nil,
|
id: String.t() | nil,
|
||||||
url: String.t() | nil,
|
org_id: pos_integer() | nil,
|
||||||
token: String.t() | nil,
|
user_id: pos_integer() | nil,
|
||||||
external_id: String.t() | nil,
|
org_key_id: pos_integer() | nil,
|
||||||
|
teams_key: String.t() | nil,
|
||||||
|
session_token: String.t() | nil,
|
||||||
hub_name: String.t() | nil,
|
hub_name: String.t() | nil,
|
||||||
hub_emoji: String.t() | nil
|
hub_emoji: String.t() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
field :url, :string
|
field :org_id, :integer
|
||||||
field :token, :string
|
field :user_id, :integer
|
||||||
field :external_id, :string
|
field :org_key_id, :integer
|
||||||
|
field :teams_key, :string
|
||||||
|
field :session_token, :string
|
||||||
field :hub_name, :string
|
field :hub_name, :string
|
||||||
field :hub_emoji, :string
|
field :hub_emoji, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
@fields ~w(
|
@fields ~w(
|
||||||
url
|
org_id
|
||||||
token
|
user_id
|
||||||
external_id
|
org_key_id
|
||||||
|
teams_key
|
||||||
|
session_token
|
||||||
hub_name
|
hub_name
|
||||||
hub_emoji
|
hub_emoji
|
||||||
)a
|
)a
|
||||||
|
@ -63,7 +69,7 @@ defmodule Livebook.Hubs.Enterprise do
|
||||||
if Hubs.hub_exists?(id) do
|
if Hubs.hub_exists?(id) do
|
||||||
{:error,
|
{:error,
|
||||||
changeset
|
changeset
|
||||||
|> add_error(:external_id, "already exists")
|
|> add_error(:hub_name, "already exists")
|
||||||
|> Map.replace!(:action, :validate)}
|
|> Map.replace!(:action, :validate)}
|
||||||
else
|
else
|
||||||
with {:ok, struct} <- apply_action(changeset, :insert) do
|
with {:ok, struct} <- apply_action(changeset, :insert) do
|
||||||
|
@ -92,7 +98,7 @@ defmodule Livebook.Hubs.Enterprise do
|
||||||
else
|
else
|
||||||
{:error,
|
{:error,
|
||||||
changeset
|
changeset
|
||||||
|> add_error(:external_id, "does not exists")
|
|> add_error(:hub_name, "does not exists")
|
||||||
|> Map.replace!(:action, :validate)}
|
|> Map.replace!(:action, :validate)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -105,9 +111,9 @@ defmodule Livebook.Hubs.Enterprise do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_id(changeset) do
|
defp add_id(changeset) do
|
||||||
case get_field(changeset, :external_id) do
|
case get_field(changeset, :hub_name) do
|
||||||
nil -> changeset
|
nil -> changeset
|
||||||
external_id -> put_change(changeset, :id, "enterprise-#{external_id}")
|
hub_name -> put_change(changeset, :id, "enterprise-#{hub_name}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -119,9 +125,11 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
|
||||||
%{
|
%{
|
||||||
enterprise
|
enterprise
|
||||||
| id: fields.id,
|
| id: fields.id,
|
||||||
url: fields.url,
|
session_token: fields.session_token,
|
||||||
token: fields.token,
|
teams_key: fields.teams_key,
|
||||||
external_id: fields.external_id,
|
org_id: fields.org_id,
|
||||||
|
user_id: fields.user_id,
|
||||||
|
org_key_id: fields.org_key_id,
|
||||||
hub_name: fields.hub_name,
|
hub_name: fields.hub_name,
|
||||||
hub_emoji: fields.hub_emoji
|
hub_emoji: fields.hub_emoji
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,9 +82,9 @@ defmodule Livebook.Hubs.EnterpriseClient do
|
||||||
## GenServer callbacks
|
## GenServer callbacks
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(%Enterprise{url: url, token: token} = enterprise) do
|
def init(%Enterprise{} = enterprise) do
|
||||||
headers = [{"X-Auth-Token", token}]
|
# TODO: Make it work with new struct and `Livebook.Teams`
|
||||||
{:ok, pid} = ClientConnection.start_link(self(), url, headers)
|
{:ok, pid} = ClientConnection.start_link(self(), Livebook.Config.teams_url())
|
||||||
|
|
||||||
{:ok, %__MODULE__{hub: enterprise, server: pid}}
|
{:ok, %__MODULE__{hub: enterprise, server: pid}}
|
||||||
end
|
end
|
||||||
|
|
110
lib/livebook/hubs/team.ex
Normal file
110
lib/livebook/hubs/team.ex
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
defmodule Livebook.Hubs.Team do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
id: String.t() | nil,
|
||||||
|
org_id: pos_integer() | nil,
|
||||||
|
user_id: pos_integer() | nil,
|
||||||
|
org_key_id: pos_integer() | nil,
|
||||||
|
teams_key: String.t() | nil,
|
||||||
|
session_token: String.t() | nil,
|
||||||
|
hub_name: String.t() | nil,
|
||||||
|
hub_emoji: String.t() | nil
|
||||||
|
}
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field :org_id, :integer
|
||||||
|
field :user_id, :integer
|
||||||
|
field :org_key_id, :integer
|
||||||
|
field :teams_key, :string
|
||||||
|
field :session_token, :string
|
||||||
|
field :hub_name, :string
|
||||||
|
field :hub_emoji, :string
|
||||||
|
end
|
||||||
|
|
||||||
|
@fields ~w(
|
||||||
|
org_id
|
||||||
|
user_id
|
||||||
|
org_key_id
|
||||||
|
teams_key
|
||||||
|
session_token
|
||||||
|
hub_name
|
||||||
|
hub_emoji
|
||||||
|
)a
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns an `%Ecto.Changeset{}` for tracking hub changes.
|
||||||
|
"""
|
||||||
|
@spec change_hub(t(), map()) :: Ecto.Changeset.t()
|
||||||
|
def change_hub(%__MODULE__{} = team, attrs \\ %{}) do
|
||||||
|
changeset(team, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp changeset(team, attrs) do
|
||||||
|
team
|
||||||
|
|> cast(attrs, @fields)
|
||||||
|
|> validate_required(@fields)
|
||||||
|
|> add_id()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_id(changeset) do
|
||||||
|
if name = get_field(changeset, :hub_name) do
|
||||||
|
change(changeset, %{id: "team-#{name}"})
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
|
||||||
|
def load(team, fields) do
|
||||||
|
%{
|
||||||
|
team
|
||||||
|
| id: fields.id,
|
||||||
|
session_token: fields.session_token,
|
||||||
|
teams_key: fields.teams_key,
|
||||||
|
org_id: fields.org_id,
|
||||||
|
user_id: fields.user_id,
|
||||||
|
org_key_id: fields.org_key_id,
|
||||||
|
hub_name: fields.hub_name,
|
||||||
|
hub_emoji: fields.hub_emoji
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_metadata(team) do
|
||||||
|
%Livebook.Hubs.Metadata{
|
||||||
|
id: team.id,
|
||||||
|
name: team.hub_name,
|
||||||
|
provider: team,
|
||||||
|
emoji: team.hub_emoji,
|
||||||
|
connected?: false
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def type(_team), do: "team"
|
||||||
|
|
||||||
|
def connection_spec(_team), do: nil
|
||||||
|
|
||||||
|
def disconnect(_team), do: raise("not implemented")
|
||||||
|
|
||||||
|
def capabilities(_team), do: []
|
||||||
|
|
||||||
|
def get_secrets(_team), do: []
|
||||||
|
|
||||||
|
def create_secret(_team, _secret), do: :ok
|
||||||
|
|
||||||
|
def update_secret(_team, _secret), do: :ok
|
||||||
|
|
||||||
|
def delete_secret(_team, _secret), do: :ok
|
||||||
|
|
||||||
|
def connection_error(_team), do: raise("not implemented")
|
||||||
|
|
||||||
|
def notebook_stamp(_hub, _notebook_source, _metadata) do
|
||||||
|
:skip
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_notebook_stamp(_hub, _notebook_source, _stamp), do: raise("not implemented")
|
||||||
|
end
|
|
@ -1,8 +1,12 @@
|
||||||
defmodule Livebook.Teams do
|
defmodule Livebook.Teams do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
|
alias Livebook.Hubs
|
||||||
|
alias Livebook.Hubs.Team
|
||||||
alias Livebook.Teams.{Client, Org}
|
alias Livebook.Teams.{Client, Org}
|
||||||
|
|
||||||
|
import Ecto.Changeset, only: [add_error: 3, apply_action: 2, apply_action!: 2, get_field: 2]
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates an Org.
|
Creates an Org.
|
||||||
|
|
||||||
|
@ -16,7 +20,7 @@ defmodule Livebook.Teams do
|
||||||
def create_org(%Org{} = org, attrs) do
|
def create_org(%Org{} = org, attrs) do
|
||||||
changeset = Org.changeset(org, attrs)
|
changeset = Org.changeset(org, attrs)
|
||||||
|
|
||||||
with {:ok, %Org{} = org} <- Ecto.Changeset.apply_action(changeset, :insert),
|
with {:ok, %Org{} = org} <- apply_action(changeset, :insert),
|
||||||
{:ok, response} <- Client.create_org(org) do
|
{:ok, response} <- Client.create_org(org) do
|
||||||
{:ok, response}
|
{:ok, response}
|
||||||
else
|
else
|
||||||
|
@ -60,12 +64,45 @@ defmodule Livebook.Teams do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a Hub.
|
||||||
|
|
||||||
|
It notifies interested processes about hub metadatas data change.
|
||||||
|
"""
|
||||||
|
@spec create_hub!(map()) :: Team.t()
|
||||||
|
def create_hub!(attrs) do
|
||||||
|
changeset = Team.change_hub(%Team{}, attrs)
|
||||||
|
team = apply_action!(changeset, :insert)
|
||||||
|
|
||||||
|
Hubs.save_hub(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates a Hub.
|
||||||
|
|
||||||
|
With success, notifies interested processes about hub metadatas data change.
|
||||||
|
Otherwise, it will return an error tuple with changeset.
|
||||||
|
"""
|
||||||
|
@spec update_hub(Team.t(), map()) :: {:ok, Team.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def update_hub(%Team{} = team, attrs) do
|
||||||
|
changeset = Team.change_hub(team, attrs)
|
||||||
|
id = get_field(changeset, :id)
|
||||||
|
|
||||||
|
if Hubs.hub_exists?(id) do
|
||||||
|
with {:ok, struct} <- apply_action(changeset, :update) do
|
||||||
|
{:ok, Hubs.save_hub(struct)}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:error, add_error(changeset, :hub_name, "does not exists")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp add_org_errors(%Ecto.Changeset{} = changeset, errors_map) do
|
defp add_org_errors(%Ecto.Changeset{} = changeset, errors_map) do
|
||||||
for {key, errors} <- errors_map,
|
for {key, errors} <- errors_map,
|
||||||
field <- String.to_atom(key),
|
field <- String.to_atom(key),
|
||||||
field in Org.__schema__(:fields),
|
field in Org.__schema__(:fields),
|
||||||
error <- errors,
|
error <- errors,
|
||||||
reduce: changeset,
|
reduce: changeset,
|
||||||
do: (acc -> Ecto.Changeset.add_error(acc, field, error))
|
do: (acc -> add_error(acc, field, error))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,18 +6,22 @@ defmodule Livebook.Teams.Org do
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
id: pos_integer() | nil,
|
id: pos_integer() | nil,
|
||||||
|
emoji: String.t() | nil,
|
||||||
name: String.t() | nil,
|
name: String.t() | nil,
|
||||||
teams_key: String.t() | nil,
|
teams_key: String.t() | nil,
|
||||||
user_code: String.t() | nil
|
user_code: String.t() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@primary_key {:id, :id, autogenerate: false}
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
|
field :emoji, :string
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :teams_key, :string
|
field :teams_key, :string
|
||||||
field :user_code, :string
|
field :user_code, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
@fields ~w(id name teams_key user_code)a
|
@fields ~w(id emoji name teams_key user_code)a
|
||||||
|
@required_fields @fields -- ~w(id user_code)a
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generates a new teams key.
|
Generates a new teams key.
|
||||||
|
@ -30,7 +34,7 @@ defmodule Livebook.Teams.Org do
|
||||||
org
|
org
|
||||||
|> cast(attrs, @fields)
|
|> cast(attrs, @fields)
|
||||||
|> generate_teams_key()
|
|> generate_teams_key()
|
||||||
|> validate_required(@fields -- [:id])
|
|> validate_required(@required_fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp generate_teams_key(changeset) do
|
defp generate_teams_key(changeset) do
|
||||||
|
|
88
lib/livebook_web/live/hub/edit/team_component.ex
Normal file
88
lib/livebook_web/live/hub/edit/team_component.ex
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
alias Livebook.Hubs.Team
|
||||||
|
alias Livebook.Teams
|
||||||
|
alias LivebookWeb.LayoutHelpers
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
changeset = Team.change_hub(assigns.hub)
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> assign_form(changeset)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div id={"#{@id}-component"} class="space-y-8">
|
||||||
|
<div class="flex relative">
|
||||||
|
<LayoutHelpers.title text={"#{@hub.hub_emoji} #{@hub.hub_name}"} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
phx-click={JS.push("delete_hub", value: %{id: @hub.id})}
|
||||||
|
class="absolute right-0 button-base button-red"
|
||||||
|
>
|
||||||
|
Delete hub
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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={@form}
|
||||||
|
phx-submit="save"
|
||||||
|
phx-change="validate"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
|
||||||
|
<.emoji_field field={f[:hub_emoji]} label="Emoji" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button-base button-blue" type="submit" phx-disable-with="Updating...">
|
||||||
|
Update Hub
|
||||||
|
</button>
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("save", %{"team" => params}, socket) do
|
||||||
|
case Teams.update_hub(socket.assigns.hub, params) do
|
||||||
|
{:ok, hub} ->
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:success, "Hub updated successfully")
|
||||||
|
|> push_navigate(to: ~p"/hub/#{hub.id}")}
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
{:noreply, assign_form(socket, changeset)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("validate", %{"team" => attrs}, socket) do
|
||||||
|
changeset =
|
||||||
|
socket.assigns.hub
|
||||||
|
|> Team.change_hub(attrs)
|
||||||
|
|> Map.replace!(:action, :validate)
|
||||||
|
|
||||||
|
{:noreply, assign_form(socket, changeset)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
||||||
|
assign(socket, form: to_form(changeset))
|
||||||
|
end
|
||||||
|
end
|
|
@ -70,6 +70,8 @@ defmodule LivebookWeb.Hub.EditLive do
|
||||||
hub={@hub}
|
hub={@hub}
|
||||||
id="enterprise-form"
|
id="enterprise-form"
|
||||||
/>
|
/>
|
||||||
|
<% "team" -> %>
|
||||||
|
<.live_component module={LivebookWeb.Hub.Edit.TeamComponent} hub={@hub} id="team-form" />
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</LayoutHelpers.layout>
|
</LayoutHelpers.layout>
|
||||||
|
|
|
@ -1,164 +0,0 @@
|
||||||
defmodule LivebookWeb.Hub.New.EnterpriseComponent do
|
|
||||||
use LivebookWeb, :live_component
|
|
||||||
|
|
||||||
import Ecto.Changeset, only: [get_field: 2]
|
|
||||||
|
|
||||||
alias Livebook.Hubs.{Enterprise, EnterpriseClient}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def update(assigns, socket) do
|
|
||||||
if connected?(socket) do
|
|
||||||
Livebook.Hubs.subscribe(:connection)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(assigns)
|
|
||||||
|> assign(
|
|
||||||
base: %Enterprise{},
|
|
||||||
changeset: Enterprise.change_hub(%Enterprise{}),
|
|
||||||
pid: nil
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div>
|
|
||||||
<.form
|
|
||||||
:let={f}
|
|
||||||
id={@id}
|
|
||||||
class="flex flex-col 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[:url]}
|
|
||||||
label="URL"
|
|
||||||
autofocus
|
|
||||||
spellcheck="false"
|
|
||||||
autocomplete="off"
|
|
||||||
phx-debounce="blur"
|
|
||||||
/>
|
|
||||||
<.password_field
|
|
||||||
type="password"
|
|
||||||
field={f[:token]}
|
|
||||||
label="Token"
|
|
||||||
spellcheck="false"
|
|
||||||
autocomplete="off"
|
|
||||||
phx-debounce="blur"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
id="connect"
|
|
||||||
type="button"
|
|
||||||
phx-click="connect"
|
|
||||||
phx-target={@myself}
|
|
||||||
class="button-base button-blue"
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= if @pid do %>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-1">
|
|
||||||
<.password_field type="password" field={f[:external_id]} label="ID" disabled />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
||||||
<.text_field field={f[:hub_name]} label="Name" readonly />
|
|
||||||
<.emoji_field field={f[:hub_emoji]} label="Emoji" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="button-base button-blue"
|
|
||||||
type="submit"
|
|
||||||
phx-disable-with="Add..."
|
|
||||||
disabled={not @changeset.valid?}
|
|
||||||
>
|
|
||||||
Add Hub
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</.form>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("connect", _params, socket) do
|
|
||||||
url = get_field(socket.assigns.changeset, :url)
|
|
||||||
token = get_field(socket.assigns.changeset, :token)
|
|
||||||
|
|
||||||
base = %Enterprise{
|
|
||||||
id: "enterprise-placeholder",
|
|
||||||
token: token,
|
|
||||||
external_id: "placeholder",
|
|
||||||
url: url,
|
|
||||||
hub_name: "Enterprise",
|
|
||||||
hub_emoji: "🏭"
|
|
||||||
}
|
|
||||||
|
|
||||||
{:ok, pid} = EnterpriseClient.start_link(base)
|
|
||||||
|
|
||||||
receive do
|
|
||||||
{:hub_connection_failed, reason} ->
|
|
||||||
EnterpriseClient.stop(base.id)
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:error, "Failed to connect with Enterprise: " <> reason)
|
|
||||||
|> push_patch(to: ~p"/hub")}
|
|
||||||
|
|
||||||
:hub_connected ->
|
|
||||||
data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version())
|
|
||||||
|
|
||||||
case EnterpriseClient.send_request(pid, data) do
|
|
||||||
{:handshake, handshake_response} ->
|
|
||||||
base = %{base | external_id: handshake_response.id}
|
|
||||||
changeset = Enterprise.validate_hub(base)
|
|
||||||
|
|
||||||
{:noreply, assign(socket, pid: pid, changeset: changeset, base: base)}
|
|
||||||
|
|
||||||
{:transport_error, reason} ->
|
|
||||||
EnterpriseClient.stop(base.id)
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:error, "Failed to connect with Enterprise: " <> reason)
|
|
||||||
|> push_patch(to: ~p"/hub")}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("save", %{"enterprise" => params}, socket) do
|
|
||||||
if socket.assigns.changeset.valid? do
|
|
||||||
case Enterprise.create_hub(socket.assigns.base, params) do
|
|
||||||
{:ok, hub} ->
|
|
||||||
if pid = socket.assigns.pid do
|
|
||||||
GenServer.stop(pid)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:success, "Hub added successfully")
|
|
||||||
|> push_navigate(to: ~p"/hub/#{hub.id}")}
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
|
||||||
{:noreply, assign(socket, changeset: changeset)}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("validate", %{"enterprise" => attrs}, socket) do
|
|
||||||
{:noreply, assign(socket, changeset: Enterprise.validate_hub(socket.assigns.base, attrs))}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,131 +0,0 @@
|
||||||
defmodule LivebookWeb.Hub.New.FlyComponent do
|
|
||||||
use LivebookWeb, :live_component
|
|
||||||
|
|
||||||
import Ecto.Changeset, only: [add_error: 3]
|
|
||||||
|
|
||||||
alias Livebook.Hubs.{Fly, FlyClient}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def update(assigns, socket) do
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(assigns)
|
|
||||||
|> assign(
|
|
||||||
base: %Fly{},
|
|
||||||
changeset: Fly.change_hub(%Fly{}),
|
|
||||||
selected_app: nil,
|
|
||||||
select_options: [],
|
|
||||||
apps: []
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div>
|
|
||||||
<.form
|
|
||||||
:let={f}
|
|
||||||
id={@id}
|
|
||||||
class="flex flex-col space-y-4"
|
|
||||||
for={@changeset}
|
|
||||||
phx-submit="save"
|
|
||||||
phx-change="validate"
|
|
||||||
phx-target={@myself}
|
|
||||||
>
|
|
||||||
<.password_field
|
|
||||||
type="password"
|
|
||||||
field={f[:access_token]}
|
|
||||||
label="Access Token"
|
|
||||||
phx-change="fetch_data"
|
|
||||||
phx-debounce="blur"
|
|
||||||
phx-target={@myself}
|
|
||||||
autofocus
|
|
||||||
spellcheck="false"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<%= if length(@apps) > 0 do %>
|
|
||||||
<.select_field
|
|
||||||
field={f[:application_id]}
|
|
||||||
label="Application"
|
|
||||||
options={@select_options}
|
|
||||||
prompt="Select one application"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
phx-disable-with="Add..."
|
|
||||||
disabled={not @changeset.valid?}
|
|
||||||
>
|
|
||||||
Add Hub
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</.form>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("fetch_data", %{"fly" => %{"access_token" => token}}, socket) do
|
|
||||||
case FlyClient.fetch_apps(token) do
|
|
||||||
{:ok, apps} ->
|
|
||||||
opts = select_options(apps)
|
|
||||||
base = %Fly{access_token: token, hub_emoji: "🚀"}
|
|
||||||
changeset = Fly.validate_hub(base)
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
assign(socket, changeset: changeset, base: base, select_options: opts, apps: apps)}
|
|
||||||
|
|
||||||
{:error, _} ->
|
|
||||||
changeset =
|
|
||||||
%Fly{}
|
|
||||||
|> Fly.validate_hub(%{access_token: token})
|
|
||||||
|> add_error(:access_token, "is invalid")
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
assign(socket, changeset: changeset, base: %Fly{}, select_options: [], apps: [])}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("save", %{"fly" => params}, socket) do
|
|
||||||
if socket.assigns.changeset.valid? do
|
|
||||||
case Fly.create_hub(socket.assigns.selected_app, params) do
|
|
||||||
{:ok, hub} ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:success, "Hub added successfully")
|
|
||||||
|> push_navigate(to: ~p"/hub/#{hub.id}")}
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
|
||||||
{:noreply, assign(socket, changeset: changeset)}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("validate", %{"fly" => attrs}, socket) do
|
|
||||||
params = Map.merge(socket.assigns.changeset.params, attrs)
|
|
||||||
|
|
||||||
application_id = params["application_id"]
|
|
||||||
selected_app = Enum.find(socket.assigns.apps, &(&1.application_id == application_id))
|
|
||||||
opts = select_options(socket.assigns.apps)
|
|
||||||
changeset = Fly.validate_hub(selected_app || socket.assigns.base, params)
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
assign(socket, changeset: changeset, selected_app: selected_app, select_options: opts)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp select_options(hubs) do
|
|
||||||
for fly <- hubs do
|
|
||||||
[key: "#{fly.organization_name} - #{fly.application_id}", value: fly.application_id]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,19 +1,30 @@
|
||||||
defmodule LivebookWeb.Hub.NewLive do
|
defmodule LivebookWeb.Hub.NewLive do
|
||||||
use LivebookWeb, :live_view
|
use LivebookWeb, :live_view
|
||||||
|
|
||||||
|
alias Livebook.Teams
|
||||||
|
alias Livebook.Teams.Org
|
||||||
alias LivebookWeb.LayoutHelpers
|
alias LivebookWeb.LayoutHelpers
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
|
||||||
on_mount LivebookWeb.SidebarHook
|
on_mount LivebookWeb.SidebarHook
|
||||||
|
|
||||||
|
@check_completion_data_internal 3000
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
enabled? = Livebook.Config.feature_flag_enabled?(:create_hub)
|
enabled? = Livebook.Config.feature_flag_enabled?(:create_hub)
|
||||||
{:ok, assign(socket, selected_type: nil, page_title: "Hub - Livebook", enabled?: enabled?)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
{:ok,
|
||||||
def handle_params(_params, _url, socket), do: {:noreply, socket}
|
assign(socket,
|
||||||
|
selected_option: nil,
|
||||||
|
page_title: "Hub - Livebook",
|
||||||
|
enabled?: enabled?,
|
||||||
|
requested_code: false,
|
||||||
|
org: nil,
|
||||||
|
verification_uri: nil,
|
||||||
|
org_form: nil
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(%{enabled?: false} = assigns) do
|
def render(%{enabled?: false} = assigns) do
|
||||||
|
@ -74,71 +85,94 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-4">
|
<.org_form
|
||||||
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
form={@org_form}
|
||||||
1. Select your Hub service
|
org={@org}
|
||||||
</h2>
|
requested_code={@requested_code}
|
||||||
|
selected={@selected_option}
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
verification_uri={@verification_uri}
|
||||||
<.card_item id="fly" selected={@selected_type} title="Fly">
|
/>
|
||||||
<:logo>
|
|
||||||
<%= Phoenix.HTML.raw(File.read!("static/images/fly.svg")) %>
|
|
||||||
</:logo>
|
|
||||||
<:headline>
|
|
||||||
Deploy notebooks to your Fly account.
|
|
||||||
</:headline>
|
|
||||||
</.card_item>
|
|
||||||
|
|
||||||
<.card_item id="enterprise" selected={@selected_type} title="Livebook Teams">
|
|
||||||
<:logo>
|
|
||||||
<img src="/images/teams.png" class="max-h-full max-w-[75%]" alt="Livebook Teams logo" />
|
|
||||||
</:logo>
|
|
||||||
<:headline>
|
|
||||||
Control access, manage secrets, and deploy notebooks within your team.
|
|
||||||
</:headline>
|
|
||||||
</.card_item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :if={@selected_type} class="flex flex-col space-y-4">
|
|
||||||
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
|
||||||
2. Configure your Hub
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<.live_component
|
|
||||||
:if={@selected_type == "fly"}
|
|
||||||
module={LivebookWeb.Hub.New.FlyComponent}
|
|
||||||
id="fly-form"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<.live_component
|
|
||||||
:if={@selected_type == "enterprise"}
|
|
||||||
module={LivebookWeb.Hub.New.EnterpriseComponent}
|
|
||||||
id="enterprise-form"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</LayoutHelpers.layout>
|
</LayoutHelpers.layout>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp org_form(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
||||||
|
1. Select your option
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<.card_item id="new-org" selected={@selected} title="Create a new organization">
|
||||||
|
<:logo><.remix_icon icon="add-circle-fill" class="text-black text-3xl" /></:logo>
|
||||||
|
<:headline>Create a new organization and invite your team members.</:headline>
|
||||||
|
</.card_item>
|
||||||
|
|
||||||
|
<.card_item id="join-org" selected={@selected} disabled title="Join an organization">
|
||||||
|
<:logo><.remix_icon icon="user-add-fill" class="text-black text-3xl" /></:logo>
|
||||||
|
<:headline>Coming soon...</:headline>
|
||||||
|
</.card_item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :if={@selected == "new-org"} class="flex flex-col space-y-4">
|
||||||
|
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
||||||
|
2. Create your Organization
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<.form
|
||||||
|
:let={f}
|
||||||
|
id="new-org-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={f[:name]} label="Name" />
|
||||||
|
<.emoji_field field={f[:emoji]} label="Emoji" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<.password_field readonly field={f[:teams_key]} label="Livebook Teams Key" />
|
||||||
|
|
||||||
|
<div :if={@requested_code} class="grid grid-cols-1 gap-3">
|
||||||
|
<span>
|
||||||
|
Access the following URL and input the User Code below to confirm the Organization creation.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<.link navigate={@verification_uri} target="_blank" class="font-bold text-blue-500">
|
||||||
|
<%= @verification_uri %>
|
||||||
|
</.link>
|
||||||
|
|
||||||
|
<span><%= @org.user_code %></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button :if={!@requested_code} class="button-base button-blue" phx-disable-with="Creating...">
|
||||||
|
Create Org
|
||||||
|
</button>
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
defp card_item(assigns) do
|
defp card_item(assigns) do
|
||||||
|
assigns = assign_new(assigns, :disabled, fn -> false end)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div
|
<div
|
||||||
id={@id}
|
id={@id}
|
||||||
class="flex flex-col cursor-pointer"
|
class={["flex flex-col cursor-pointer", disabled_class(@disabled)]}
|
||||||
phx-click={JS.push("select_type", value: %{value: @id})}
|
phx-click={JS.push("select_option", value: %{value: @id})}
|
||||||
>
|
>
|
||||||
<div class={[
|
<div class={[
|
||||||
"flex items-center justify-center p-6 border-2 rounded-t-2xl h-[150px]",
|
"flex items-center justify-center p-6 border-2 rounded-t-2xl h-[150px]",
|
||||||
if(@id == @selected, do: "border-gray-200", else: "border-gray-100")
|
card_item_border_class(@id, @selected)
|
||||||
]}>
|
]}>
|
||||||
<%= render_slot(@logo) %>
|
<%= render_slot(@logo) %>
|
||||||
</div>
|
</div>
|
||||||
<div class={[
|
<div class={["px-6 py-4 rounded-b-2xl grow", card_item_class(@id, @selected)]}>
|
||||||
"px-6 py-4 rounded-b-2xl grow",
|
|
||||||
if(@id == @selected, do: "bg-gray-200", else: "bg-gray-100")
|
|
||||||
]}>
|
|
||||||
<p class="text-gray-800 font-semibold cursor-pointer">
|
<p class="text-gray-800 font-semibold cursor-pointer">
|
||||||
<%= @title %>
|
<%= @title %>
|
||||||
</p>
|
</p>
|
||||||
|
@ -151,8 +185,108 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp disabled_class(true), do: "opacity-30 pointer-events-none"
|
||||||
|
defp disabled_class(false), do: ""
|
||||||
|
|
||||||
|
defp card_item_border_class(id, id), do: "border-gray-200"
|
||||||
|
defp card_item_border_class(_, _), do: "border-gray-100"
|
||||||
|
|
||||||
|
defp card_item_class(id, id), do: "bg-gray-200"
|
||||||
|
defp card_item_class(_, _), do: "bg-gray-100"
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("select_type", %{"value" => service}, socket) do
|
def handle_event("select_option", %{"value" => option}, socket) do
|
||||||
{:noreply, assign(socket, selected_type: service)}
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(selected_option: option)
|
||||||
|
|> assign_form(option)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("validate", %{"new_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", %{"new_org" => attrs}, socket) do
|
||||||
|
case Teams.create_org(socket.assigns.org, attrs) do
|
||||||
|
{:ok, 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, @check_completion_data_internal)
|
||||||
|
|
||||||
|
{: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, %{assigns: %{org: org}} = socket) do
|
||||||
|
case Teams.get_org_request_completion_data(org) do
|
||||||
|
{:ok, :awaiting_confirmation} ->
|
||||||
|
Process.send_after(self(), :check_completion_data, @check_completion_data_internal)
|
||||||
|
|
||||||
|
{: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}")}
|
||||||
|
|
||||||
|
{: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 creation request expired, could you please try again?"
|
||||||
|
)
|
||||||
|
|> assign_form(changeset)}
|
||||||
|
|
||||||
|
{:transport_error, message} ->
|
||||||
|
{:noreply, put_flash(socket, :error, message)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(_any, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
|
defp assign_form(socket, "new-org") do
|
||||||
|
org = %Org{emoji: "💡"}
|
||||||
|
changeset = Teams.change_org(org)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(org: org)
|
||||||
|
|> assign_form(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
||||||
|
assign(socket, org_form: to_form(changeset, as: :new_org))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,166 +1,62 @@
|
||||||
defmodule LivebookWeb.Hub.NewLiveTest do
|
defmodule LivebookWeb.Hub.NewLiveTest do
|
||||||
use LivebookWeb.ConnCase
|
use Livebook.TeamsIntegrationCase, async: true
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
alias Livebook.Hubs
|
|
||||||
|
|
||||||
test "render hub selection cards", %{conn: conn} do
|
test "render hub selection cards", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/hub")
|
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||||
|
|
||||||
assert html =~ "Fly"
|
# shows the new options
|
||||||
assert html =~ "Livebook Teams"
|
assert has_element?(view, "#new-org")
|
||||||
|
assert has_element?(view, "#join-org")
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "fly" do
|
describe "new-org" do
|
||||||
test "persists new hub", %{conn: conn} do
|
test "persist a new hub", %{conn: conn, node: node, user: user} do
|
||||||
fly_bypass("123456789")
|
name = "New Org Test #{System.unique_integer([:positive])}"
|
||||||
|
teams_key = Livebook.Teams.Org.teams_key()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||||
|
path = ~p"/hub/team-#{name}"
|
||||||
|
|
||||||
|
# select the new org option
|
||||||
assert view
|
assert view
|
||||||
|> element("#fly")
|
|> element("#new-org")
|
||||||
|> render_click() =~ "2. Configure your Hub"
|
|> render_click() =~ "2. Create your Organization"
|
||||||
|
|
||||||
assert view
|
# builds the form data
|
||||||
|> element(~s/input[name="fly[access_token]"]/)
|
org_attrs = %{"new_org" => %{"name" => name, "teams_key" => teams_key, "emoji" => "🐈"}}
|
||||||
|> render_change(%{"fly" => %{"access_token" => "dummy access token"}}) =~
|
|
||||||
~s(<option value="123456789">Foo Bar - 123456789</option>)
|
|
||||||
|
|
||||||
attrs = %{
|
# finds the form and change data
|
||||||
"access_token" => "dummy access token",
|
new_org_form = element(view, "#new-org-form")
|
||||||
"application_id" => "123456789",
|
render_change(new_org_form, org_attrs)
|
||||||
"hub_name" => "My Foo Hub",
|
|
||||||
"hub_emoji" => "🐈"
|
|
||||||
}
|
|
||||||
|
|
||||||
view
|
# submits the form
|
||||||
|> element("#fly-form")
|
render_submit(new_org_form, org_attrs)
|
||||||
|> render_change(%{"fly" => attrs})
|
|
||||||
|
|
||||||
refute view
|
# check if the form has the url to confirm
|
||||||
|> element("#fly-form .invalid-feedback")
|
link_element = element(view, "#new-org-form a")
|
||||||
|> has_element?()
|
html = render(link_element)
|
||||||
|
parsed_html = Floki.parse_document!(html)
|
||||||
|
assert [url] = Floki.attribute(parsed_html, "href")
|
||||||
|
assert [_port, [org_request_id]] = Regex.scan(~r/(?<=\D|^)\d{1,4}(?=\D|$)/, url)
|
||||||
|
id = String.to_integer(org_request_id)
|
||||||
|
|
||||||
assert {:ok, view, _html} =
|
# force org request confirmation
|
||||||
view
|
org_request = :erpc.call(node, Hub.Integration, :get_org_request!, [id])
|
||||||
|> element("#fly-form")
|
:erpc.call(node, Hub.Integration, :confirm_org_request, [org_request, user])
|
||||||
|> render_submit(%{"fly" => attrs})
|
|
||||||
|> follow_redirect(conn)
|
|
||||||
|
|
||||||
assert render(view) =~ "Hub added successfully"
|
# wait for the c:handle_info/2 cycle
|
||||||
|
# check if the page redirected to edit hub page
|
||||||
|
# and check the flash message
|
||||||
|
%{"success" => "Hub added successfully"} = assert_redirect(view, path, 1200)
|
||||||
|
|
||||||
|
# checks if the hub is in the sidebar
|
||||||
|
{:ok, view, _html} = live(conn, path)
|
||||||
hubs_html = view |> element("#hubs") |> render()
|
hubs_html = view |> element("#hubs") |> render()
|
||||||
|
|
||||||
assert hubs_html =~ "🐈"
|
assert hubs_html =~ "🐈"
|
||||||
assert hubs_html =~ "/hub/fly-123456789"
|
assert hubs_html =~ path
|
||||||
assert hubs_html =~ "My Foo Hub"
|
assert hubs_html =~ name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "fails to create existing hub", %{conn: conn} do
|
|
||||||
hub = insert_hub(:fly, id: "fly-foo", application_id: "foo")
|
|
||||||
fly_bypass(hub.application_id)
|
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
|
||||||
|
|
||||||
assert view
|
|
||||||
|> element("#fly")
|
|
||||||
|> render_click() =~ "2. Configure your Hub"
|
|
||||||
|
|
||||||
assert view
|
|
||||||
|> element(~s/input[name="fly[access_token]"]/)
|
|
||||||
|> render_change(%{"fly" => %{"access_token" => "dummy access token"}}) =~
|
|
||||||
~s(<option value="foo">Foo Bar - foo</option>)
|
|
||||||
|
|
||||||
attrs = %{
|
|
||||||
"access_token" => "dummy access token",
|
|
||||||
"application_id" => "foo",
|
|
||||||
"hub_name" => "My Foo Hub",
|
|
||||||
"hub_emoji" => "🐈"
|
|
||||||
}
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("#fly-form")
|
|
||||||
|> render_change(%{"fly" => attrs})
|
|
||||||
|
|
||||||
refute view
|
|
||||||
|> element("#fly-form .invalid-feedback")
|
|
||||||
|> has_element?()
|
|
||||||
|
|
||||||
assert view
|
|
||||||
|> element("#fly-form")
|
|
||||||
|> render_submit(%{"fly" => attrs}) =~ "already exists"
|
|
||||||
|
|
||||||
assert_hub(view, hub)
|
|
||||||
assert Hubs.fetch_hub!(hub.id) == hub
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fly_bypass(app_id) do
|
|
||||||
bypass = Bypass.open()
|
|
||||||
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
|
|
||||||
|
|
||||||
Bypass.expect(bypass, "POST", "/", fn conn ->
|
|
||||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
|
||||||
body = Jason.decode!(body)
|
|
||||||
|
|
||||||
response =
|
|
||||||
cond do
|
|
||||||
body["query"] =~ "apps" -> fetch_apps_response(app_id)
|
|
||||||
body["query"] =~ "app" -> fetch_app_response(app_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> Plug.Conn.put_resp_content_type("application/json")
|
|
||||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_apps_response(app_id) do
|
|
||||||
app = %{
|
|
||||||
"id" => app_id,
|
|
||||||
"organization" => %{
|
|
||||||
"id" => "l3soyvjmvtmwtl6l2drnbfuvltipprge",
|
|
||||||
"name" => "Foo Bar",
|
|
||||||
"type" => "PERSONAL"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%{"data" => %{"apps" => %{"nodes" => [app]}}}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_app_response(app_id) do
|
|
||||||
app = %{
|
|
||||||
"id" => app_id,
|
|
||||||
"name" => app_id,
|
|
||||||
"hostname" => app_id <> ".fly.dev",
|
|
||||||
"platformVersion" => "nomad",
|
|
||||||
"deployed" => true,
|
|
||||||
"status" => "running",
|
|
||||||
"secrets" => [
|
|
||||||
%{
|
|
||||||
"createdAt" => to_string(DateTime.utc_now()),
|
|
||||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
|
||||||
"id" => Livebook.Utils.random_short_id(),
|
|
||||||
"name" => "LIVEBOOK_PASSWORD"
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
"createdAt" => to_string(DateTime.utc_now()),
|
|
||||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
|
||||||
"id" => Livebook.Utils.random_short_id(),
|
|
||||||
"name" => "LIVEBOOK_SECRET_KEY_BASE"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
%{"data" => %{"app" => app}}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp assert_hub(view, hub) do
|
|
||||||
hubs_html = view |> element("#hubs") |> render()
|
|
||||||
|
|
||||||
assert hubs_html =~ hub.hub_emoji
|
|
||||||
assert hubs_html =~ ~p"/hub/#{hub.id}"
|
|
||||||
assert hubs_html =~ hub.hub_name
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,15 +31,17 @@ defmodule Livebook.Factory do
|
||||||
end
|
end
|
||||||
|
|
||||||
def build(:enterprise) do
|
def build(:enterprise) do
|
||||||
id = Livebook.Utils.random_id()
|
name = "Enteprise #{Livebook.Utils.random_short_id()}"
|
||||||
|
|
||||||
%Livebook.Hubs.Enterprise{
|
%Livebook.Hubs.Enterprise{
|
||||||
id: "enterprise-#{id}",
|
id: "enterprise-#{name}",
|
||||||
hub_name: "Enterprise",
|
hub_name: name,
|
||||||
hub_emoji: "🏭",
|
hub_emoji: "🏭",
|
||||||
external_id: id,
|
org_id: 1,
|
||||||
token: Livebook.Utils.random_cookie(),
|
user_id: 1,
|
||||||
url: "http://localhost"
|
org_key_id: 1,
|
||||||
|
teams_key: Livebook.Utils.random_id(),
|
||||||
|
session_token: Livebook.Utils.random_cookie()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -74,6 +76,7 @@ defmodule Livebook.Factory do
|
||||||
def build(:org) do
|
def build(:org) do
|
||||||
%Livebook.Teams.Org{
|
%Livebook.Teams.Org{
|
||||||
id: nil,
|
id: nil,
|
||||||
|
emoji: "🏭",
|
||||||
name: "Org Name #{System.unique_integer([:positive])}",
|
name: "Org Name #{System.unique_integer([:positive])}",
|
||||||
teams_key: Livebook.Teams.Org.teams_key(),
|
teams_key: Livebook.Teams.Org.teams_key(),
|
||||||
user_code: "request"
|
user_code: "request"
|
||||||
|
|
Loading…
Add table
Reference in a new issue