mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-29 19:20:46 +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
|
if System.get_env("CI") == "true" do
|
||||||
config :livebook, :node, {:longnames, :"livebook@127.0.0.1"}
|
config :livebook, :node, {:longnames, :"livebook@127.0.0.1"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Teams
|
||||||
|
config :livebook, check_completion_data_interval: 300
|
||||||
|
|
|
@ -18,10 +18,28 @@ defmodule Livebook.Teams do
|
||||||
| {:error, Ecto.Changeset.t()}
|
| {:error, Ecto.Changeset.t()}
|
||||||
| {:transport_error, String.t()}
|
| {:transport_error, String.t()}
|
||||||
def create_org(%Org{} = org, attrs) do
|
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)
|
changeset = Org.changeset(org, attrs)
|
||||||
|
|
||||||
with {:ok, %Org{} = org} <- apply_action(changeset, :insert),
|
with {:ok, %Org{} = org} <- apply_action(changeset, :insert),
|
||||||
{:ok, response} <- Client.create_org(org) do
|
{:ok, response} <- callback.(org) do
|
||||||
{:ok, response}
|
{:ok, response}
|
||||||
else
|
else
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
@ -99,7 +117,7 @@ defmodule Livebook.Teams do
|
||||||
|
|
||||||
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,
|
||||||
|
|
|
@ -10,10 +10,16 @@ defmodule Livebook.Teams.Client do
|
||||||
@spec create_org(Org.t()) ::
|
@spec create_org(Org.t()) ::
|
||||||
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
|
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
|
||||||
def create_org(org) do
|
def create_org(org) do
|
||||||
hash = :crypto.hash(:sha256, org.teams_key)
|
post("/api/org-request", %{name: org.name, key_hash: Org.key_hash(org)})
|
||||||
key_hash = Base.url_encode64(hash)
|
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
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
@ -29,17 +29,18 @@ defmodule Livebook.Teams.Org do
|
||||||
@spec teams_key() :: String.t()
|
@spec teams_key() :: String.t()
|
||||||
def teams_key, do: Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)
|
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
|
@doc false
|
||||||
def changeset(org, attrs) do
|
def changeset(org, attrs) do
|
||||||
org
|
org
|
||||||
|> cast(attrs, @fields)
|
|> cast(attrs, @fields)
|
||||||
|> generate_teams_key()
|
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
end
|
|> validate_format(:name, ~r/^[a-z0-9][a-z0-9\-]*$/)
|
||||||
|
|
||||||
defp generate_teams_key(changeset) do
|
|
||||||
if get_field(changeset, :teams_key),
|
|
||||||
do: changeset,
|
|
||||||
else: put_change(changeset, :teams_key, teams_key())
|
|
||||||
end
|
end
|
||||||
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}
|
saved_hubs={@saved_hubs}
|
||||||
>
|
>
|
||||||
<div class="p-4 md:px-12 md:py-7 max-w-screen-md mx-auto">
|
<div class="p-4 md:px-12 md:py-7 max-w-screen-md mx-auto">
|
||||||
<%= case @type do %>
|
<.hub_component
|
||||||
<% "fly" -> %>
|
type={@type}
|
||||||
<.live_component
|
hub={@hub}
|
||||||
module={LivebookWeb.Hub.Edit.FlyComponent}
|
live_action={@live_action}
|
||||||
hub={@hub}
|
secrets={@secrets}
|
||||||
id="fly-form"
|
secret_name={@secret_name}
|
||||||
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 %>
|
|
||||||
</div>
|
</div>
|
||||||
</LayoutHelpers.layout>
|
</LayoutHelpers.layout>
|
||||||
"""
|
"""
|
||||||
end
|
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
|
@impl true
|
||||||
def handle_event("delete_hub", %{"id" => id}, socket) do
|
def handle_event("delete_hub", %{"id" => id}, socket) do
|
||||||
on_confirm = fn socket ->
|
on_confirm = fn socket ->
|
||||||
|
|
|
@ -8,7 +8,11 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
|
|
||||||
on_mount LivebookWeb.SidebarHook
|
on_mount LivebookWeb.SidebarHook
|
||||||
|
|
||||||
@check_completion_data_internal 3000
|
@check_completion_data_interval Application.compile_env(
|
||||||
|
:livebook,
|
||||||
|
:check_completion_data_interval,
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
@ -22,7 +26,10 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
requested_code: false,
|
requested_code: false,
|
||||||
org: nil,
|
org: nil,
|
||||||
verification_uri: nil,
|
verification_uri: nil,
|
||||||
org_form: nil
|
form: nil,
|
||||||
|
form_title: nil,
|
||||||
|
button_label: nil,
|
||||||
|
request_code_info: nil
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -85,85 +92,79 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.org_form
|
<div class="flex flex-col space-y-4">
|
||||||
form={@org_form}
|
<h2 class="text-xl text-gray-800 font-medium pb-2 border-b border-gray-200">
|
||||||
org={@org}
|
1. Select your option
|
||||||
requested_code={@requested_code}
|
</h2>
|
||||||
selected={@selected_option}
|
|
||||||
verification_uri={@verification_uri}
|
<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>
|
</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)
|
assigns = assign_new(assigns, :disabled, fn -> false end)
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div
|
<div
|
||||||
id={@id}
|
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})}
|
phx-click={JS.push("select_option", value: %{value: @id})}
|
||||||
>
|
>
|
||||||
<div class={[
|
<div class={[
|
||||||
|
@ -185,9 +186,6 @@ 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(id, id), do: "border-gray-200"
|
||||||
defp card_item_border_class(_, _), do: "border-gray-100"
|
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
|
def handle_event("select_option", %{"value" => option}, socket) do
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(selected_option: option)
|
|> assign(selected_option: option, requested_code: false, verification_uri: nil)
|
||||||
|> assign_form(option)}
|
|> assign_form(option)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("validate", %{"new_org" => attrs}, socket) do
|
def handle_event("validate", %{"org" => attrs}, socket) do
|
||||||
changeset =
|
changeset =
|
||||||
socket.assigns.org
|
socket.assigns.org
|
||||||
|> Teams.change_org(attrs)
|
|> Teams.change_org(attrs)
|
||||||
|
@ -211,14 +209,20 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
{:noreply, assign_form(socket, changeset)}
|
{:noreply, assign_form(socket, changeset)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("save", %{"new_org" => attrs}, socket) do
|
def handle_event("save", %{"org" => attrs}, socket) do
|
||||||
case Teams.create_org(socket.assigns.org, attrs) 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} ->
|
{:ok, response} ->
|
||||||
attrs = Map.merge(attrs, response)
|
attrs = Map.merge(attrs, response)
|
||||||
changeset = Teams.change_org(socket.assigns.org, attrs)
|
changeset = Teams.change_org(socket.assigns.org, attrs)
|
||||||
org = Ecto.Changeset.apply_action!(changeset, :insert)
|
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,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|
@ -237,7 +241,7 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
def handle_info(:check_completion_data, %{assigns: %{org: org}} = socket) do
|
def handle_info(:check_completion_data, %{assigns: %{org: org}} = socket) do
|
||||||
case Teams.get_org_request_completion_data(org) do
|
case Teams.get_org_request_completion_data(org) do
|
||||||
{:ok, :awaiting_confirmation} ->
|
{: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}
|
{:noreply, socket}
|
||||||
|
|
||||||
|
@ -264,10 +268,7 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(requested_code: false, org: org, verification_uri: nil)
|
|> assign(requested_code: false, org: org, verification_uri: nil)
|
||||||
|> put_flash(
|
|> put_flash(:error, "Oh no! Your org request expired, could you please try again?")
|
||||||
:error,
|
|
||||||
"Oh no! Your org creation request expired, could you please try again?"
|
|
||||||
)
|
|
||||||
|> assign_form(changeset)}
|
|> assign_form(changeset)}
|
||||||
|
|
||||||
{:transport_error, message} ->
|
{:transport_error, message} ->
|
||||||
|
@ -277,16 +278,37 @@ defmodule LivebookWeb.Hub.NewLive do
|
||||||
|
|
||||||
def handle_info(_any, socket), do: {:noreply, socket}
|
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: "💡"}
|
org = %Org{emoji: "💡"}
|
||||||
changeset = Teams.change_org(org)
|
changeset = Teams.change_org(org)
|
||||||
|
|
||||||
socket
|
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)
|
|> assign_form(changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ defmodule Livebook.TeamsTest do
|
||||||
use Livebook.TeamsIntegrationCase, async: true
|
use Livebook.TeamsIntegrationCase, async: true
|
||||||
|
|
||||||
alias Livebook.Teams
|
alias Livebook.Teams
|
||||||
|
alias Livebook.Teams.Org
|
||||||
|
|
||||||
describe "create_org/1" do
|
describe "create_org/1" do
|
||||||
test "returns the device flow data to confirm the org creation" do
|
test "returns the device flow data to confirm the org creation" do
|
||||||
|
@ -25,6 +26,41 @@ defmodule Livebook.TeamsTest do
|
||||||
end
|
end
|
||||||
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
|
describe "get_org_request_completion_data/1" do
|
||||||
test "returns the org data when it has been confirmed", %{node: node, user: user} do
|
test "returns the org data when it has been confirmed", %{node: node, user: user} do
|
||||||
teams_key = Teams.Org.teams_key()
|
teams_key = Teams.Org.teams_key()
|
||||||
|
|
|
@ -6,243 +6,6 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
||||||
|
|
||||||
alias Livebook.Hubs
|
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
|
describe "personal" do
|
||||||
setup do
|
setup do
|
||||||
Livebook.Hubs.subscribe([:secrets])
|
Livebook.Hubs.subscribe([:secrets])
|
||||||
|
@ -382,114 +145,4 @@ defmodule LivebookWeb.Hub.EditLiveTest do
|
||||||
assert hubs_html =~ ~p"/hub/#{hub.id}"
|
assert hubs_html =~ ~p"/hub/#{hub.id}"
|
||||||
assert hubs_html =~ hub.hub_name
|
assert hubs_html =~ hub.hub_name
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule LivebookWeb.Hub.NewLiveTest do
|
defmodule LivebookWeb.Hub.NewLiveTest do
|
||||||
use Livebook.TeamsIntegrationCase, async: true
|
use Livebook.TeamsIntegrationCase, async: true
|
||||||
|
|
||||||
|
alias Livebook.Teams.Org
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
test "render hub selection cards", %{conn: conn} do
|
test "render hub selection cards", %{conn: conn} do
|
||||||
|
@ -13,11 +15,12 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
||||||
|
|
||||||
describe "new-org" do
|
describe "new-org" do
|
||||||
test "persist a new hub", %{conn: conn, node: node, user: user} 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()
|
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")
|
{:ok, view, _html} = live(conn, ~p"/hub")
|
||||||
path = ~p"/hub/team-#{name}"
|
|
||||||
|
|
||||||
# select the new org option
|
# select the new org option
|
||||||
assert view
|
assert view
|
||||||
|
@ -25,31 +28,33 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
||||||
|> render_click() =~ "2. Create your Organization"
|
|> render_click() =~ "2. Create your Organization"
|
||||||
|
|
||||||
# builds the form data
|
# 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
|
# finds the form and change data
|
||||||
new_org_form = element(view, "#new-org-form")
|
form = element(view, "#new-org-form")
|
||||||
render_change(new_org_form, org_attrs)
|
render_change(form, attrs)
|
||||||
|
|
||||||
# submits the form
|
# submits the form
|
||||||
render_submit(new_org_form, org_attrs)
|
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
|
# check if the form has the url to confirm
|
||||||
link_element = element(view, "#new-org-form a")
|
link_element = element(view, "#new-org-form a")
|
||||||
html = render(link_element)
|
assert render(link_element) =~ "/org-request/#{org_request.id}/confirm"
|
||||||
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)
|
|
||||||
|
|
||||||
# force org request confirmation
|
# 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])
|
:erpc.call(node, Hub.Integration, :confirm_org_request, [org_request, user])
|
||||||
|
|
||||||
# wait for the c:handle_info/2 cycle
|
# wait for the c:handle_info/2 cycle
|
||||||
# check if the page redirected to edit hub page
|
# check if the page redirected to edit hub page
|
||||||
# and check the flash message
|
# 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
|
# checks if the hub is in the sidebar
|
||||||
{:ok, view, _html} = live(conn, path)
|
{:ok, view, _html} = live(conn, path)
|
||||||
|
@ -59,4 +64,65 @@ defmodule LivebookWeb.Hub.NewLiveTest do
|
||||||
assert hubs_html =~ name
|
assert hubs_html =~ name
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -77,9 +77,9 @@ defmodule Livebook.Factory do
|
||||||
%Livebook.Teams.Org{
|
%Livebook.Teams.Org{
|
||||||
id: nil,
|
id: nil,
|
||||||
emoji: "🏭",
|
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: nil
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue