Show link to deployed apps after deploy (#2658)

This commit is contained in:
Alexandre de Souza 2024-06-18 10:12:20 -03:00 committed by GitHub
parent ca30cbc509
commit 22abd815eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 62 additions and 23 deletions

View file

@ -274,6 +274,11 @@ defmodule Livebook.Hubs.TeamClient do
{:noreply, handle_event(topic, data, state)} {:noreply, handle_event(topic, data, state)}
end end
def handle_info({:apps_manager_status, _}, state)
when not state.connected? or state.deployment_group_id == nil do
{:noreply, state}
end
def handle_info({:apps_manager_status, entries}, %{hub: %{id: id}} = state) do def handle_info({:apps_manager_status, entries}, %{hub: %{id: id}} = state) do
app_deployment_statuses = app_deployment_statuses =
for %{app_spec: %Apps.TeamsAppSpec{hub_id: ^id} = app_spec, running?: running?} <- entries do for %{app_spec: %Apps.TeamsAppSpec{hub_id: ^id} = app_spec, running?: running?} <- entries do

View file

@ -9,7 +9,7 @@ defmodule Livebook.Teams.Connection do
@no_state :no_state @no_state :no_state
@loop_ping_delay 5_000 @loop_ping_delay 5_000
@websocket_messages [:ssl, :tcp, :ssl_closed, :tcp_closed, :ssl_error, :tcp_error] @expected_messages [:ssl, :tcp, :ssl_closed, :tcp_closed, :ssl_error, :tcp_error]
defstruct [:listener, :headers, :http_conn, :websocket, :ref] defstruct [:listener, :headers, :http_conn, :websocket, :ref]
@ -39,13 +39,13 @@ defmodule Livebook.Teams.Connection do
@impl true @impl true
def handle_event(event_type, event_data, state, data) def handle_event(event_type, event_data, state, data)
def handle_event(:internal, :connect, @no_state, %__MODULE__{} = data) do def handle_event(:internal, :connect, @no_state, data) do
case WebSocket.connect(data.headers) do case WebSocket.connect(data.headers) do
{:ok, conn, websocket, ref} -> {:ok, conn, websocket, ref} ->
send(data.listener, :connected) send(data.listener, :connected)
send(self(), {:loop_ping, ref}) send(self(), {:loop_ping, ref})
{:keep_state, %__MODULE__{data | http_conn: conn, ref: ref, websocket: websocket}} {:keep_state, %{data | http_conn: conn, ref: ref, websocket: websocket}}
{:transport_error, reason} -> {:transport_error, reason} ->
send(data.listener, {:connection_error, reason}) send(data.listener, {:connection_error, reason})
@ -63,14 +63,14 @@ defmodule Livebook.Teams.Connection do
{:keep_state_and_data, {:next_event, :internal, :connect}} {:keep_state_and_data, {:next_event, :internal, :connect}}
end end
def handle_event(:info, {:loop_ping, ref}, @no_state, %__MODULE__{ref: ref} = data) do def handle_event(:info, {:loop_ping, ref}, @no_state, %{ref: ref} = data) do
case WebSocket.send(data.http_conn, data.websocket, data.ref, :ping) do case WebSocket.send(data.http_conn, data.websocket, data.ref, :ping) do
{:ok, conn, websocket} -> {:ok, conn, websocket} ->
Process.send_after(self(), {:loop_ping, data.ref}, @loop_ping_delay) Process.send_after(self(), {:loop_ping, data.ref}, @loop_ping_delay)
{:keep_state, %__MODULE__{data | http_conn: conn, websocket: websocket}} {:keep_state, %{data | http_conn: conn, websocket: websocket}}
{:error, conn, websocket, _reason} -> {:error, conn, websocket, _reason} ->
{:keep_state, %__MODULE__{data | http_conn: conn, websocket: websocket}} {:keep_state, %{data | http_conn: conn, websocket: websocket}}
end end
end end
@ -78,11 +78,15 @@ defmodule Livebook.Teams.Connection do
:keep_state_and_data :keep_state_and_data
end end
def handle_event(:info, message, @no_state, %__MODULE__{} = data) def handle_event(:info, message, @no_state, data) when elem(message, 0) in @expected_messages do
when elem(message, 0) in @websocket_messages do
handle_websocket_message(message, data) handle_websocket_message(message, data)
end end
def handle_event(:info, message, @no_state, %{http_conn: nil})
when elem(message, 0) in @expected_messages do
:keep_state_and_data
end
def handle_event(:info, _message, @no_state, _data) do def handle_event(:info, _message, @no_state, _data) do
:keep_state_and_data :keep_state_and_data
end end
@ -94,7 +98,7 @@ defmodule Livebook.Teams.Connection do
{:keep_state, %{data | http_conn: conn, websocket: websocket}} {:keep_state, %{data | http_conn: conn, websocket: websocket}}
{:error, conn, websocket, reason} -> {:error, conn, websocket, reason} ->
data = %__MODULE__{data | http_conn: conn, websocket: websocket} data = %{data | http_conn: conn, websocket: websocket}
send(data.listener, {:connection_error, reason}) send(data.listener, {:connection_error, reason})
:gen_statem.reply(from, {:error, reason}) :gen_statem.reply(from, {:error, reason})
@ -104,10 +108,10 @@ defmodule Livebook.Teams.Connection do
# Private # Private
defp handle_websocket_message(message, %__MODULE__{} = data) do defp handle_websocket_message(message, data) do
case WebSocket.receive(data.http_conn, data.ref, data.websocket, message) do case WebSocket.receive(data.http_conn, data.ref, data.websocket, message) do
{:ok, conn, websocket, binaries} -> {:ok, conn, websocket, binaries} ->
data = %__MODULE__{data | http_conn: conn, websocket: websocket} data = %{data | http_conn: conn, websocket: websocket}
for binary <- binaries do for binary <- binaries do
%{type: {topic, message}} = LivebookProto.Event.decode(binary) %{type: {topic, message}} = LivebookProto.Event.decode(binary)
@ -118,7 +122,7 @@ defmodule Livebook.Teams.Connection do
{:error, conn, websocket, reason} -> {:error, conn, websocket, reason} ->
send(data.listener, {:connection_error, reason}) send(data.listener, {:connection_error, reason})
data = %__MODULE__{data | http_conn: conn, websocket: websocket} data = %{data | http_conn: conn, websocket: websocket}
{:keep_state, data, {:next_event, :internal, :connect}} {:keep_state, data, {:next_event, :internal, :connect}}
end end

View file

@ -108,7 +108,13 @@ defmodule LivebookWeb.CoreComponents do
slot :inner_block slot :inner_block
def message_box(assigns)
def message_box(assigns) do def message_box(assigns) do
if assigns.message && assigns.inner_block != [] do
raise ArgumentError, "expected either message or inner_block, got both."
end
~H""" ~H"""
<div class={[ <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", "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",
@ -122,9 +128,9 @@ defmodule LivebookWeb.CoreComponents do
class="whitespace-pre-wrap pr-2 max-h-52 overflow-y-auto tiny-scrollbar" class="whitespace-pre-wrap pr-2 max-h-52 overflow-y-auto tiny-scrollbar"
phx-no-format phx-no-format
><%= @message %></div> ><%= @message %></div>
<div :if={@inner_block}> <%= if @inner_block != [] do %>
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>
</div> <% end %>
</div> </div>
""" """
end end

View file

@ -67,7 +67,19 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
<div :if={@messages != []} class="flex flex-col gap-2"> <div :if={@messages != []} class="flex flex-col gap-2">
<.message_box :for={{kind, message} <- @messages} kind={kind}> <.message_box :for={{kind, message} <- @messages} kind={kind}>
<%= raw(message) %> <div class="flex flex-auto items-center justify-between">
<span class="whitespace-pre-wrap"><%= raw(message) %></span>
<.link
:if={kind == :success}
href={"#{Livebook.Config.teams_url()}/orgs/#{@hub.org_id}"}
target="_blank"
class="font-medium text-blue-600"
>
<span>See all deployed apps</span>
<.remix_icon icon="external-link-line" />
</.link>
</div>
</.message_box> </.message_box>
</div> </div>
@ -296,7 +308,7 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
<div class="flex gap-2 items-center text-gray-700"> <div class="flex gap-2 items-center text-gray-700">
<h3 class="text-sm"> <h3 class="text-sm">
<span class="font-semibold"><%= @deployment_group.name %></span> <span class="font-semibold"><%= @deployment_group.name %></span>
(internal-domain.example.com) <span :if={url = @deployment_group.url}>(<%= url %>)</span>
</h3> </h3>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">

View file

@ -43,7 +43,14 @@ defmodule Livebook.Integration.AppsTest do
Apps.Deployer.deploy_monitor(deployer_pid, app_spec) Apps.Deployer.deploy_monitor(deployer_pid, app_spec)
assert_receive {:app_created, %{pid: pid, slug: ^slug}} assert_receive {:app_created, %{pid: pid, slug: ^slug}}
assert_receive {:app_updated, %{slug: ^slug, sessions: [session]}}
assert_receive {:app_updated,
%{
slug: ^slug,
sessions: [
%{app_status: %{execution: :executed, lifecycle: :active}} = session
]
}}
assert %{secrets: %{^secret_name => ^secret}} = Livebook.Session.get_data(session.pid) assert %{secrets: %{^secret_name => ^secret}} = Livebook.Session.get_data(session.pid)

View file

@ -546,14 +546,18 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
assert render(view) =~ assert render(view) =~
"App deployment created successfully" "App deployment created successfully"
assert render(view) =~ "#{Livebook.Config.teams_url()}/orgs/#{team.org_id}"
end end
test "deployment flow with existing deployment groups in the hub", test "deployment flow with existing deployment groups in the hub",
%{conn: conn, user: user, node: node, session: session} do %{conn: conn, user: user, node: node, session: session} do
Livebook.Teams.Broadcasts.subscribe([:deployment_groups])
team = create_team_hub(user, node) team = create_team_hub(user, node)
Session.set_notebook_hub(session.pid, team.id) Session.set_notebook_hub(session.pid, team.id)
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id) id = insert_deployment_group(mode: :online, hub_id: team.id).id
assert_receive {:deployment_group_created, %{id: ^id} = deployment_group}
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
@ -581,9 +585,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|> element(~s/[phx-click="select_deployment_group"][phx-value-id="#{deployment_group.id}"]/) |> element(~s/[phx-click="select_deployment_group"][phx-value-id="#{deployment_group.id}"]/)
|> render_click() |> render_click()
%{id: id} = deployment_group
assert_receive {:operation, {:set_notebook_deployment_group, _, ^id}} assert_receive {:operation, {:set_notebook_deployment_group, _, ^id}}
assert render(view) =~ "The selected deployment group has no app servers." assert render(view) =~ "The selected deployment group has no app servers."
view view
@ -593,7 +595,6 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
# Step: agent instance setup # Step: agent instance setup
assert render(view) =~ "Step: add app server" assert render(view) =~ "Step: add app server"
assert render(view) =~ "Awaiting an app server to be set up." assert render(view) =~ "Awaiting an app server to be set up."
[deployment_group] = Livebook.Hubs.TeamClient.get_deployment_groups(team.id) [deployment_group] = Livebook.Hubs.TeamClient.get_deployment_groups(team.id)
@ -613,6 +614,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
test "shows an error when the deployment size is higher than the maximum size of 20MB", 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 %{conn: conn, user: user, node: node, session: session} do
Livebook.Teams.Broadcasts.subscribe([:deployment_groups])
team = create_team_hub(user, node) team = create_team_hub(user, node)
Session.set_notebook_hub(session.pid, team.id) Session.set_notebook_hub(session.pid, team.id)
@ -620,8 +622,11 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug} app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
Session.set_app_settings(session.pid, app_settings) Session.set_app_settings(session.pid, app_settings)
deployment_group = insert_deployment_group(mode: :online, hub_id: team.id) id = insert_deployment_group(mode: :online, hub_id: team.id).id
Session.set_notebook_deployment_group(session.pid, deployment_group.id) assert_receive {:deployment_group_created, %{id: ^id}}
Session.set_notebook_deployment_group(session.pid, id)
assert_receive {:operation, {:set_notebook_deployment_group, _, ^id}}
%{files_dir: files_dir} = session %{files_dir: files_dir} = session
image_file = FileSystem.File.resolve(files_dir, "image.jpg") image_file = FileSystem.File.resolve(files_dir, "image.jpg")