mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 01:55:56 +08:00
Add end-to-end app deployment flow with Livebook Teams (#2602)
This commit is contained in:
parent
4d58f32668
commit
63668c49fb
|
@ -11,7 +11,7 @@ const deploy = args.includes("--deploy");
|
|||
|
||||
const outDir = path.resolve(
|
||||
__dirname,
|
||||
deploy ? "../static/assets" : "../tmp/static_dev/assets"
|
||||
deploy ? "../static/assets" : "../tmp/static_dev/assets",
|
||||
);
|
||||
|
||||
async function main() {
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
"scripts": {
|
||||
"deploy": "node build.js --deploy",
|
||||
"watch": "node build.js --watch",
|
||||
"format": "prettier --write '{js,test,css}/**/*.{js,json,css,md}' --no-error-on-unmatched-pattern",
|
||||
"format-check": "prettier --check '{js,test,css}/**/*.{js,json,css,md}' --no-error-on-unmatched-pattern",
|
||||
"format": "prettier --write '{js,test,css}/**/*.{js,json,css,md}' '*.js' --no-error-on-unmatched-pattern",
|
||||
"format-check": "prettier --check '{js,test,css}/**/*.{js,json,css,md}' '*.js' --no-error-on-unmatched-pattern",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const plugin = require("tailwindcss/plugin")
|
||||
const plugin = require("tailwindcss/plugin");
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
|
@ -102,6 +102,19 @@ module.exports = {
|
|||
},
|
||||
"brand-pink": "#e44c75",
|
||||
},
|
||||
keyframes: {
|
||||
shake: {
|
||||
"0%": { transform: "translateX(0)" },
|
||||
"20%": { transform: "translateX(-10px)" },
|
||||
"40%": { transform: "translateX(8px)" },
|
||||
"60%": { transform: "translateX(-6px)" },
|
||||
"80%": { transform: "translateX(4px)" },
|
||||
"100%": { transform: "translateX(0)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
shake: "shake 0.5s linear 0.2s",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
@ -110,9 +123,18 @@ module.exports = {
|
|||
addVariant("phx-connected", [".phx-connected&", ".phx-connected &"]);
|
||||
addVariant("phx-error", [".phx-error&", ".phx-error &"]);
|
||||
addVariant("phx-form-error", [":not(.phx-no-feedback).show-errors &"]);
|
||||
addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"]);
|
||||
addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"]);
|
||||
addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"]);
|
||||
})
|
||||
addVariant("phx-click-loading", [
|
||||
".phx-click-loading&",
|
||||
".phx-click-loading &",
|
||||
]);
|
||||
addVariant("phx-submit-loading", [
|
||||
".phx-submit-loading&",
|
||||
".phx-submit-loading &",
|
||||
]);
|
||||
addVariant("phx-change-loading", [
|
||||
".phx-change-loading&",
|
||||
".phx-change-loading &",
|
||||
]);
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -230,7 +230,7 @@ defmodule Livebook.Session.Data do
|
|||
| {:set_deployed_app_slug, client_id(), String.t()}
|
||||
| {:app_deactivate, client_id()}
|
||||
| {:app_shutdown, client_id()}
|
||||
| {:set_notebook_deployment_group, String.t()}
|
||||
| {:set_notebook_deployment_group, client_id(), String.t()}
|
||||
|
||||
@type action ::
|
||||
:connect_runtime
|
||||
|
|
|
@ -84,7 +84,7 @@ defmodule LivebookWeb.Confirm do
|
|||
<.modal id={@id} width={:medium} show={true}>
|
||||
<form
|
||||
id={"#{@id}-confirm-content"}
|
||||
class="p-6 flex flex-col"
|
||||
class="flex flex-col"
|
||||
phx-submit={JS.push("confirm") |> hide_modal(@id)}
|
||||
data-el-confirm-form
|
||||
>
|
||||
|
|
|
@ -55,7 +55,11 @@ defmodule LivebookWeb.CoreComponents do
|
|||
<.remix_icon icon="close-line" />
|
||||
</div>
|
||||
<.remix_icon :if={@kind == :info} icon="information-fill" class="text-xl text-blue-500" />
|
||||
<.remix_icon :if={@kind == :success} icon="checkbox-circle-fill" class="text-xl text-blue-500" />
|
||||
<.remix_icon
|
||||
:if={@kind == :success}
|
||||
icon="checkbox-circle-fill"
|
||||
class="text-xl text-green-bright-400"
|
||||
/>
|
||||
<.remix_icon :if={@kind == :warning} icon="alert-fill" class="text-xl text-yellow-500" />
|
||||
<.remix_icon :if={@kind == :error} icon="error-warning-fill" class="text-xl text-red-500" />
|
||||
<span class="whitespace-pre-wrap pr-2 max-h-52 overflow-y-auto tiny-scrollbar" phx-no-format><%= message %></span>
|
||||
|
@ -109,7 +113,7 @@ defmodule LivebookWeb.CoreComponents do
|
|||
<div class={[
|
||||
"shadow text-sm flex items-center space-x-3 rounded-lg px-4 py-2 border-l-4 rounded-l-none bg-white text-gray-700",
|
||||
@kind == :info && "border-blue-500",
|
||||
@kind == :success && "border-blue-500",
|
||||
@kind == :success && "border-green-bright-400",
|
||||
@kind == :warning && "border-yellow-300",
|
||||
@kind == :error && "border-red-500"
|
||||
]}>
|
||||
|
@ -186,7 +190,7 @@ defmodule LivebookWeb.CoreComponents do
|
|||
id={"#{@id}-content"}
|
||||
class={[
|
||||
"relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl",
|
||||
"w-full",
|
||||
"w-full p-6",
|
||||
modal_width_class(@width)
|
||||
]}
|
||||
role="dialog"
|
||||
|
@ -307,7 +311,7 @@ defmodule LivebookWeb.CoreComponents do
|
|||
<menu
|
||||
id={"#{@id}-content"}
|
||||
class={[
|
||||
"absolute z-[100] rounded-lg bg-white flex flex-col py-2 shadow-[0_15px_99px_-0px_rgba(12,24,41,0.15)] hidden",
|
||||
"absolute z-[100] hidden",
|
||||
menu_position_class(@position),
|
||||
@md_position && menu_md_position_class(@md_position),
|
||||
@sm_position && menu_sm_position_class(@sm_position),
|
||||
|
@ -316,21 +320,50 @@ defmodule LivebookWeb.CoreComponents do
|
|||
role="menu"
|
||||
phx-click-away={hide_menu(@id)}
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
<div
|
||||
id={"#{@id}-content-inner"}
|
||||
class="rounded-lg bg-white flex flex-col py-2 shadow-[0_15px_99px_-0px_rgba(12,24,41,0.15)]"
|
||||
>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</menu>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp show_menu(id) do
|
||||
JS.show(to: "##{id}-overlay")
|
||||
|> JS.show(to: "##{id}-content", display: "flex")
|
||||
|> JS.dispatch("lb:scroll_into_view", to: "##{id}-content")
|
||||
@doc """
|
||||
Shows a menu rendered with `menu/1`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:animate` - whether to play an animation when the menu is opened.
|
||||
Defaults to `false`
|
||||
|
||||
"""
|
||||
def show_menu(js \\ %JS{}, id, opts \\ []) do
|
||||
opts = Keyword.validate!(opts, animate: false)
|
||||
|
||||
js =
|
||||
js
|
||||
|> JS.show(to: "##{id}-overlay")
|
||||
|> JS.show(to: "##{id}-content", display: "flex")
|
||||
|> JS.dispatch("lb:scroll_into_view", to: "##{id}-content")
|
||||
|
||||
if opts[:animate] do
|
||||
JS.add_class(js, "animate-shake", to: "##{id}-content-inner")
|
||||
else
|
||||
js
|
||||
end
|
||||
end
|
||||
|
||||
defp hide_menu(id) do
|
||||
JS.hide(to: "##{id}-overlay")
|
||||
@doc """
|
||||
Hides a menu rendered with `menu/1`.
|
||||
"""
|
||||
def hide_menu(js \\ %JS{}, id) do
|
||||
js
|
||||
|> JS.hide(to: "##{id}-overlay")
|
||||
|> JS.hide(to: "##{id}-content")
|
||||
|> JS.remove_class("animate-shake", to: "##{id}-content-inner")
|
||||
end
|
||||
|
||||
defp menu_position_class(:top_left), do: "top-0 left-0 transform -translate-y-full -mt-1"
|
||||
|
|
|
@ -63,11 +63,15 @@ defmodule LivebookWeb.HTMLHelpers do
|
|||
@doc """
|
||||
Formats the given UTC datetime relatively to present.
|
||||
"""
|
||||
@spec format_datetime_relatively(DateTime.t()) :: String.t()
|
||||
def format_datetime_relatively(date) do
|
||||
@spec format_datetime_relatively(DateTime.t() | NaiveDateTime.t()) :: String.t()
|
||||
def format_datetime_relatively(%DateTime{} = date) do
|
||||
date |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words()
|
||||
end
|
||||
|
||||
def format_datetime_relatively(%NaiveDateTime{} = date) do
|
||||
Livebook.Utils.Time.time_ago_in_words(date)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of human readable messages for all upload and upload
|
||||
entry errors.
|
||||
|
|
|
@ -50,7 +50,7 @@ defmodule LivebookWeb.AppLive do
|
|||
</div>
|
||||
|
||||
<.modal id="sessions-modal" show width={:big} patch={~p"/apps"}>
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<%= @app.notebook_name %>
|
||||
</h3>
|
||||
|
|
|
@ -31,7 +31,7 @@ defmodule LivebookWeb.AppSessionLive.SourceComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
App source
|
||||
</h3>
|
||||
|
|
|
@ -330,7 +330,7 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
|
|||
|
||||
defp teams_key_modal(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Teams key
|
||||
</h3>
|
||||
|
|
|
@ -35,7 +35,7 @@ defmodule LivebookWeb.Hub.FileSystemFormComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<%= @title %>
|
||||
</h3>
|
||||
|
|
|
@ -33,7 +33,7 @@ defmodule LivebookWeb.Hub.SecretFormComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<%= @title %>
|
||||
</h3>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
defmodule LivebookWeb.Hub.Teams.DeploymentGroupInstanceComponent do
|
||||
defmodule LivebookWeb.Hub.Teams.DeploymentGroupAgentComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.Hubs
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, messages: [])}
|
||||
{:ok, assign(socket, messages: [], hide_title: false)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -40,8 +40,8 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupInstanceComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col gap-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 :if={not @hide_title} class="text-2xl font-semibold text-gray-800">
|
||||
App server setup
|
||||
</h3>
|
||||
|
|
@ -52,7 +52,7 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupComponent do
|
|||
</div>
|
||||
<!-- Overview -->
|
||||
<div :if={@deployment_group.mode == :online} class="flex flex-col lg:flex-row justify-center">
|
||||
<.labeled_text class="grow mt-6 lg:border-l lg:pl-4" label="App servers">
|
||||
<.labeled_text class="grow mt-6 lg:border-l border-gray-200 lg:pl-4" label="App servers">
|
||||
<span class="text-lg font-normal" aria-label="app servers">
|
||||
<%= @agents_count %>
|
||||
</span>
|
||||
|
@ -63,7 +63,7 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupComponent do
|
|||
+ Deploy
|
||||
</.link>
|
||||
</.labeled_text>
|
||||
<.labeled_text class="grow mt-6 lg:border-l lg:pl-4" label="Apps deployed">
|
||||
<.labeled_text class="grow mt-6 lg:border-l border-gray-200 lg:pl-4" label="Apps deployed">
|
||||
<span class="text-lg font-normal" aria-label="apps deployed">
|
||||
<%= @app_deployments_count %>
|
||||
</span>
|
||||
|
@ -74,7 +74,7 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupComponent do
|
|||
+ Add new
|
||||
</.link>
|
||||
</.labeled_text>
|
||||
<.labeled_text class="grow mt-6 lg:border-l lg:pl-4" label="Authentication">
|
||||
<.labeled_text class="grow mt-6 lg:border-l border-gray-200 lg:pl-4" label="Authentication">
|
||||
<span class="text-lg font-normal">
|
||||
<%= Livebook.ZTA.provider_name(@deployment_group.zta_provider) %>
|
||||
</span>
|
||||
|
@ -148,10 +148,11 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupComponent do
|
|||
patch={~p"/hub/#{@hub.id}"}
|
||||
>
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Teams.DeploymentGroupInstanceComponent}
|
||||
module={LivebookWeb.Hub.Teams.DeploymentGroupAgentComponent}
|
||||
id="deployment-group-agent-instance"
|
||||
hub={@hub}
|
||||
deployment_group={@deployment_group}
|
||||
return_to={nil}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
|
@ -162,7 +163,7 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupComponent do
|
|||
width={:medium}
|
||||
patch={~p"/hub/#{@hub.id}"}
|
||||
>
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
New app deployment
|
||||
</h3>
|
||||
|
|
|
@ -6,7 +6,7 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, form: nil, error_message: nil)}
|
||||
{:ok, assign(socket, form: nil, error_message: nil, hide_title: false, force_mode: nil)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -16,7 +16,14 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
if socket.assigns.form do
|
||||
{:ok, socket}
|
||||
else
|
||||
{:ok, assign_form(socket, change_deployment_group(socket, %{}))}
|
||||
attrs =
|
||||
if mode = socket.assigns.force_mode do
|
||||
%{mode: mode}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
{:ok, assign_form(socket, change_deployment_group(socket, attrs))}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -27,8 +34,8 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 :if={not @hide_title} class="text-2xl font-semibold text-gray-800">
|
||||
Add deployment group
|
||||
</h3>
|
||||
<div :if={@error_message} class="error-box">
|
||||
|
@ -50,12 +57,22 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
Type
|
||||
</label>
|
||||
<div class="flex gap-y-6 sm:gap-x-4">
|
||||
<.radio_card field={@form[:mode]} title="Online" value={:online}>
|
||||
<.radio_card
|
||||
field={@form[:mode]}
|
||||
title="Online"
|
||||
value={:online}
|
||||
disabled={@force_mode != nil}
|
||||
>
|
||||
Deploy notebooks to your infrastructure with the click of a button.
|
||||
This mode requires running app servers connected to Livebook Teams.
|
||||
</.radio_card>
|
||||
|
||||
<.radio_card field={@form[:mode]} title="Airgapped" value={:offline}>
|
||||
<.radio_card
|
||||
field={@form[:mode]}
|
||||
title="Airgapped"
|
||||
value={:offline}
|
||||
disabled={@force_mode}
|
||||
>
|
||||
Manually deploy notebooks to your infrastructure via Dockerfiles.
|
||||
Connection to Livebook Teams is not required.
|
||||
</.radio_card>
|
||||
|
@ -79,7 +96,7 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
<span class="font-normal">Add</span>
|
||||
</.button>
|
||||
|
||||
<.button color="gray" outlined patch={@return_to}>
|
||||
<.button :if={@return_to} color="gray" outlined patch={@return_to}>
|
||||
Cancel
|
||||
</.button>
|
||||
</div>
|
||||
|
@ -93,9 +110,9 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
defp radio_card(assigns) do
|
||||
~H"""
|
||||
<label class={[
|
||||
"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none w-1/2",
|
||||
to_string(@field.value) == to_string(@value) &&
|
||||
"border-blue-500 ring-1 ring-blue-500"
|
||||
"relative flex rounded-lg border p-4 w-1/2",
|
||||
if(to_string(@field.value) == to_string(@value), do: "border-blue-500", else: "border-gray-200"),
|
||||
if(@disabled, do: "opacity-70", else: "cursor-pointer")
|
||||
]}>
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -103,6 +120,7 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
value={@value}
|
||||
checked={to_string(@field.value) == to_string(@value)}
|
||||
class="sr-only"
|
||||
disabled={@disabled}
|
||||
/>
|
||||
<span class="flex flex-1">
|
||||
<span class="flex flex-col">
|
||||
|
@ -121,8 +139,6 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
if(to_string(@field.value) == to_string(@value), do: "visible", else: "invisible")
|
||||
]}
|
||||
/>
|
||||
<span class="pointer-events-none absolute -inset-px rounded-lg border-2" aria-hidden="true">
|
||||
</span>
|
||||
</label>
|
||||
"""
|
||||
end
|
||||
|
@ -133,10 +149,14 @@ defmodule LivebookWeb.Hub.Teams.DeploymentGroupFormComponent do
|
|||
|
||||
with {:ok, deployment_group} <- Ecto.Changeset.apply_action(changeset, :update),
|
||||
{:ok, _id} <- Teams.create_deployment_group(socket.assigns.hub, deployment_group) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, "Deployment group added successfully")
|
||||
|> push_patch(to: ~p"/hub/#{socket.assigns.hub.id}")}
|
||||
if return_to = socket.assigns.return_to do
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:success, "Deployment group added successfully")
|
||||
|> push_patch(to: return_to)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
else
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign_form(socket, Map.replace!(changeset, :action, :validate))}
|
||||
|
|
|
@ -181,6 +181,14 @@ defmodule LivebookWeb.SessionLive do
|
|||
{socket, %{context: params["context"]}}
|
||||
end
|
||||
|
||||
defp handle_params(:add_agent, params, _url, socket) do
|
||||
hub = Livebook.Hubs.fetch_hub!(socket.private.data.notebook.hub_id)
|
||||
deployment_groups = Livebook.Hubs.Provider.deployment_groups(hub)
|
||||
deployment_group_id = params["deployment_group_id"]
|
||||
deployment_group = Enum.find(deployment_groups, &(&1.id == deployment_group_id))
|
||||
{socket, %{deployment_group: deployment_group}}
|
||||
end
|
||||
|
||||
defp handle_params(:catch_all, %{"path_parts" => path_parts}, requested_url, socket) do
|
||||
path_parts =
|
||||
Enum.map(path_parts, fn
|
||||
|
|
|
@ -58,7 +58,7 @@ defmodule LivebookWeb.SessionLive.AppDockerComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-8">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
App deployment with Docker
|
||||
</h3>
|
||||
|
|
|
@ -65,9 +65,16 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
|
|||
<.button
|
||||
color="blue"
|
||||
patch={
|
||||
if Livebook.Notebook.AppSettings.valid?(@settings),
|
||||
do: ~p"/sessions/#{@session.id}/app-teams",
|
||||
else: ~p"/sessions/#{@session.id}/settings/app?context=app-teams"
|
||||
cond do
|
||||
Livebook.Hubs.Provider.type(@hub) != "team" ->
|
||||
~p"/sessions/#{@session.id}/app-teams-hub-info"
|
||||
|
||||
not Livebook.Notebook.AppSettings.valid?(@settings) ->
|
||||
~p"/sessions/#{@session.id}/settings/app?context=app-teams"
|
||||
|
||||
true ->
|
||||
~p"/sessions/#{@session.id}/app-teams"
|
||||
end
|
||||
}
|
||||
>
|
||||
<.remix_icon icon="rocket-line" /> Deploy with Livebook Teams
|
||||
|
|
|
@ -21,7 +21,7 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-8">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
App settings
|
||||
</h3>
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
defmodule LivebookWeb.SessionLive.AppTeamsComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket = assign(socket, assigns)
|
||||
deployment_groups = Livebook.Teams.get_deployment_groups(assigns.hub)
|
||||
app_deployments = Livebook.Teams.get_app_deployments(assigns.hub)
|
||||
|
||||
deployment_group =
|
||||
if assigns.deployment_group_id do
|
||||
Enum.find(deployment_groups, &(&1.id == assigns.deployment_group_id))
|
||||
end
|
||||
|
||||
app_deployment =
|
||||
if deployment_group do
|
||||
Enum.find(
|
||||
app_deployments,
|
||||
&(&1.slug == assigns.app_settings.slug and &1.deployment_group_id == deployment_group.id)
|
||||
)
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(
|
||||
settings_valid?: Livebook.Notebook.AppSettings.valid?(socket.assigns.app_settings),
|
||||
app_deployment: app_deployment,
|
||||
deployment_groups: deployment_groups,
|
||||
deployment_group: deployment_group,
|
||||
deployment_group_form: %{"deployment_group_id" => assigns.deployment_group_id},
|
||||
deployment_group_id: assigns.deployment_group_id
|
||||
)
|
||||
|> assign_new(:messages, fn -> [] end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
App deployment with Livebook Teams
|
||||
</h3>
|
||||
<.content
|
||||
file={@file}
|
||||
hub={@hub}
|
||||
app_deployment={@app_deployment}
|
||||
deployment_group={@deployment_group}
|
||||
deployment_groups={@deployment_groups}
|
||||
deployment_group_form={@deployment_group_form}
|
||||
deployment_group_id={@deployment_group_id}
|
||||
session={@session}
|
||||
messages={@messages}
|
||||
myself={@myself}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp content(%{settings_valid?: false} = assigns) do
|
||||
~H"""
|
||||
<div class="flex justify-between">
|
||||
<p class="text-gray-700">
|
||||
To deploy this app, make sure to specify valid settings.
|
||||
</p>
|
||||
<.link
|
||||
class="text-blue-600 font-medium"
|
||||
patch={~p"/sessions/#{@session.id}/settings/app?context=app-teams"}
|
||||
>
|
||||
<span>Configure</span>
|
||||
<.remix_icon icon="arrow-right-line" />
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp content(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-gray-700">
|
||||
You can deploy this app in the to your own cloud using Livebook Teams. To do that, select
|
||||
which deployment group you want to send this app and then click the button to Deploy.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-12">
|
||||
<p class="text-gray-700">
|
||||
<.label>Workspace</.label>
|
||||
<span>
|
||||
<span class="text-lg"><%= @hub.hub_emoji %></span>
|
||||
<span><%= @hub.hub_name %></span>
|
||||
</span>
|
||||
</p>
|
||||
<%= if @deployment_groups do %>
|
||||
<%= if @deployment_groups != [] do %>
|
||||
<.form
|
||||
for={@deployment_group_form}
|
||||
phx-change="select_deployment_group"
|
||||
phx-target={@myself}
|
||||
id="select_deployment_group_form"
|
||||
>
|
||||
<.select_field
|
||||
help={deployment_group_help()}
|
||||
field={@deployment_group_form[:deployment_group_id]}
|
||||
options={deployment_group_options(@deployment_groups)}
|
||||
label="Deployment Group"
|
||||
name="deployment_group_id"
|
||||
value={@deployment_group_id}
|
||||
/>
|
||||
</.form>
|
||||
<% else %>
|
||||
<p class="text-gray-700">
|
||||
<.label help={deployment_group_help()}>
|
||||
Deployment Group
|
||||
</.label>
|
||||
<span>None configured</span>
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div :if={@messages != []} class="flex flex-col gap-2">
|
||||
<.message_box :for={{kind, message} <- @messages} kind={kind}>
|
||||
<%= raw(message) %>
|
||||
</.message_box>
|
||||
</div>
|
||||
|
||||
<div :if={@app_deployment} class="space-y-3 pb-4">
|
||||
<p class="text-gray-700">Current deployed version:</p>
|
||||
|
||||
<ul class="text-gray-700 space-y-3">
|
||||
<li class="flex gap-2">
|
||||
<div class="font-bold">Title:</div>
|
||||
<span><%= @app_deployment.title %></span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<div class="font-bold">Deployed by:</div>
|
||||
<span><%= @app_deployment.deployed_by %></span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<div class="font-bold">Deployed at:</div>
|
||||
<span><%= @app_deployment.deployed_at %></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<.button
|
||||
id="deploy-livebook-agent-button"
|
||||
color="blue"
|
||||
phx-click="deploy_app"
|
||||
phx-target={@myself}
|
||||
>
|
||||
<.remix_icon icon="rocket-line" /> Deploy to Livebook Agent
|
||||
</.button>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("select_deployment_group", %{"deployment_group_id" => id}, socket) do
|
||||
id = if(id != "", do: id)
|
||||
Livebook.Session.set_notebook_deployment_group(socket.assigns.session.pid, id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("deploy_app", _, socket) do
|
||||
with {:ok, app_deployment} <- pack_app(socket),
|
||||
:ok <- deploy_app(socket, app_deployment) do
|
||||
message =
|
||||
"App deployment for #{app_deployment.slug} with title #{app_deployment.title} created successfully"
|
||||
|
||||
{:noreply, assign(socket, messages: [{:info, message}])}
|
||||
end
|
||||
end
|
||||
|
||||
defp pack_app(socket) do
|
||||
notebook = Livebook.Session.get_notebook(socket.assigns.session.pid)
|
||||
files_dir = socket.assigns.session.files_dir
|
||||
|
||||
case Livebook.Teams.AppDeployment.new(notebook, files_dir) do
|
||||
{:ok, app_deployment} ->
|
||||
{:ok, app_deployment}
|
||||
|
||||
{:warning, warnings} ->
|
||||
messages = Enum.map(warnings, &{:error, &1})
|
||||
{:noreply, assign(socket, messages: messages)}
|
||||
|
||||
{:error, error} ->
|
||||
error = "Failed to pack files: #{error}"
|
||||
{:noreply, assign(socket, messages: [{:error, error}])}
|
||||
end
|
||||
end
|
||||
|
||||
defp deploy_app(socket, app_deployment) do
|
||||
case Livebook.Teams.deploy_app(socket.assigns.hub, app_deployment) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, %{errors: errors}} ->
|
||||
errors = Enum.map(errors, fn {key, error} -> "#{key}: #{normalize_error(error)}" end)
|
||||
{:noreply, assign(socket, messages: errors)}
|
||||
|
||||
{:transport_error, error} ->
|
||||
{:noreply, assign(socket, messages: [{:error, error}])}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_error({msg, opts}) do
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(value))
|
||||
end)
|
||||
end
|
||||
|
||||
defp deployment_group_options(deployment_groups) do
|
||||
for deployment_group <- [%{name: "none", id: nil}] ++ deployment_groups,
|
||||
do: {deployment_group.name, deployment_group.id}
|
||||
end
|
||||
|
||||
defp deployment_group_help() do
|
||||
"Share deployment credentials, secrets, and configuration with deployment groups."
|
||||
end
|
||||
end
|
514
lib/livebook_web/live/session_live/app_teams_live.ex
Normal file
514
lib/livebook_web/live/session_live/app_teams_live.ex
Normal file
|
@ -0,0 +1,514 @@
|
|||
defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||
use LivebookWeb, :live_view
|
||||
|
||||
# We use a child LV, because we want to subscribe and react to many
|
||||
# hub-specific events, but they are only relevant while this view
|
||||
# is open.
|
||||
#
|
||||
# We also use the :action assign (similar to :live_action), instead
|
||||
# of relaying on URL changes. The main reason for this being, that
|
||||
# the initial action is conditional and we couldn't patch the URL
|
||||
# on mount.
|
||||
|
||||
alias Livebook.Session
|
||||
alias Livebook.Teams
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"session_pid" => session_pid}, socket) do
|
||||
session = Session.get_by_pid(session_pid)
|
||||
|
||||
%{
|
||||
hub_id: hub_id,
|
||||
app_settings: app_settings,
|
||||
deployment_group_id: deployment_group_id
|
||||
} = Session.get_notebook(session_pid)
|
||||
|
||||
hub = Livebook.Hubs.fetch_hub!(hub_id)
|
||||
|
||||
if connected?(socket) do
|
||||
Session.subscribe(session.id)
|
||||
|
||||
Teams.Broadcasts.subscribe([:deployment_groups, :app_deployments, :agents])
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(
|
||||
session: session,
|
||||
hub: hub,
|
||||
slug: app_settings.slug,
|
||||
settings_valid?: Livebook.Notebook.AppSettings.valid?(app_settings),
|
||||
messages: [],
|
||||
action: :deployment_groups
|
||||
)
|
||||
|> assign_deployment_groups()
|
||||
|> assign_app_deployments()
|
||||
|> assign_agents()
|
||||
|> assign_deployment_group(deployment_group_id)
|
||||
|> assign_app_deployment()
|
||||
|> assign_initial()
|
||||
|> navigate_if_no_deployment_groups()
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col space-y-8">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
App deployment with Livebook Teams
|
||||
</h3>
|
||||
<h4 :if={subtitle = subtitle(@action)} class="text-gray-600">
|
||||
<.remix_icon icon="corner-down-right-line" /> <%= subtitle %>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div :if={@messages != []} class="flex flex-col gap-2">
|
||||
<.message_box :for={{kind, message} <- @messages} kind={kind}>
|
||||
<%= raw(message) %>
|
||||
</.message_box>
|
||||
</div>
|
||||
|
||||
<.content
|
||||
hub={@hub}
|
||||
settings_valid?={@settings_valid?}
|
||||
app_deployment={@app_deployment}
|
||||
deployment_groups={@deployment_groups}
|
||||
num_agents={@num_agents}
|
||||
num_app_deployments={@num_app_deployments}
|
||||
deployment_group={@deployment_group}
|
||||
session={@session}
|
||||
messages={@messages}
|
||||
action={@action}
|
||||
initial?={@initial?}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp subtitle(:add_deployment_group), do: "Step: add deployment group"
|
||||
defp subtitle(:add_agent), do: "Step: add app server"
|
||||
defp subtitle(_), do: nil
|
||||
|
||||
defp content(%{settings_valid?: false} = assigns) do
|
||||
~H"""
|
||||
<div class="flex justify-between">
|
||||
<p class="text-gray-700">
|
||||
To deploy this app, make sure to specify valid settings.
|
||||
</p>
|
||||
<.link
|
||||
class="text-blue-600 font-medium"
|
||||
patch={~p"/sessions/#{@session.id}/settings/app?context=app-teams"}
|
||||
>
|
||||
<span>Configure</span>
|
||||
<.remix_icon icon="arrow-right-line" />
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp content(%{action: :add_deployment_group} = assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col gap-8">
|
||||
<.message_box kind={:info}>
|
||||
You must create a deployment group before deploying the app.
|
||||
</.message_box>
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Teams.DeploymentGroupFormComponent}
|
||||
id="add-deployment-group"
|
||||
session={@session}
|
||||
hub={@hub}
|
||||
return_to={nil}
|
||||
force_mode={:online}
|
||||
hide_title
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp content(%{action: :add_agent} = assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col gap-8">
|
||||
<.message_box :if={@initial?} kind={:info}>
|
||||
You must set up an app server for the app to run on.
|
||||
</.message_box>
|
||||
<div>
|
||||
<.live_component
|
||||
module={LivebookWeb.Hub.Teams.DeploymentGroupAgentComponent}
|
||||
id="add-agent"
|
||||
session={@session}
|
||||
hub={@hub}
|
||||
deployment_group={@deployment_group}
|
||||
hide_title
|
||||
/>
|
||||
<div class="mt-6 pt-6 border-t border-gray-200 flex flex-col gap-4">
|
||||
<h4 class="text-lg font-semibold text-gray-800">
|
||||
Status
|
||||
</h4>
|
||||
<%= if @num_agents[@deployment_group.id] do %>
|
||||
<.message_box kind={:success}>
|
||||
An app server is running, click "Deploy" to ship the app!
|
||||
</.message_box>
|
||||
<% else %>
|
||||
<.message_box kind={:info}>
|
||||
Awaiting an app server to be set up. If you deploy the app,
|
||||
it will only start once there is an app server to run it.
|
||||
</.message_box>
|
||||
<% end %>
|
||||
<div class="flex gap-2">
|
||||
<%= if @num_agents[@deployment_group.id] do %>
|
||||
<.button color="blue" phx-click="deploy_app">
|
||||
<.remix_icon icon="rocket-line" /> Deploy
|
||||
</.button>
|
||||
<% else %>
|
||||
<.button color="blue" outlined phx-click="deploy_app">
|
||||
<.remix_icon icon="rocket-line" /> Deploy anyway
|
||||
</.button>
|
||||
<% end %>
|
||||
<.button color="gray" outlined phx-click="go_deployment_groups">
|
||||
See deployment groups
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp content(%{action: :deployment_groups} = assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col gap-6">
|
||||
<p class="text-gray-700">
|
||||
Deploy this app to your cloud infrastructure. The app will be pushed to the Livebook
|
||||
Teams app servers associated with the deployment group that you choose.
|
||||
</p>
|
||||
|
||||
<%= if @deployment_group do %>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-gray-700">
|
||||
Using deployment group in <.workspace hub={@hub} /> workspace:
|
||||
</p>
|
||||
<button class="font-medium text-blue-600" phx-click="unselect_deployment_group">
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<.deployment_group_entry
|
||||
deployment_group={@deployment_group}
|
||||
num_agents={@num_agents}
|
||||
num_app_deployments={@num_app_deployments}
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :if={@app_deployment} class="space-y-3">
|
||||
<p class="text-gray-700">Currently deployed version:</p>
|
||||
<.app_deployment_card app_deployment={@app_deployment} />
|
||||
</div>
|
||||
|
||||
<.message_box :if={@num_agents[@deployment_group.id] == nil} kind={:warning}>
|
||||
The selected deployment group has no app servers. If you deploy the app,
|
||||
it will only start once there is an app server to run it.
|
||||
</.message_box>
|
||||
|
||||
<%= if @num_agents[@deployment_group.id] do %>
|
||||
<div>
|
||||
<.button color="blue" phx-click="deploy_app">
|
||||
<.remix_icon icon="rocket-line" /> Deploy
|
||||
</.button>
|
||||
</div>
|
||||
<% else %>
|
||||
<div>
|
||||
<.button color="blue" phx-click="go_add_agent">
|
||||
<.remix_icon icon="add-line" /> Add app server
|
||||
</.button>
|
||||
<.button color="blue" outlined phx-click="deploy_app">
|
||||
<.remix_icon icon="rocket-line" /> Deploy anyway
|
||||
</.button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<.no_entries :if={@deployment_groups == []}>
|
||||
No online deployment groups yet.
|
||||
<.link
|
||||
class="font-medium text-blue-600"
|
||||
patch={~p"/sessions/#{@session.id}/add-deployment-group"}
|
||||
>
|
||||
Add deployment group <.remix_icon icon="arrow-right-line" class="align-middle" />
|
||||
</.link>
|
||||
</.no_entries>
|
||||
|
||||
<div :if={@deployment_groups != []} class="flex flex-col gap-2">
|
||||
<p class="text-gray-700">
|
||||
Online deployment groups in <.workspace hub={@hub} /> workspace:
|
||||
</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<.deployment_group_entry
|
||||
:for={deployment_group <- @deployment_groups}
|
||||
deployment_group={deployment_group}
|
||||
num_agents={@num_agents}
|
||||
num_app_deployments={@num_app_deployments}
|
||||
selectable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp workspace(assigns) do
|
||||
~H"""
|
||||
<span class="font-medium">
|
||||
<span class="text-lg"><%= @hub.hub_emoji %></span>
|
||||
<span><%= @hub.hub_name %></span>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :active, :boolean, default: false
|
||||
attr :selectable, :boolean, default: false
|
||||
attr :deployment_group, :map, required: true
|
||||
attr :num_agents, :map, required: true
|
||||
attr :num_app_deployments, :map, required: true
|
||||
attr :rest, :global
|
||||
|
||||
defp deployment_group_entry(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class={[
|
||||
"border p-3 rounded-lg",
|
||||
@selectable && "cursor-pointer",
|
||||
if(@active,
|
||||
do: "border-blue-600 bg-blue-50",
|
||||
else: "border-gray-200"
|
||||
)
|
||||
]}
|
||||
phx-click={@selectable && "select_deployment_group"}
|
||||
phx-value-id={@deployment_group.id}
|
||||
{@rest}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex gap-2 items-center text-gray-700">
|
||||
<h3 class="text-sm">
|
||||
<span class="font-semibold"><%= @deployment_group.name %></span>
|
||||
(internal-domain.example.com)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="text-sm text-gray-700 border-l border-gray-300 pl-2">
|
||||
App servers: <%= @num_agents[@deployment_group.id] || 0 %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 border-l border-gray-300 pl-2">
|
||||
Apps deployed: <%= @num_app_deployments[@deployment_group.id] || 0 %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp app_deployment_card(assigns) do
|
||||
~H"""
|
||||
<div class="flex gap-4 sm:gap-12 border border-gray-200 rounded-lg p-4">
|
||||
<.labeled_text label="Title">
|
||||
<%= @app_deployment.title %>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Deployed by">
|
||||
<%= @app_deployment.deployed_by %>
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Deployed">
|
||||
<%= LivebookWeb.HTMLHelpers.format_datetime_relatively(@app_deployment.deployed_at) %> ago
|
||||
</.labeled_text>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("unselect_deployment_group", %{}, socket) do
|
||||
Livebook.Session.set_notebook_deployment_group(socket.assigns.session.pid, nil)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("select_deployment_group", %{"id" => id}, socket) do
|
||||
Livebook.Session.set_notebook_deployment_group(socket.assigns.session.pid, id)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("deploy_app", _, socket) do
|
||||
with {:ok, app_deployment} <- pack_app(socket),
|
||||
:ok <- deploy_app(socket, app_deployment) do
|
||||
message =
|
||||
"App deployment for #{app_deployment.slug} with title #{app_deployment.title} created successfully."
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> navigate(:deployment_groups)
|
||||
|> assign(messages: [{:success, message}])}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("go_add_agent", %{}, socket) do
|
||||
{:noreply, navigate(socket, :add_agent)}
|
||||
end
|
||||
|
||||
def handle_event("go_deployment_groups", %{}, socket) do
|
||||
{:noreply, navigate(socket, :deployment_groups)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({event, deployment_group}, socket)
|
||||
when event in [
|
||||
:deployment_group_created,
|
||||
:deployment_group_update,
|
||||
:deployment_group_deleted
|
||||
] and deployment_group.hub_id == socket.assigns.hub.id do
|
||||
current_deployment_group_id =
|
||||
if current_deployment_group = socket.assigns.deployment_group do
|
||||
current_deployment_group.id
|
||||
end
|
||||
|
||||
{socket, deployment_group_id} =
|
||||
if socket.assigns.initial? and event == :deployment_group_created do
|
||||
Livebook.Session.set_notebook_deployment_group(
|
||||
socket.assigns.session.pid,
|
||||
deployment_group.id
|
||||
)
|
||||
|
||||
{navigate(socket, :add_agent), deployment_group.id}
|
||||
else
|
||||
{socket, current_deployment_group_id}
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_deployment_groups()
|
||||
|> assign_deployment_group(deployment_group_id)
|
||||
|> navigate_if_no_deployment_groups()}
|
||||
end
|
||||
|
||||
def handle_info({event, agent}, socket)
|
||||
when event in [:agent_joined, :agent_left] and agent.hub_id == socket.assigns.hub.id do
|
||||
{:noreply, assign_agents(socket)}
|
||||
end
|
||||
|
||||
def handle_info({event, app_deployment}, socket)
|
||||
when event in [:app_deployment_started, :app_deployment_stopped] and
|
||||
app_deployment.hub_id == socket.assigns.hub.id do
|
||||
{:noreply, socket |> assign_app_deployments() |> assign_app_deployment()}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
{:operation, {:set_notebook_deployment_group, _client_id, deployment_group_id}},
|
||||
socket
|
||||
) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_deployment_group(deployment_group_id)
|
||||
|> assign_app_deployment()}
|
||||
end
|
||||
|
||||
def handle_info(_message, socket), do: {:noreply, socket}
|
||||
|
||||
defp assign_deployment_groups(socket) do
|
||||
deployment_groups =
|
||||
socket.assigns.hub
|
||||
|> Teams.get_deployment_groups()
|
||||
|> Enum.filter(&(&1.mode == :online))
|
||||
|> Enum.sort_by(& &1.name)
|
||||
|
||||
assign(socket, deployment_groups: deployment_groups)
|
||||
end
|
||||
|
||||
defp assign_app_deployments(socket) do
|
||||
app_deployments = Teams.get_app_deployments(socket.assigns.hub)
|
||||
num_app_deployments = Enum.frequencies_by(app_deployments, & &1.deployment_group_id)
|
||||
assign(socket, app_deployments: app_deployments, num_app_deployments: num_app_deployments)
|
||||
end
|
||||
|
||||
defp assign_agents(socket) do
|
||||
agents = Teams.get_agents(socket.assigns.hub)
|
||||
num_agents = Enum.frequencies_by(agents, & &1.deployment_group_id)
|
||||
assign(socket, num_agents: num_agents)
|
||||
end
|
||||
|
||||
defp assign_deployment_group(socket, deployment_group_id) do
|
||||
deployment_group =
|
||||
if deployment_group_id do
|
||||
Enum.find(socket.assigns.deployment_groups, &(&1.id == deployment_group_id))
|
||||
end
|
||||
|
||||
assign(socket, deployment_group: deployment_group)
|
||||
end
|
||||
|
||||
defp assign_app_deployment(socket) do
|
||||
app_deployment =
|
||||
if deployment_group = socket.assigns.deployment_group do
|
||||
Enum.find(
|
||||
socket.assigns.app_deployments,
|
||||
&(&1.slug == socket.assigns.slug and &1.deployment_group_id == deployment_group.id)
|
||||
)
|
||||
end
|
||||
|
||||
assign(socket, app_deployment: app_deployment)
|
||||
end
|
||||
|
||||
defp assign_initial(socket) do
|
||||
assign(socket, initial?: socket.assigns.deployment_groups == [])
|
||||
end
|
||||
|
||||
defp navigate(socket, action)
|
||||
when action in [:deployment_groups, :add_deployment_group, :add_agent] do
|
||||
assign(socket, action: action, messages: [])
|
||||
end
|
||||
|
||||
defp navigate_if_no_deployment_groups(socket) do
|
||||
if socket.assigns.deployment_groups == [] do
|
||||
navigate(socket, :add_deployment_group)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp pack_app(socket) do
|
||||
notebook = Livebook.Session.get_notebook(socket.assigns.session.pid)
|
||||
files_dir = socket.assigns.session.files_dir
|
||||
|
||||
case Teams.AppDeployment.new(notebook, files_dir) do
|
||||
{:ok, app_deployment} ->
|
||||
{:ok, app_deployment}
|
||||
|
||||
{:warning, warnings} ->
|
||||
messages = Enum.map(warnings, &{:error, &1})
|
||||
{:noreply, assign(socket, messages: messages)}
|
||||
|
||||
{:error, error} ->
|
||||
error = "Failed to pack files: #{error}"
|
||||
{:noreply, assign(socket, messages: [{:error, error}])}
|
||||
end
|
||||
end
|
||||
|
||||
defp deploy_app(socket, app_deployment) do
|
||||
case Teams.deploy_app(socket.assigns.hub, app_deployment) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, %{errors: errors}} ->
|
||||
errors = Enum.map(errors, fn {key, error} -> "#{key}: #{normalize_error(error)}" end)
|
||||
{:noreply, assign(socket, messages: errors)}
|
||||
|
||||
{:transport_error, error} ->
|
||||
{:noreply, assign(socket, messages: [{:error, error}])}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_error({msg, opts}) do
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(value))
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -27,7 +27,7 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Bin
|
||||
</h3>
|
||||
|
|
|
@ -20,7 +20,7 @@ defmodule LivebookWeb.SessionLive.CodeCellSettingsComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-8">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Cell settings
|
||||
</h3>
|
||||
|
|
|
@ -19,7 +19,7 @@ defmodule LivebookWeb.SessionLive.ExportComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Export
|
||||
</h3>
|
||||
|
|
|
@ -34,7 +34,7 @@ defmodule LivebookWeb.SessionLive.InsertImageComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-8">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Insert image
|
||||
</h3>
|
||||
|
|
|
@ -21,7 +21,7 @@ defmodule LivebookWeb.SessionLive.PackageSearchLive do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Search packages
|
||||
</h3>
|
||||
|
|
|
@ -63,7 +63,7 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-8">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Save to file
|
||||
</h3>
|
||||
|
|
|
@ -25,7 +25,7 @@ defmodule LivebookWeb.SessionLive.RenameFileEntryComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Rename file
|
||||
</h3>
|
||||
|
|
|
@ -116,21 +116,25 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
/>
|
||||
</.modal>
|
||||
|
||||
<.modal :if={@live_action == :app_teams} id="app-teams-modal" show width={:big} patch={@self_path}>
|
||||
<%= live_render(@socket, LivebookWeb.SessionLive.AppTeamsLive,
|
||||
id: "app-teams",
|
||||
session: %{
|
||||
"session_pid" => @session.pid
|
||||
}
|
||||
) %>
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :app_teams}
|
||||
id="app-teams-modal"
|
||||
:if={@live_action == :app_teams_hub_info}
|
||||
id="app-teams-hub-info-modal"
|
||||
show
|
||||
width={:medium}
|
||||
width={:big}
|
||||
patch={@self_path}
|
||||
>
|
||||
<.live_component
|
||||
module={LivebookWeb.SessionLive.AppTeamsComponent}
|
||||
id="app-teams"
|
||||
<.app_teams_hub_info_content
|
||||
any_team_hub?={Enum.any?(@saved_hubs, &(Livebook.Hubs.Provider.type(&1.provider) == "team"))}
|
||||
session={@session}
|
||||
hub={@data_view.hub}
|
||||
file={@data_view.file}
|
||||
app_settings={@data_view.app_settings}
|
||||
deployment_group_id={@data_view.deployment_group_id}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
|
@ -430,6 +434,7 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
app={@app}
|
||||
deployed_app_slug={@data_view.deployed_app_slug}
|
||||
any_session_secrets?={@data_view.any_session_secrets?}
|
||||
hub={@data_view.hub}
|
||||
/>
|
||||
</div>
|
||||
<div data-el-runtime-info>
|
||||
|
@ -794,9 +799,42 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
"""
|
||||
end
|
||||
|
||||
defp app_teams_hub_info_content(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col space-y-4">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
App deployment with Livebook Teams
|
||||
</h3>
|
||||
|
||||
<%= if @any_team_hub? do %>
|
||||
<.message_box kind={:info}>
|
||||
In order to deploy your app using Livebook Teams, you need to select a Livebook Teams
|
||||
workspace. To change the workspace, use the dropdown right below the notebook title.
|
||||
<.link
|
||||
class="text-blue-600 font-medium"
|
||||
patch={~p"/sessions/#{@session.id}"}
|
||||
phx-click={show_menu(%JS{}, "notebook-hub-menu", animate: true)}
|
||||
>
|
||||
<span>Change workspace</span>
|
||||
<.remix_icon icon="arrow-right-line" />
|
||||
</.link>
|
||||
</.message_box>
|
||||
<% else %>
|
||||
<.message_box kind={:info}>
|
||||
In order to deploy your app using Livebook Teams, you need to create an organization.
|
||||
<.link class="text-blue-600 font-medium" patch={~p"/hub"}>
|
||||
<span>Add organization</span>
|
||||
<.remix_icon icon="arrow-right-line" />
|
||||
</.link>
|
||||
</.message_box>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def add_file_entry_content(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Add file
|
||||
</h3>
|
||||
|
|
|
@ -24,7 +24,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 max-w-4xl flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Runtime settings
|
||||
</h3>
|
||||
|
|
|
@ -37,7 +37,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 w-full flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<%= @title %>
|
||||
</h3>
|
||||
|
|
|
@ -155,7 +155,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
Keyboard shortcuts
|
||||
</h3>
|
||||
|
|
|
@ -24,7 +24,7 @@ defmodule LivebookWeb.SettingsLive.EnvVarComponent do
|
|||
assigns = assign_new(assigns, :on_save, fn -> "save" end)
|
||||
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
<%= if @operation == :new, do: "Add environment variable", else: "Edit environment variable" %>
|
||||
</h3>
|
||||
|
|
|
@ -18,7 +18,7 @@ defmodule LivebookWeb.UserComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6 flex flex-col space-y-5">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<h3 class="text-2xl font-semibold text-gray-800">
|
||||
User profile
|
||||
</h3>
|
||||
|
|
|
@ -109,6 +109,7 @@ defmodule LivebookWeb.Router do
|
|||
live "/sessions/:id/settings/app", SessionLive, :app_settings
|
||||
live "/sessions/:id/app-docker", SessionLive, :app_docker
|
||||
live "/sessions/:id/app-teams", SessionLive, :app_teams
|
||||
live "/sessions/:id/app-teams-hub-info", SessionLive, :app_teams_hub_info
|
||||
live "/sessions/:id/add-file/:tab", SessionLive, :add_file_entry
|
||||
live "/sessions/:id/rename-file/:name", SessionLive, :rename_file_entry
|
||||
live "/sessions/:id/bin", SessionLive, :bin
|
||||
|
|
|
@ -189,7 +189,7 @@ defmodule LivebookWeb.Integration.Hub.DeploymentGroupTest do
|
|||
end
|
||||
|
||||
test "shows the agent count", %{conn: conn, hub: hub} do
|
||||
%{id: id} = insert_deployment_group(mode: :online, hub_id: hub.id)
|
||||
%{id: id} = deployment_group = insert_deployment_group(mode: :online, hub_id: hub.id)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
|
||||
|
||||
|
@ -200,23 +200,7 @@ defmodule LivebookWeb.Integration.Hub.DeploymentGroupTest do
|
|||
|> Floki.text()
|
||||
|> String.trim() == "0"
|
||||
|
||||
org_id = to_string(hub.org_id)
|
||||
|
||||
# Simulates the agent join event
|
||||
pid = Livebook.Hubs.TeamClient.get_pid(hub.id)
|
||||
agent = build(:agent, hub_id: hub.id, org_id: org_id, deployment_group_id: to_string(id))
|
||||
|
||||
livebook_proto_agent =
|
||||
%LivebookProto.Agent{
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
org_id: agent.org_id,
|
||||
deployment_group_id: agent.deployment_group_id
|
||||
}
|
||||
|
||||
livebook_proto_agent_joined = %LivebookProto.AgentJoined{agent: livebook_proto_agent}
|
||||
send(pid, {:event, :agent_joined, livebook_proto_agent_joined})
|
||||
assert_receive {:agent_joined, ^agent}
|
||||
simulate_agent_join(hub, deployment_group)
|
||||
|
||||
assert view
|
||||
|> element("#hub-deployment-group-#{id} [aria-label=\"app servers\"]")
|
||||
|
|
|
@ -378,7 +378,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "deployment group for app deployment" do
|
||||
describe "offline deployment with docker" do
|
||||
@tag :tmp_dir
|
||||
test "show deployment group on app deployment",
|
||||
%{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
|
||||
|
@ -475,61 +475,143 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
assert render(view) =~ "None configured"
|
||||
refute has_element?(view, "#select_deployment_group_form")
|
||||
end
|
||||
end
|
||||
|
||||
@tag :tmp_dir
|
||||
test "deploys the app to livebook teams api",
|
||||
%{conn: conn, user: user, node: node, session: session, tmp_dir: tmp_dir} do
|
||||
team = create_team_hub(user, node)
|
||||
Session.set_notebook_hub(session.pid, team.id)
|
||||
describe "online deployment" do
|
||||
test "shows a message when non-teams hub is selected",
|
||||
%{conn: conn, user: user, node: node, session: session} do
|
||||
create_team_hub(user, node)
|
||||
|
||||
notebook_path = Path.join(tmp_dir, "notebook.livemd")
|
||||
file = FileSystem.File.local(notebook_path)
|
||||
Session.set_file(session.pid, file)
|
||||
|
||||
slug = Livebook.Utils.random_short_id()
|
||||
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
|
||||
Session.set_app_settings(session.pid, app_settings)
|
||||
|
||||
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id)
|
||||
Session.set_notebook_deployment_group(session.pid, deployment_group.id)
|
||||
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")
|
||||
|
||||
assert render(view) =~ "Deploy to Livebook Agent"
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#deploy-livebook-agent-button")
|
||||
|> element("a", "Deploy with Livebook Teams")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~
|
||||
"App deployment for #{slug} with title Untitled notebook created successfully"
|
||||
"In order to deploy your app using Livebook Teams, you need to select"
|
||||
end
|
||||
|
||||
test "deploys the app to livebook teams api without saving the file",
|
||||
test "deployment flow with no deployment groups in the hub",
|
||||
%{conn: conn, user: user, node: node, session: session} do
|
||||
team = create_team_hub(user, node)
|
||||
Session.set_notebook_hub(session.pid, team.id)
|
||||
|
||||
slug = Livebook.Utils.random_short_id()
|
||||
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
|
||||
Session.set_app_settings(session.pid, app_settings)
|
||||
|
||||
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id)
|
||||
Session.set_notebook_deployment_group(session.pid, deployment_group.id)
|
||||
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")
|
||||
|
||||
assert render(view) =~ "Deploy to Livebook Agent"
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("#deploy-livebook-agent-button")
|
||||
|> element("a", "Deploy with Livebook Teams")
|
||||
|> render_click()
|
||||
|
||||
# Step: configuring valid app settings
|
||||
|
||||
assert render(view) =~ "You must configure your app before deploying it."
|
||||
|
||||
slug = Livebook.Utils.random_short_id()
|
||||
|
||||
view
|
||||
|> element(~s/#app-settings-modal form/)
|
||||
|> render_submit(%{"app_settings" => %{"slug" => slug}})
|
||||
|
||||
# From this point forward we are in a child LV
|
||||
view = find_live_child(view, "app-teams")
|
||||
assert render(view) =~ "App deployment with Livebook Teams"
|
||||
|
||||
# Step: deployment group creation
|
||||
|
||||
assert render(view) =~ "Step: add deployment group"
|
||||
assert render(view) =~ "You must create a deployment group before deploying the app."
|
||||
|
||||
view
|
||||
|> element(~s/#add-deployment-group-form/)
|
||||
|> render_submit(%{"deployment_group" => %{"name" => "test"}})
|
||||
|
||||
# Step: agent instance setup
|
||||
|
||||
assert render(view) =~ "Step: add app server"
|
||||
assert render(view) =~ "You must set up an app server for the app to run on."
|
||||
|
||||
assert render(view) =~ "Awaiting an app server to be set up."
|
||||
|
||||
[deployment_group] = Livebook.Hubs.TeamClient.get_deployment_groups(team.id)
|
||||
simulate_agent_join(team, deployment_group)
|
||||
|
||||
assert render(view) =~ "An app server is running"
|
||||
|
||||
# Step: deploy
|
||||
|
||||
view
|
||||
|> element("button", "Deploy")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~
|
||||
"App deployment for #{slug} with title Untitled notebook created successfully"
|
||||
end
|
||||
|
||||
test "returns error when the deployment size is higher than the maximum size of 20MB",
|
||||
test "deployment flow with existing deployment groups in the hub",
|
||||
%{conn: conn, user: user, node: node, session: session} do
|
||||
team = create_team_hub(user, node)
|
||||
Session.set_notebook_hub(session.pid, team.id)
|
||||
|
||||
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id)
|
||||
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
|
||||
view
|
||||
|> element("a", "Deploy with Livebook Teams")
|
||||
|> render_click()
|
||||
|
||||
# Step: configuring valid app settings
|
||||
|
||||
assert render(view) =~ "You must configure your app before deploying it."
|
||||
|
||||
slug = Livebook.Utils.random_short_id()
|
||||
|
||||
view
|
||||
|> element(~s/#app-settings-modal form/)
|
||||
|> render_submit(%{"app_settings" => %{"slug" => slug}})
|
||||
|
||||
# From this point forward we are in a child LV
|
||||
view = find_live_child(view, "app-teams")
|
||||
assert render(view) =~ "App deployment with Livebook Teams"
|
||||
|
||||
# Step: selecting deployment group
|
||||
|
||||
view
|
||||
|> element(~s/[phx-click="select_deployment_group"][phx-value-id="#{deployment_group.id}"]/)
|
||||
|> render_click()
|
||||
|
||||
%{id: id} = deployment_group
|
||||
assert_receive {:operation, {:set_notebook_deployment_group, _, ^id}}
|
||||
|
||||
assert render(view) =~ "The selected deployment group has no app servers."
|
||||
|
||||
view
|
||||
|> element(~s/button/, "Add app server")
|
||||
|> render_click()
|
||||
|
||||
# Step: agent instance setup
|
||||
|
||||
assert render(view) =~ "Step: add app server"
|
||||
|
||||
assert render(view) =~ "Awaiting an app server to be set up."
|
||||
|
||||
[deployment_group] = Livebook.Hubs.TeamClient.get_deployment_groups(team.id)
|
||||
simulate_agent_join(team, deployment_group)
|
||||
|
||||
assert render(view) =~ "An app server is running"
|
||||
|
||||
# Step: deploy
|
||||
|
||||
view
|
||||
|> element("button", "Deploy")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~
|
||||
"App deployment for #{slug} with title Untitled notebook created successfully"
|
||||
end
|
||||
|
||||
test "shows an error when the deployment size is higher than the maximum size of 20MB",
|
||||
%{conn: conn, user: user, node: node, session: session} do
|
||||
team = create_team_hub(user, node)
|
||||
Session.set_notebook_hub(session.pid, team.id)
|
||||
|
@ -548,10 +630,12 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")
|
||||
|
||||
assert render(view) =~ "Deploy to Livebook Agent"
|
||||
# From this point forward we are in a child LV
|
||||
view = find_live_child(view, "app-teams")
|
||||
assert render(view) =~ "App deployment with Livebook Teams"
|
||||
|
||||
view
|
||||
|> element("#deploy-livebook-agent-button")
|
||||
|> element("button", "Deploy")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~
|
||||
|
|
|
@ -254,6 +254,33 @@ defmodule Livebook.HubHelpers do
|
|||
:erpc.call(node, TeamsRPC, fun, args)
|
||||
end
|
||||
|
||||
def simulate_agent_join(hub, deployment_group) do
|
||||
Livebook.Teams.Broadcasts.subscribe([:agents])
|
||||
|
||||
# Simulates the agent join event
|
||||
pid = Livebook.Hubs.TeamClient.get_pid(hub.id)
|
||||
|
||||
agent =
|
||||
build(:agent,
|
||||
hub_id: hub.id,
|
||||
org_id: to_string(hub.org_id),
|
||||
deployment_group_id: to_string(deployment_group.id)
|
||||
)
|
||||
|
||||
livebook_proto_agent =
|
||||
%LivebookProto.Agent{
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
org_id: agent.org_id,
|
||||
deployment_group_id: agent.deployment_group_id
|
||||
}
|
||||
|
||||
livebook_proto_agent_joined = %LivebookProto.AgentJoined{agent: livebook_proto_agent}
|
||||
send(pid, {:event, :agent_joined, livebook_proto_agent_joined})
|
||||
|
||||
assert_receive {:agent_joined, ^agent}
|
||||
end
|
||||
|
||||
defp hub_pid(hub) do
|
||||
if pid = GenServer.whereis({:via, Registry, {Livebook.HubsRegistry, hub.id}}) do
|
||||
{:ok, pid}
|
||||
|
|
Loading…
Reference in a new issue