mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-29 03:02:51 +08:00
Implement users to join an organization (#1912)
This commit is contained in:
parent
7ff4b640f6
commit
070c0599b5
12 changed files with 290 additions and 784 deletions
|
@ -31,3 +31,6 @@ config :livebook, :feature_flags, create_hub: true
|
|||
if System.get_env("CI") == "true" do
|
||||
config :livebook, :node, {:longnames, :"livebook@127.0.0.1"}
|
||||
end
|
||||
|
||||
# Teams
|
||||
config :livebook, check_completion_data_interval: 300
|
||||
|
|
|
@ -18,10 +18,28 @@ defmodule Livebook.Teams do
|
|||
| {:error, Ecto.Changeset.t()}
|
||||
| {:transport_error, String.t()}
|
||||
def create_org(%Org{} = org, attrs) do
|
||||
create_org_request(org, attrs, &Client.create_org/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Joins an Org.
|
||||
|
||||
With success, returns the response from Livebook Teams API to continue the org joining flow.
|
||||
Otherwise, it will return an error tuple with changeset.
|
||||
"""
|
||||
@spec join_org(Org.t(), map()) ::
|
||||
{:ok, map()}
|
||||
| {:error, Ecto.Changeset.t()}
|
||||
| {:transport_error, String.t()}
|
||||
def join_org(%Org{} = org, attrs) do
|
||||
create_org_request(org, attrs, &Client.join_org/1)
|
||||
end
|
||||
|
||||
defp create_org_request(%Org{} = org, attrs, callback) when is_function(callback, 1) do
|
||||
changeset = Org.changeset(org, attrs)
|
||||
|
||||
with {:ok, %Org{} = org} <- apply_action(changeset, :insert),
|
||||
{:ok, response} <- Client.create_org(org) do
|
||||
{:ok, response} <- callback.(org) do
|
||||
{:ok, response}
|
||||
else
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
|
@ -99,7 +117,7 @@ defmodule Livebook.Teams do
|
|||
|
||||
defp add_org_errors(%Ecto.Changeset{} = changeset, errors_map) do
|
||||
for {key, errors} <- errors_map,
|
||||
field <- String.to_atom(key),
|
||||
field = String.to_atom(key),
|
||||
field in Org.__schema__(:fields),
|
||||
error <- errors,
|
||||
reduce: changeset,
|
||||
|
|
|
@ -10,10 +10,16 @@ defmodule Livebook.Teams.Client do
|
|||
@spec create_org(Org.t()) ::
|
||||
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
|
||||
def create_org(org) do
|
||||
hash = :crypto.hash(:sha256, org.teams_key)
|
||||
key_hash = Base.url_encode64(hash)
|
||||
post("/api/org-request", %{name: org.name, key_hash: Org.key_hash(org)})
|
||||
end
|
||||
|
||||
post("/api/org-request", %{name: org.name, key_hash: key_hash})
|
||||
@doc """
|
||||
Send a request to Livebook Team API to join an org.
|
||||
"""
|
||||
@spec join_org(Org.t()) ::
|
||||
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
|
||||
def join_org(org) do
|
||||
post("/api/org-request/join", %{name: org.name, key_hash: Org.key_hash(org)})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -29,17 +29,18 @@ defmodule Livebook.Teams.Org do
|
|||
@spec teams_key() :: String.t()
|
||||
def teams_key, do: Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)
|
||||
|
||||
@doc """
|
||||
Generates a hash key.
|
||||
"""
|
||||
@spec key_hash(t()) :: String.t()
|
||||
def key_hash(%__MODULE__{teams_key: teams_key}),
|
||||
do: Base.url_encode64(:crypto.hash(:sha256, teams_key), padding: false)
|
||||
|
||||
@doc false
|
||||
def changeset(org, attrs) do
|
||||
org
|
||||
|> cast(attrs, @fields)
|
||||
|> generate_teams_key()
|
||||
|> validate_required(@required_fields)
|
||||
end
|
||||
|
||||
defp generate_teams_key(changeset) do
|
||||
if get_field(changeset, :teams_key),
|
||||
do: changeset,
|
||||
else: put_change(changeset, :teams_key, teams_key())
|
||||
|> validate_format(:name, ~r/^[a-z0-9][a-z0-9\-]*$/)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
defmodule LivebookWeb.Hub.Edit.EnterpriseComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.Hubs.Enterprise
|
||||
alias LivebookWeb.LayoutHelpers
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
changeset = Enterprise.change_hub(assigns.hub)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(changeset: 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={@changeset}
|
||||
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>
|
||||
<div>
|
||||
<button
|
||||
class="button-base button-blue"
|
||||
type="submit"
|
||||
phx-disable-with="Updating..."
|
||||
disabled={not @changeset.valid?}
|
||||
>
|
||||
Update Hub
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"enterprise" => params}, socket) do
|
||||
case Enterprise.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(socket, changeset: changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"enterprise" => attrs}, socket) do
|
||||
{:noreply, assign(socket, changeset: Enterprise.validate_hub(socket.assigns.hub, attrs))}
|
||||
end
|
||||
end
|
|
@ -1,212 +0,0 @@
|
|||
defmodule LivebookWeb.Hub.Edit.FlyComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.Hubs.{Fly, FlyClient}
|
||||
alias LivebookWeb.LayoutHelpers
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
changeset = Fly.change_hub(assigns.hub)
|
||||
{:ok, app} = FlyClient.fetch_app(assigns.hub)
|
||||
env_vars = env_vars_from_secrets(app["secrets"])
|
||||
|
||||
env_var =
|
||||
if name = assigns.env_var_id do
|
||||
Enum.find(env_vars, &(&1.name == name))
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(
|
||||
app_url: "https://fly.io/apps/#{app["name"]}",
|
||||
changeset: changeset,
|
||||
env_vars: env_vars,
|
||||
env_var: env_var
|
||||
)}
|
||||
end
|
||||
|
||||
defp env_vars_from_secrets(secrets) do
|
||||
for secret <- secrets do
|
||||
%Livebook.Settings.EnvVar{name: secret["name"]}
|
||||
end
|
||||
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">
|
||||
<div class="flex items-center justify-between border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center space-x-12">
|
||||
<.labeled_text label="Application ID">
|
||||
<%= @hub.application_id %>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Type">
|
||||
Fly
|
||||
</.labeled_text>
|
||||
</div>
|
||||
|
||||
<a href={@app_url} class="button-base button-outlined-gray" target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" class="align-middle mr-1" />
|
||||
<span>Manage app on Fly</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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={@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[:hub_name]} label="Name" />
|
||||
<.emoji_field field={f[:hub_emoji]} label="Emoji" />
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="button-base button-blue"
|
||||
phx-disable-with="Updating..."
|
||||
disabled={not @changeset.valid?}
|
||||
>
|
||||
Update Hub
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
</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">
|
||||
Environment Variables
|
||||
</h2>
|
||||
|
||||
<.live_component
|
||||
module={LivebookWeb.EnvVarsComponent}
|
||||
id="env-vars"
|
||||
env_vars={@env_vars}
|
||||
return_to={~p"/hub/#{@hub.id}"}
|
||||
add_env_var_path={~p"/hub/#{@hub.id}/env-var/new"}
|
||||
edit_label="Replace"
|
||||
target={@myself}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.modal
|
||||
:if={@live_action in [:add_env_var, :edit_env_var]}
|
||||
id="env-var-modal"
|
||||
show
|
||||
width={:medium}
|
||||
target={@myself}
|
||||
patch={~p"/hub/#{@hub.id}"}
|
||||
>
|
||||
<.live_component
|
||||
module={LivebookWeb.EnvVarComponent}
|
||||
id="env-var"
|
||||
on_save={JS.push("save_env_var", target: @myself)}
|
||||
env_var={@env_var}
|
||||
headline="Configute your Fly application system environment variables"
|
||||
return_to={~p"/hub/#{@hub.id}"}
|
||||
/>
|
||||
</.modal>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"fly" => params}, socket) do
|
||||
case Fly.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(socket, changeset: changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"fly" => attrs}, socket) do
|
||||
{:noreply, assign(socket, changeset: Fly.validate_hub(socket.assigns.hub, attrs))}
|
||||
end
|
||||
|
||||
# EnvVar component callbacks
|
||||
|
||||
def handle_event("save_env_var", %{"env_var" => attrs}, socket) do
|
||||
env_operation = attrs["operation"]
|
||||
attrs = %{"key" => attrs["name"], "value" => attrs["value"]}
|
||||
|
||||
case FlyClient.set_secrets(socket.assigns.hub, [attrs]) do
|
||||
{:ok, _} ->
|
||||
message =
|
||||
if env_operation == "new",
|
||||
do: "Environment variable added",
|
||||
else: "Environment variable updated"
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, message)
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")}
|
||||
|
||||
{:error, _} ->
|
||||
message =
|
||||
if env_operation == "new",
|
||||
do: "Failed to add environment variable",
|
||||
else: "Failed to update environment variable"
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, message)
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("edit_env_var", %{"env_var" => key}, socket) do
|
||||
{:noreply, push_patch(socket, to: ~p"/hub/#{socket.assigns.hub.id}/env-var/edit/#{key}")}
|
||||
end
|
||||
|
||||
def handle_event("delete_env_var", %{"env_var" => key}, socket) do
|
||||
on_confirm = fn socket ->
|
||||
case FlyClient.unset_secrets(socket.assigns.hub, [key]) do
|
||||
{:ok, _} ->
|
||||
socket
|
||||
|> put_flash(:success, "Environment variable deleted")
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")
|
||||
|
||||
{:error, _} ->
|
||||
socket
|
||||
|> put_flash(:error, "Failed to delete environment variable")
|
||||
|> push_navigate(to: ~p"/hub/#{socket.assigns.hub.id}")
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
confirm(socket, on_confirm,
|
||||
title: "Delete #{key}",
|
||||
description: "Are you sure you want to delete environment variable?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)}
|
||||
end
|
||||
end
|
|
@ -46,38 +46,35 @@ defmodule LivebookWeb.Hub.EditLive do
|
|||
saved_hubs={@saved_hubs}
|
||||
>
|
||||
<div class="p-4 md:px-12 md:py-7 max-w-screen-md mx-auto">
|
||||
<%= case @type do %>
|
||||
<% "fly" -> %>
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Edit.FlyComponent}
|
||||
hub={@hub}
|
||||
id="fly-form"
|
||||
live_action={@live_action}
|
||||
env_var_id={@env_var_id}
|
||||
/>
|
||||
<% "personal" -> %>
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Edit.PersonalComponent}
|
||||
hub={@hub}
|
||||
secrets={@secrets}
|
||||
live_action={@live_action}
|
||||
secret_name={@secret_name}
|
||||
id="personal-form"
|
||||
/>
|
||||
<% "enterprise" -> %>
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Edit.EnterpriseComponent}
|
||||
hub={@hub}
|
||||
id="enterprise-form"
|
||||
/>
|
||||
<% "team" -> %>
|
||||
<.live_component module={LivebookWeb.Hub.Edit.TeamComponent} hub={@hub} id="team-form" />
|
||||
<% end %>
|
||||
<.hub_component
|
||||
type={@type}
|
||||
hub={@hub}
|
||||
live_action={@live_action}
|
||||
secrets={@secrets}
|
||||
secret_name={@secret_name}
|
||||
/>
|
||||
</div>
|
||||
</LayoutHelpers.layout>
|
||||
"""
|
||||
end
|
||||
|
||||
defp hub_component(%{type: "personal"} = assigns) do
|
||||
~H"""
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Edit.PersonalComponent}
|
||||
hub={@hub}
|
||||
secrets={@secrets}
|
||||
live_action={@live_action}
|
||||
secret_name={@secret_name}
|
||||
id="personal-form"
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
defp hub_component(%{type: "team"} = assigns) do
|
||||
~H(<.live_component module={LivebookWeb.Hub.Edit.TeamComponent} hub={@hub} id="team-form" />)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete_hub", %{"id" => id}, socket) do
|
||||
on_confirm = fn socket ->
|
||||
|
|
|
@ -8,7 +8,11 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
|
||||
on_mount LivebookWeb.SidebarHook
|
||||
|
||||
@check_completion_data_internal 3000
|
||||
@check_completion_data_interval Application.compile_env(
|
||||
:livebook,
|
||||
:check_completion_data_interval,
|
||||
3000
|
||||
)
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
|
@ -22,7 +26,10 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
requested_code: false,
|
||||
org: nil,
|
||||
verification_uri: nil,
|
||||
org_form: nil
|
||||
form: nil,
|
||||
form_title: nil,
|
||||
button_label: nil,
|
||||
request_code_info: nil
|
||||
)}
|
||||
end
|
||||
|
||||
|
@ -85,85 +92,79 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<.org_form
|
||||
form={@org_form}
|
||||
org={@org}
|
||||
requested_code={@requested_code}
|
||||
selected={@selected_option}
|
||||
verification_uri={@verification_uri}
|
||||
/>
|
||||
<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_option} 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_option} title="Join an organization">
|
||||
<:logo><.remix_icon icon="user-add-fill" class="text-black text-3xl" /></:logo>
|
||||
<:headline>Join within the organization of your team members.</:headline>
|
||||
</.card_item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :if={@selected_option} class="flex flex-col space-y-4">
|
||||
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
||||
2. <%= @form_title %>
|
||||
</h2>
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
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={f[:name]} label="Name" />
|
||||
<.emoji_field field={f[:emoji]} label="Emoji" />
|
||||
</div>
|
||||
|
||||
<.password_field
|
||||
readonly={@selected_option == "new-org"}
|
||||
field={f[:teams_key]}
|
||||
label="Livebook Teams Key"
|
||||
/>
|
||||
|
||||
<div :if={@requested_code} class="grid grid-cols-1 gap-3">
|
||||
<span><%= @request_code_info %></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..."
|
||||
>
|
||||
<%= @button_label %>
|
||||
</button>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutHelpers.layout>
|
||||
"""
|
||||
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
|
||||
assigns = assign_new(assigns, :disabled, fn -> false end)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
id={@id}
|
||||
class={["flex flex-col cursor-pointer", disabled_class(@disabled)]}
|
||||
class="flex flex-col cursor-pointer"
|
||||
phx-click={JS.push("select_option", value: %{value: @id})}
|
||||
>
|
||||
<div class={[
|
||||
|
@ -185,9 +186,6 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
"""
|
||||
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"
|
||||
|
||||
|
@ -198,11 +196,11 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
def handle_event("select_option", %{"value" => option}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(selected_option: option)
|
||||
|> assign(selected_option: option, requested_code: false, verification_uri: nil)
|
||||
|> assign_form(option)}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"new_org" => attrs}, socket) do
|
||||
def handle_event("validate", %{"org" => attrs}, socket) do
|
||||
changeset =
|
||||
socket.assigns.org
|
||||
|> Teams.change_org(attrs)
|
||||
|
@ -211,14 +209,20 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
{:noreply, assign_form(socket, changeset)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"new_org" => attrs}, socket) do
|
||||
case Teams.create_org(socket.assigns.org, attrs) do
|
||||
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, 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)
|
||||
Process.send_after(self(), :check_completion_data, @check_completion_data_interval)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|
@ -237,7 +241,7 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
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)
|
||||
Process.send_after(self(), :check_completion_data, @check_completion_data_interval)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
|
@ -264,10 +268,7 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
{: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?"
|
||||
)
|
||||
|> put_flash(:error, "Oh no! Your org request expired, could you please try again?")
|
||||
|> assign_form(changeset)}
|
||||
|
||||
{:transport_error, message} ->
|
||||
|
@ -277,16 +278,37 @@ defmodule LivebookWeb.Hub.NewLive do
|
|||
|
||||
def handle_info(_any, socket), do: {:noreply, socket}
|
||||
|
||||
defp assign_form(socket, "new-org") do
|
||||
defp assign_form(socket, "join-org") do
|
||||
org = %Org{emoji: "💡"}
|
||||
changeset = Teams.change_org(org)
|
||||
|
||||
socket
|
||||
|> assign(org: org)
|
||||
|> assign(
|
||||
org: org,
|
||||
form_title: "Join an Organization",
|
||||
button_label: "Join Org",
|
||||
request_code_info:
|
||||
"Access the following URL and input the User Code below to confirm the Organization creation."
|
||||
)
|
||||
|> 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,
|
||||
form_title: "Create your Organization",
|
||||
button_label: "Create Org",
|
||||
request_code_info:
|
||||
"Access the following URL and input the User Code below to confirm to join an Organization."
|
||||
)
|
||||
|> assign_form(changeset)
|
||||
end
|
||||
|
||||
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
||||
assign(socket, org_form: to_form(changeset, as: :new_org))
|
||||
assign(socket, form: to_form(changeset))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule Livebook.TeamsTest do
|
|||
use Livebook.TeamsIntegrationCase, async: true
|
||||
|
||||
alias Livebook.Teams
|
||||
alias Livebook.Teams.Org
|
||||
|
||||
describe "create_org/1" do
|
||||
test "returns the device flow data to confirm the org creation" do
|
||||
|
@ -25,6 +26,41 @@ defmodule Livebook.TeamsTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "join_org/1" do
|
||||
test "returns the device flow data to confirm the org creation", %{user: user, node: node} do
|
||||
org = build(:org)
|
||||
key_hash = Org.key_hash(org)
|
||||
|
||||
teams_org = :erpc.call(node, Hub.Integration, :create_org, [[name: org.name]])
|
||||
:erpc.call(node, Hub.Integration, :create_org_key, [[org: teams_org, key_hash: key_hash]])
|
||||
:erpc.call(node, Hub.Integration, :create_user_org, [[org: teams_org, user: user]])
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
"device_code" => _device_code,
|
||||
"expires_in" => 300,
|
||||
"id" => _org_id,
|
||||
"user_code" => _user_code,
|
||||
"verification_uri" => _verification_uri
|
||||
}} = Teams.join_org(org, %{})
|
||||
end
|
||||
|
||||
test "returns changeset errors when data is invalid" do
|
||||
org = build(:org)
|
||||
|
||||
assert {:error, changeset} = Teams.join_org(org, %{name: nil})
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
end
|
||||
|
||||
test "returns changeset errors when org doesn't exist" do
|
||||
org = build(:org)
|
||||
|
||||
assert {:error, changeset} = Teams.join_org(org, %{})
|
||||
assert "does not exist" in errors_on(changeset).name
|
||||
assert "does not match existing key" in errors_on(changeset).teams_key
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_org_request_completion_data/1" do
|
||||
test "returns the org data when it has been confirmed", %{node: node, user: user} do
|
||||
teams_key = Teams.Org.teams_key()
|
||||
|
|
|
@ -6,243 +6,6 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
|
||||
alias Livebook.Hubs
|
||||
|
||||
describe "fly" do
|
||||
setup do
|
||||
bypass = Bypass.open()
|
||||
Application.put_env(:livebook, :fly_graphql_endpoint, "http://localhost:#{bypass.port}")
|
||||
|
||||
{:ok, bypass: bypass}
|
||||
end
|
||||
|
||||
test "updates hub", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :mount} end)
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
assert html =~ "Manage app on Fly"
|
||||
assert html =~ "https://fly.io/apps/#{hub.application_id}"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
refute html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
|
||||
attrs = %{
|
||||
"hub_name" => "Personal Hub",
|
||||
"hub_emoji" => "🐈"
|
||||
}
|
||||
|
||||
view
|
||||
|> element("#fly-form")
|
||||
|> render_change(%{"fly" => attrs})
|
||||
|
||||
refute view
|
||||
|> element("#fly-form .invalid-feedback")
|
||||
|> has_element?()
|
||||
|
||||
assert {:ok, view, _html} =
|
||||
view
|
||||
|> element("#fly-form")
|
||||
|> render_submit(%{"fly" => attrs})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert render(view) =~ "Hub updated successfully"
|
||||
|
||||
assert_hub(view, %{hub | hub_emoji: attrs["hub_emoji"], hub_name: attrs["hub_name"]})
|
||||
refute Hubs.fetch_hub!(hub.id) == hub
|
||||
end
|
||||
|
||||
test "deletes hub", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :mount} end)
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub_id = "fly-#{app_id}"
|
||||
|
||||
hub = insert_hub(:fly, id: hub_id, hub_name: "My Deletable Hub", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
view
|
||||
|> element("button", "Delete hub")
|
||||
|> render_click()
|
||||
|
||||
assert {:ok, view, _html} =
|
||||
view
|
||||
|> render_confirm()
|
||||
|> follow_redirect(conn)
|
||||
|
||||
hubs_html = view |> element("#hubs") |> render()
|
||||
|
||||
refute hubs_html =~ ~p"/hub/#{hub.id}"
|
||||
refute hubs_html =~ hub.hub_name
|
||||
|
||||
assert Hubs.fetch_hub(hub_id) == :error
|
||||
end
|
||||
|
||||
test "add env var", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :mount} end)
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
assert html =~ "Manage app on Fly"
|
||||
assert html =~ "https://fly.io/apps/#{hub.application_id}"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
refute html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
|
||||
view
|
||||
|> element("#add-env-var")
|
||||
|> render_click(%{})
|
||||
|
||||
assert_patch(view, ~p"/hub/#{hub.id}/env-var/new")
|
||||
assert render(view) =~ "Add environment variable"
|
||||
|
||||
attrs = params_for(:env_var, name: "FOO_ENV_VAR")
|
||||
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_change(%{"env_var" => attrs})
|
||||
|
||||
refute view
|
||||
|> element("#env-var-form button[disabled]")
|
||||
|> has_element?()
|
||||
|
||||
:ok = Agent.update(pid, fn state -> %{state | type: :add} end)
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_submit(%{"env_var" => attrs})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Environment variable added"
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
end
|
||||
|
||||
test "update env var", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :foo} end)
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
assert html =~ "Manage app on Fly"
|
||||
assert html =~ "https://fly.io/apps/#{hub.application_id}"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
|
||||
view
|
||||
|> element("#env-var-FOO_ENV_VAR-edit")
|
||||
|> render_click(%{"env_var" => "FOO_ENV_VAR"})
|
||||
|
||||
assert_patch(view, ~p"/hub/#{hub.id}/env-var/edit/FOO_ENV_VAR")
|
||||
assert render(view) =~ "Edit environment variable"
|
||||
|
||||
attrs = params_for(:env_var, name: "FOO_ENV_VAR")
|
||||
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_change(%{"env_var" => attrs})
|
||||
|
||||
refute view
|
||||
|> element("#env-var-form button[disabled]")
|
||||
|> has_element?()
|
||||
|
||||
:ok = Agent.update(pid, fn state -> %{state | type: :updated_foo} end)
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
view
|
||||
|> element("#env-var-form")
|
||||
|> render_submit(%{"env_var" => attrs})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Environment variable updated"
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
end
|
||||
|
||||
test "delete env var", %{conn: conn, bypass: bypass} do
|
||||
{:ok, pid} = Agent.start(fn -> %{fun: &fetch_app_response/2, type: :add} end)
|
||||
|
||||
app_id = Livebook.Utils.random_short_id()
|
||||
hub = insert_hub(:fly, id: "fly-#{app_id}", application_id: app_id)
|
||||
fly_bypass(bypass, app_id, pid)
|
||||
|
||||
{:ok, view, html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
assert html =~ "Manage app on Fly"
|
||||
assert html =~ "https://fly.io/apps/#{hub.application_id}"
|
||||
|
||||
assert html =~ "Environment Variables"
|
||||
assert html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
|
||||
:ok = Agent.update(pid, fn state -> %{state | type: :mount} end)
|
||||
|
||||
view
|
||||
|> element(~s/#env-var-FOO_ENV_VAR-delete/)
|
||||
|> render_click()
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
view
|
||||
|> render_confirm()
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Environment variable deleted"
|
||||
assert html =~ "Environment Variables"
|
||||
refute html =~ "FOO_ENV_VAR"
|
||||
assert html =~ "LIVEBOOK_PASSWORD"
|
||||
assert html =~ "LIVEBOOK_SECRET_KEY_BASE"
|
||||
end
|
||||
end
|
||||
|
||||
describe "enterprise" do
|
||||
test "updates hub", %{conn: conn} do
|
||||
hub = insert_hub(:enterprise)
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
attrs = %{"hub_emoji" => "🐈"}
|
||||
|
||||
view
|
||||
|> element("#enterprise-form")
|
||||
|> render_change(%{"enterprise" => attrs})
|
||||
|
||||
refute view
|
||||
|> element("#enterprise-form .invalid-feedback")
|
||||
|> has_element?()
|
||||
|
||||
assert {:ok, view, _html} =
|
||||
view
|
||||
|> element("#enterprise-form")
|
||||
|> render_submit(%{"enterprise" => attrs})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert render(view) =~ "Hub updated successfully"
|
||||
|
||||
assert_hub(view, %{hub | hub_emoji: attrs["hub_emoji"]})
|
||||
refute Hubs.fetch_hub!(hub.id) == hub
|
||||
|
||||
Hubs.delete_hub(hub.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "personal" do
|
||||
setup do
|
||||
Livebook.Hubs.subscribe([:secrets])
|
||||
|
@ -382,114 +145,4 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
|||
assert hubs_html =~ ~p"/hub/#{hub.id}"
|
||||
assert hubs_html =~ hub.hub_name
|
||||
end
|
||||
|
||||
defp fly_bypass(bypass, app_id, agent_pid) do
|
||||
Bypass.expect(bypass, "POST", "/", fn conn ->
|
||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||
body = Jason.decode!(body)
|
||||
|
||||
response =
|
||||
cond do
|
||||
body["query"] =~ "setSecrets" ->
|
||||
set_secrets_response()
|
||||
|
||||
body["query"] =~ "unsetSecrets" ->
|
||||
unset_secrets_response()
|
||||
|
||||
true ->
|
||||
Agent.get(agent_pid, fn
|
||||
%{fun: fun, type: type} -> fun.(app_id, type)
|
||||
%{fun: fun} -> fun.()
|
||||
end)
|
||||
end
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.resp(200, Jason.encode!(response))
|
||||
end)
|
||||
end
|
||||
|
||||
defp fetch_app_response(app_id, type) do
|
||||
app = %{
|
||||
"id" => app_id,
|
||||
"name" => app_id,
|
||||
"hostname" => app_id <> ".fly.dev",
|
||||
"platformVersion" => "nomad",
|
||||
"deployed" => true,
|
||||
"status" => "running",
|
||||
"secrets" => secrets(type)
|
||||
}
|
||||
|
||||
%{"data" => %{"app" => app}}
|
||||
end
|
||||
|
||||
defp secrets(:mount) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123",
|
||||
"name" => "LIVEBOOK_PASSWORD"
|
||||
},
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "456",
|
||||
"name" => "LIVEBOOK_SECRET_KEY_BASE"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp secrets(:add) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "789",
|
||||
"name" => "FOO_ENV_VAR"
|
||||
},
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123",
|
||||
"name" => "LIVEBOOK_PASSWORD"
|
||||
},
|
||||
%{
|
||||
"createdAt" => to_string(DateTime.utc_now()),
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "456",
|
||||
"name" => "LIVEBOOK_SECRET_KEY_BASE"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp secrets(:foo) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => "2022-08-31 14:47:39.904338Z",
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123456789",
|
||||
"name" => "FOO_ENV_VAR"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp secrets(:updated_foo) do
|
||||
[
|
||||
%{
|
||||
"createdAt" => "2022-08-31 14:47:41.632669Z",
|
||||
"digest" => to_string(Livebook.Utils.random_cookie()),
|
||||
"id" => "123456789",
|
||||
"name" => "FOO_ENV_VAR"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp set_secrets_response do
|
||||
%{"data" => %{"setSecrets" => %{"app" => %{"secrets" => secrets(:add)}}}}
|
||||
end
|
||||
|
||||
defp unset_secrets_response do
|
||||
%{"data" => %{"unsetSecrets" => %{"app" => %{"secrets" => secrets(:mount)}}}}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule LivebookWeb.Hub.NewLiveTest do
|
||||
use Livebook.TeamsIntegrationCase, async: true
|
||||
|
||||
alias Livebook.Teams.Org
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
test "render hub selection cards", %{conn: conn} do
|
||||
|
@ -13,11 +15,12 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
|
||||
describe "new-org" do
|
||||
test "persist a new hub", %{conn: conn, node: node, user: user} do
|
||||
name = "New Org Test #{System.unique_integer([:positive])}"
|
||||
name = "new-org-test"
|
||||
teams_key = Livebook.Teams.Org.teams_key()
|
||||
key_hash = Org.key_hash(build(:org, teams_key: teams_key))
|
||||
path = ~p"/hub/team-#{name}"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
path = ~p"/hub/team-#{name}"
|
||||
|
||||
# select the new org option
|
||||
assert view
|
||||
|
@ -25,31 +28,33 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
|> render_click() =~ "2. Create your Organization"
|
||||
|
||||
# builds the form data
|
||||
org_attrs = %{"new_org" => %{"name" => name, "teams_key" => teams_key, "emoji" => "🐈"}}
|
||||
attrs = %{"org" => %{"name" => name, "teams_key" => teams_key, "emoji" => "🐈"}}
|
||||
|
||||
# finds the form and change data
|
||||
new_org_form = element(view, "#new-org-form")
|
||||
render_change(new_org_form, org_attrs)
|
||||
form = element(view, "#new-org-form")
|
||||
render_change(form, attrs)
|
||||
|
||||
# submits the form
|
||||
render_submit(new_org_form, org_attrs)
|
||||
# submits the form
|
||||
render_submit(form, attrs)
|
||||
|
||||
# gets the org request by name and key hash
|
||||
org_request =
|
||||
:erpc.call(node, Hub.Integration, :get_org_request_by!, [
|
||||
[name: name, key_hash: key_hash]
|
||||
])
|
||||
|
||||
# check if the form has the url to confirm
|
||||
link_element = element(view, "#new-org-form a")
|
||||
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 render(link_element) =~ "/org-request/#{org_request.id}/confirm"
|
||||
|
||||
# force org request confirmation
|
||||
org_request = :erpc.call(node, Hub.Integration, :get_org_request!, [id])
|
||||
:erpc.call(node, Hub.Integration, :confirm_org_request, [org_request, user])
|
||||
|
||||
# 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)
|
||||
%{"success" => "Hub added successfully"} =
|
||||
assert_redirect(view, path, check_completion_data_interval())
|
||||
|
||||
# checks if the hub is in the sidebar
|
||||
{:ok, view, _html} = live(conn, path)
|
||||
|
@ -59,4 +64,65 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
|||
assert hubs_html =~ name
|
||||
end
|
||||
end
|
||||
|
||||
describe "join-org" do
|
||||
test "persist a new hub", %{conn: conn, node: node, user: user} do
|
||||
name = "join-org-test"
|
||||
teams_key = Livebook.Teams.Org.teams_key()
|
||||
key_hash = Org.key_hash(build(:org, teams_key: teams_key))
|
||||
path = ~p"/hub/team-#{name}"
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||
|
||||
# previously create the org and associate user with org
|
||||
org = :erpc.call(node, Hub.Integration, :create_org, [[name: name]])
|
||||
:erpc.call(node, Hub.Integration, :create_org_key, [[org: org, key_hash: key_hash]])
|
||||
:erpc.call(node, Hub.Integration, :create_user_org, [[org: org, user: user]])
|
||||
|
||||
# select the new org option
|
||||
assert view
|
||||
|> element("#join-org")
|
||||
|> render_click() =~ "2. Join an Organization"
|
||||
|
||||
# builds the form data
|
||||
attrs = %{"org" => %{"name" => name, "teams_key" => teams_key, "emoji" => "🐈"}}
|
||||
|
||||
# finds the form and change data
|
||||
form = element(view, "#join-org-form")
|
||||
render_change(form, attrs)
|
||||
|
||||
# submits the form
|
||||
render_submit(form, attrs)
|
||||
|
||||
# gets the org request by name and key hash
|
||||
org_request =
|
||||
:erpc.call(node, Hub.Integration, :get_org_request_by!, [
|
||||
[name: name, key_hash: key_hash]
|
||||
])
|
||||
|
||||
# check if the form has the url to confirm
|
||||
link_element = element(view, "#join-org-form a")
|
||||
assert render(link_element) =~ "/org-request/#{org_request.id}/confirm"
|
||||
|
||||
# force org request confirmation
|
||||
:erpc.call(node, Hub.Integration, :confirm_org_request, [org_request, user])
|
||||
|
||||
# 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, check_completion_data_interval())
|
||||
|
||||
# checks if the hub is in the sidebar
|
||||
{:ok, view, _html} = live(conn, path)
|
||||
hubs_html = view |> element("#hubs") |> render()
|
||||
assert hubs_html =~ "🐈"
|
||||
assert hubs_html =~ path
|
||||
assert hubs_html =~ name
|
||||
end
|
||||
end
|
||||
|
||||
defp check_completion_data_interval do
|
||||
Application.fetch_env!(:livebook, :check_completion_data_interval) + 100
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,9 +77,9 @@ defmodule Livebook.Factory do
|
|||
%Livebook.Teams.Org{
|
||||
id: nil,
|
||||
emoji: "🏭",
|
||||
name: "Org Name #{System.unique_integer([:positive])}",
|
||||
name: "org-name-#{System.unique_integer([:positive])}",
|
||||
teams_key: Livebook.Teams.Org.teams_key(),
|
||||
user_code: "request"
|
||||
user_code: nil
|
||||
}
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue