- <.link
- :for={app <- apps_listing(@apps)}
- navigate={~p"/apps/#{app.slug}"}
- class="px-4 py-3 border border-gray-200 rounded-xl text-gray-800 pointer hover:bg-gray-50 flex items-center justify-between"
- >
-
{app.notebook_name}
- <.remix_icon :if={not app.public?} icon="lock-password-line" />
-
+
+
+
+
+
Apps
+
Find your applications
-
-
- <.no_entries :if={@apps == []}>
- No apps running.
-
-
-
-
- No app notebooks found. Follow these steps to list your apps here:
-
-
- -
- Open a notebook
-
- -
- Click <.remix_icon icon="rocket-line" class="align-baseline text-lg" />
- in the sidebar and configure the app as public
-
- -
- Save the notebook to the
- {Livebook.Config.apps_path()}
- folder
-
- -
- Relaunch your Livebook app
-
-
+
+ <%= if @apps != [] do %>
+
+
+
+
+ <.remix_icon
+ icon="search-line"
+ class="absolute left-3 bottom-[8px] text-gray-400"
+ />
+ <.text_field
+ id="search-app"
+ name="search_term"
+ placeholder="Search apps..."
+ value={@search_term}
+ phx-keyup="search"
+ phx-debounce="300"
+ class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
+ />
+
+
+
+
+
+
+
+
+ <.remix_icon icon="windy-line" class="text-gray-400 text-2xl" />
+
No apps found
+
Try adjusting your search or filter criteria
+
+
+
+
+ <.remix_icon icon={icon} />
+ {app_folder}
+ ({length(apps)})
+
+
+ <.link
+ :for={app <- apps_listing(apps)}
+ id={"app-#{app.slug}"}
+ navigate={~p"/apps/#{app.slug}"}
+ class="border bg-gray-50 border-gray-300 rounded-lg p-4 hover:shadow-md hover:border-blue-300 transition-all duration-200"
+ >
+
+
+ {app.notebook_name}
+
+
+ <.remix_icon
+ :if={not app.public?}
+ icon="lock-password-line"
+ class="h-4 w-4 text-gray-400"
+ />
+ <.remix_icon
+ icon="arrow-right-line"
+ class="h-4 w-4 text-gray-400 group-hover:text-blue-600 transition-colors"
+ />
+
+
+
+
+
+
+
+ <% else %>
+
+
+
+ <.remix_icon icon="windy-line" class="size-16 text-gray-400 text-2xl" />
+
No apps found
+
Follow these steps to list your apps here:
+
+
+
+ -
+
+ 1
+
+ Open a notebook
+
+ -
+
+ 2
+
+
+ Click
+ <.remix_icon icon="rocket-line" class="inline align-baseline text-base" />
+ in the sidebar and configure the app as public
+
+
+ -
+
+ 3
+
+
+ Save the notebook to the
+
+ {Livebook.Config.apps_path()}
+
+ folder
+
+
+ -
+
+ 4
+
+ Relaunch your Livebook app
+
+
+
+
+
+ <.remix_icon icon="windy-line" class="size-16 text-gray-400 text-2xl" />
+
No apps running
+
Start some apps to see them listed here
+
+
+ <% end %>
@@ -101,15 +211,46 @@ defmodule LivebookWeb.AppsLive do
"""
end
+ @impl true
+ def handle_event("search", %{"value" => search_term}, socket) do
+ {:noreply,
+ socket
+ |> assign(search_term: search_term)
+ |> apply_filters()}
+ end
+
+ def handle_event("select_app_folder", %{"app_folder" => app_folder_id}, socket) do
+ {:noreply,
+ socket
+ |> assign(selected_app_folder: app_folder_id)
+ |> apply_filters()}
+ end
+
@impl true
def handle_info({type, _app} = event, socket)
when type in [:app_created, :app_updated, :app_closed] do
- {:noreply, update(socket, :apps, &LivebookWeb.AppComponents.update_app_list(&1, event))}
+ {:noreply,
+ socket
+ |> assign(apps: LivebookWeb.AppComponents.update_app_list(socket.assigns.apps, event))
+ |> apply_filters()}
end
def handle_info({:server_authorization_updated, _}, socket) do
- apps = Livebook.Apps.list_authorized_apps(socket.assigns.current_user)
- {:noreply, assign(socket, :apps, apps)}
+ {:noreply,
+ socket
+ |> assign(
+ apps: Livebook.Apps.list_authorized_apps(socket.assigns.current_user),
+ logout_enabled?:
+ Livebook.Config.logout_enabled?() and socket.assigns.current_user.email != nil
+ )
+ |> apply_filters()}
+ end
+
+ def handle_info({type, _app_folder}, socket) when type in @events do
+ {:noreply,
+ socket
+ |> load_app_folders()
+ |> apply_filters()}
end
def handle_info(_message, socket), do: {:noreply, socket}
@@ -117,4 +258,74 @@ defmodule LivebookWeb.AppsLive do
defp apps_listing(apps) do
Enum.sort_by(apps, & &1.notebook_name)
end
+
+ def load_app_folders(socket) do
+ app_folders =
+ Enum.flat_map(Livebook.Hubs.get_hubs(), &Livebook.Hubs.Provider.get_app_folders/1)
+
+ app_folder_options =
+ for app_folder <- app_folders do
+ {app_folder.name, app_folder.id}
+ end
+
+ assign(socket, app_folders: app_folders, app_folder_options: app_folder_options)
+ end
+
+ defp apply_filters(socket) do
+ apps = socket.assigns.apps
+ app_folders = socket.assigns.app_folders
+
+ filtered_apps =
+ filter_apps(apps, socket.assigns.search_term, socket.assigns.selected_app_folder)
+
+ grouped_apps =
+ filtered_apps
+ |> Enum.group_by(fn
+ %{app_spec: %{app_folder_id: id}} -> Enum.find_value(app_folders, &(&1.id == id && id))
+ _ -> nil
+ end)
+ |> Enum.map(fn
+ {nil, apps} ->
+ {"Ungrouped apps", "ungrouped-apps", "asterisk", apps}
+
+ {id, apps} ->
+ app_folder_name = Enum.find_value(app_folders, &(&1.id == id && &1.name))
+ {app_folder_name, "app-folder-#{id}", "folder-line", apps}
+ end)
+ |> Enum.sort_by(&elem(&1, 0))
+
+ show_app_folders? = Enum.any?(apps, &is_struct(&1.app_spec, Livebook.Apps.TeamsAppSpec))
+
+ assign(socket,
+ grouped_apps: grouped_apps,
+ filtered_apps: filtered_apps,
+ show_app_folders?: show_app_folders?
+ )
+ end
+
+ defp filter_apps(apps, term, app_folder_id) do
+ apps
+ |> search_apps(term)
+ |> filter_by_app_folder(app_folder_id)
+ end
+
+ defp search_apps(apps, ""), do: apps
+
+ defp search_apps(apps, term) do
+ term = String.downcase(term)
+
+ Enum.filter(apps, fn app ->
+ String.contains?(String.downcase(app.notebook_name), term) or
+ String.contains?(app.slug, term)
+ end)
+ end
+
+ defp filter_by_app_folder(apps, ""), do: apps
+
+ defp filter_by_app_folder(apps, app_folder_id) do
+ Enum.filter(apps, fn
+ %{app_spec: %{app_folder_id: id}} -> id == app_folder_id
+ _otherwise -> false
+ end)
+ end
end
diff --git a/lib/livebook_web/live/session_live/app_settings_component.ex b/lib/livebook_web/live/session_live/app_settings_component.ex
index 037e567f4..060f4331a 100644
--- a/lib/livebook_web/live/session_live/app_settings_component.ex
+++ b/lib/livebook_web/live/session_live/app_settings_component.ex
@@ -20,7 +20,10 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do
{:ok,
socket
|> assign(assigns)
- |> assign(app_folder_options: app_folder_options, changeset: changeset)}
+ |> assign(
+ app_folder_options: app_folder_options,
+ changeset: changeset
+ )}
end
@impl true
@@ -48,6 +51,7 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do
<.text_field field={f[:slug]} label="Slug" spellcheck="false" phx-debounce />
<.select_field
+ :if={@hub_id != Livebook.Hubs.Personal.id()}
field={f[:app_folder_id]}
label="Folder"
prompt="Select a folder..."
diff --git a/lib/livebook_web/live/session_live/app_teams_live.ex b/lib/livebook_web/live/session_live/app_teams_live.ex
index 816167c1e..d5709db48 100644
--- a/lib/livebook_web/live/session_live/app_teams_live.ex
+++ b/lib/livebook_web/live/session_live/app_teams_live.ex
@@ -602,11 +602,9 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
end)
end
- defp app_folder_name(_hub, id) when id in [nil, ""], do: "Ungrouped apps"
-
defp app_folder_name(hub, id) do
hub
|> Teams.get_app_folders()
- |> Enum.find_value(&(&1.id == id && &1.name))
+ |> Enum.find_value("Ungrouped apps", &(&1.id == id && &1.name))
end
end
diff --git a/lib/livebook_web/live/session_live/render.ex b/lib/livebook_web/live/session_live/render.ex
index b11f11f14..b847a829e 100644
--- a/lib/livebook_web/live/session_live/render.ex
+++ b/lib/livebook_web/live/session_live/render.ex
@@ -104,6 +104,7 @@ defmodule LivebookWeb.SessionLive.Render do
context={@action_assigns.context}
deployed_app_slug={@data_view.deployed_app_slug}
app_folders={@data_view.hub_app_folders}
+ hub_id={@data_view.hub.id}
/>
diff --git a/test/livebook_teams/live_markdown/import_test.exs b/test/livebook_teams/live_markdown/import_test.exs
index d14c443d3..873bcfba1 100644
--- a/test/livebook_teams/live_markdown/import_test.exs
+++ b/test/livebook_teams/live_markdown/import_test.exs
@@ -11,7 +11,7 @@ defmodule Livebook.Integration.LiveMarkdown.ImportTest do
@moduletag subscribe_to_teams_topics: [:clients, :app_folders]
describe "app settings" do
- test "don't import app folder if does not exists anymore",
+ test "keep the app folder id even if it does not exist anymore",
%{node: node, team: team, org: org} do
app_folder = TeamsRPC.create_app_folder(node, name: "delete me", org: org)
@@ -39,8 +39,10 @@ defmodule Livebook.Integration.LiveMarkdown.ImportTest do
TeamsRPC.delete_app_folder(node, app_folder)
assert_receive {:app_folder_deleted, %{id: ^app_folder_id, hub_id: ^hub_id}}
- assert {%Notebook{name: "Deleted from folder", app_settings: %{app_folder_id: nil}},
- %{warnings: warnings}} = LiveMarkdown.Import.notebook_from_livemd(markdown)
+ assert {%Notebook{
+ name: "Deleted from folder",
+ app_settings: %{app_folder_id: ^app_folder_id}
+ }, %{warnings: warnings}} = LiveMarkdown.Import.notebook_from_livemd(markdown)
assert "notebook is assigned to a non-existent app folder, defaulting to ungrouped app folder" in warnings
end
diff --git a/test/livebook_teams/web/admin_live_test.exs b/test/livebook_teams/web/admin_live_test.exs
index 1aecdcfed..26f41978d 100644
--- a/test/livebook_teams/web/admin_live_test.exs
+++ b/test/livebook_teams/web/admin_live_test.exs
@@ -16,12 +16,13 @@ defmodule LivebookWeb.Integration.AdminLiveTest do
%{conn: conn, node: node, code: code} = context do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["dev-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
)
@@ -76,6 +77,7 @@ defmodule LivebookWeb.Integration.AdminLiveTest do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
@@ -99,10 +101,9 @@ defmodule LivebookWeb.Integration.AdminLiveTest do
{:ok, view, _html} = live(conn, ~p"/settings")
assert render(view) =~ "System settings"
- TeamsRPC.update_authorization_group(node, authorization_group, %{
- access_type: :apps,
- prefixes: ["ops-"]
- })
+ TeamsRPC.update_authorization_group(node, authorization_group, %{access_type: :apps}, [
+ app_folder
+ ])
id = to_string(deployment_group.id)
assert_receive {:server_authorization_updated, %{id: ^id}}
@@ -121,12 +122,13 @@ defmodule LivebookWeb.Integration.AdminLiveTest do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["ops-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: deployment_group
)
diff --git a/test/livebook_teams/web/app_session_live_test.exs b/test/livebook_teams/web/app_session_live_test.exs
index edd97ce57..a3a2765bb 100644
--- a/test/livebook_teams/web/app_session_live_test.exs
+++ b/test/livebook_teams/web/app_session_live_test.exs
@@ -8,7 +8,13 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
setup :teams
@moduletag subscribe_to_hubs_topics: [:connection]
- @moduletag subscribe_to_teams_topics: [:clients, :agents, :app_deployments, :app_server]
+ @moduletag subscribe_to_teams_topics: [
+ :clients,
+ :agents,
+ :app_deployments,
+ :app_server,
+ :app_folders
+ ]
setup do
Livebook.Apps.subscribe()
@@ -23,12 +29,13 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["dev-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
)
@@ -46,7 +53,16 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
slug = "dev-oban-app"
context = change_to_user_session(context)
- deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder
+ )
change_to_agent_session(context)
pid = wait_livebook_app_start(slug)
@@ -111,12 +127,13 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["mkt-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: deployment_group
)
@@ -134,7 +151,16 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
slug = "mkt-analytics-#{Livebook.Utils.random_short_id()}"
context = change_to_user_session(context)
- deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder
+ )
change_to_agent_session(context)
pid = wait_livebook_app_start(slug)
@@ -144,8 +170,11 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
{:ok, view, _html} = live(conn, path)
assert render(view) =~ "LivebookApp:#{slug}"
- {:ok, %{prefixes: ["ops-"]}} =
- TeamsRPC.update_authorization_group(node, authorization_group, %{prefixes: ["ops-"]})
+ app_folder2 = TeamsRPC.create_app_folder(node, org: context.org)
+ app_folder_id = app_folder2.id
+
+ {:ok, %{app_folders: [%{id: ^app_folder_id}]}} =
+ TeamsRPC.update_authorization_group(node, authorization_group, %{}, [app_folder2])
id = to_string(deployment_group.id)
@@ -164,12 +193,14 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
+ app_folder2 = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["mkt-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: deployment_group
)
@@ -187,7 +218,16 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
slug = "analytics-app-#{Livebook.Utils.random_short_id()}"
context = change_to_user_session(context)
- deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder2
+ )
change_to_agent_session(context)
pid = wait_livebook_app_start(slug)
@@ -207,5 +247,70 @@ defmodule LivebookWeb.Integration.AppSessionLiveTest do
{:ok, view, _html} = live(conn, path)
assert render(view) =~ "LivebookApp:#{slug}"
end
+
+ @tag :tmp_dir
+ test "renders unauthorized if app's folder is deleted in real-time",
+ %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
+ {:ok, deployment_group} =
+ TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
+
+ oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
+
+ authorization_group =
+ TeamsRPC.create_authorization_group(node,
+ group_name: "marketing",
+ access_type: :apps,
+ app_folders: [app_folder],
+ oidc_provider: oidc_provider,
+ deployment_group: deployment_group
+ )
+
+ TeamsRPC.update_user_info_groups(
+ node,
+ code,
+ [
+ %{
+ "provider_id" => to_string(oidc_provider.id),
+ "group_name" => authorization_group.group_name
+ }
+ ]
+ )
+
+ slug = "mkt-analytics-#{Livebook.Utils.random_short_id()}"
+ context = change_to_user_session(context)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder
+ )
+
+ change_to_agent_session(context)
+ pid = wait_livebook_app_start(slug)
+ session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new())
+ path = ~p"/apps/#{slug}/sessions/#{session_id}"
+
+ {:ok, view, _html} = live(conn, path)
+ assert render(view) =~ "LivebookApp:#{slug}"
+
+ app_folder_id = to_string(app_folder.id)
+
+ TeamsRPC.delete_app_folder(node, app_folder)
+ assert_receive {:app_folder_deleted, %{id: ^app_folder_id}}
+
+ id = to_string(deployment_group.id)
+
+ assert_receive {:server_authorization_updated, %{id: ^id}}
+ assert_receive {:app_deployment_updated, %{slug: ^slug, app_folder_id: nil}}
+ assert_redirect view, path
+
+ {:ok, view, _html} = live(conn, path)
+ assert render(view) =~ "Not authorized"
+ end
end
end
diff --git a/test/livebook_teams/web/apps_live_test.exs b/test/livebook_teams/web/apps_live_test.exs
index eaffce17e..b9b98b26b 100644
--- a/test/livebook_teams/web/apps_live_test.exs
+++ b/test/livebook_teams/web/apps_live_test.exs
@@ -13,9 +13,12 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
:agents,
:deployment_groups,
:app_deployments,
- :app_server
+ :app_server,
+ :app_folders
]
+ @moduletag :tmp_dir
+
setup do
Livebook.Apps.subscribe()
:ok
@@ -24,17 +27,17 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
describe "authorized apps" do
setup :livebook_teams_auth
- @tag :tmp_dir
test "shows one app if user doesn't have full access",
%{conn: conn, code: code, node: node, tmp_dir: tmp_dir} = context do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["dev-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
)
@@ -52,7 +55,16 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
slug = "dev-app-#{Livebook.Utils.random_short_id()}"
context = change_to_user_session(context)
- deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder
+ )
change_to_agent_session(context)
wait_livebook_app_start(slug)
@@ -66,7 +78,6 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
assert html =~ slug
end
- @tag :tmp_dir
test "shows all apps if user have full access",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
@@ -121,7 +132,6 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
end
end
- @tag :tmp_dir
test "updates the apps list in real-time",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
{:ok, %{groups_auth: true} = deployment_group} =
@@ -131,12 +141,13 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
assert_receive {:deployment_group_updated, %{id: ^id, groups_auth: true}}
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["mkt-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: deployment_group
)
@@ -154,7 +165,15 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
slug = "marketing-report-#{Livebook.Utils.random_short_id()}"
context = change_to_user_session(context)
- deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node
+ )
change_to_agent_session(context)
wait_livebook_app_start(slug)
@@ -169,7 +188,6 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
assert render(view) =~ slug
end
- @tag :tmp_dir
test "shows all apps if disable the authentication in real-time",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
{:ok, %{groups_auth: true} = deployment_group} =
@@ -179,12 +197,14 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
assert_receive {:deployment_group_updated, %{id: ^id, groups_auth: true}}
oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
+ app_folder2 = TeamsRPC.create_app_folder(node, org: context.org)
authorization_group =
TeamsRPC.create_authorization_group(node,
group_name: "marketing",
access_type: :apps,
- prefixes: ["mkt-"],
+ app_folders: [app_folder],
oidc_provider: oidc_provider,
deployment_group: deployment_group
)
@@ -202,7 +222,16 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
slug = "marketing-app-#{Livebook.Utils.random_short_id()}"
context = change_to_user_session(context)
- deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder2
+ )
change_to_agent_session(context)
wait_livebook_app_start(slug)
@@ -216,5 +245,273 @@ defmodule LivebookWeb.Integration.AppsLiveTest do
{:ok, view, _} = live(conn, ~p"/apps")
assert render(view) =~ slug
end
+
+ test "updates the folder name in real-time",
+ %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
+ {:ok, %{groups_auth: true} = deployment_group} =
+ TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
+
+ id = to_string(deployment_group.id)
+ assert_receive {:deployment_group_updated, %{id: ^id, groups_auth: true}}
+
+ oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
+
+ authorization_group =
+ TeamsRPC.create_authorization_group(node,
+ group_name: "marketing",
+ access_type: :apps,
+ app_folders: [app_folder],
+ oidc_provider: oidc_provider,
+ deployment_group: deployment_group
+ )
+
+ TeamsRPC.update_user_info_groups(
+ node,
+ code,
+ [
+ %{
+ "provider_id" => to_string(oidc_provider.id),
+ "group_name" => authorization_group.group_name
+ }
+ ]
+ )
+
+ slug = Livebook.Utils.random_short_id()
+ context = change_to_user_session(context)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder
+ )
+
+ change_to_agent_session(context)
+ wait_livebook_app_start(slug)
+
+ {:ok, view, _} = live(conn, ~p"/apps")
+ assert render(view) =~ app_folder.name
+ assert render(view) =~ slug
+
+ new_name = "NewAppFolderName"
+ app_folder_id = to_string(app_folder.id)
+
+ {:ok, _app_folder} = TeamsRPC.update_app_folder(node, app_folder, name: new_name)
+ assert_receive {:app_folder_updated, %{id: ^app_folder_id, name: ^new_name}}
+
+ refute render(view) =~ app_folder.name
+ assert render(view) =~ new_name
+ assert render(view) =~ slug
+ end
+
+ test "deletes the folder and move the app to ungrouped apps folder in real-time",
+ %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
+ {:ok, %{groups_auth: true} = deployment_group} =
+ TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
+
+ id = to_string(deployment_group.id)
+ assert_receive {:deployment_group_updated, %{id: ^id, groups_auth: true}}
+
+ oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
+
+ authorization_group =
+ TeamsRPC.create_authorization_group(node,
+ group_name: "marketing",
+ access_type: :apps,
+ app_folders: [app_folder],
+ oidc_provider: oidc_provider,
+ deployment_group: deployment_group
+ )
+
+ TeamsRPC.update_user_info_groups(
+ node,
+ code,
+ [
+ %{
+ "provider_id" => to_string(oidc_provider.id),
+ "group_name" => authorization_group.group_name
+ }
+ ]
+ )
+
+ slug = Livebook.Utils.random_short_id()
+ context = change_to_user_session(context)
+
+ deploy_app(
+ slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_folder
+ )
+
+ change_to_agent_session(context)
+ wait_livebook_app_start(slug)
+
+ {:ok, view, _} = live(conn, ~p"/apps")
+ assert render(view) =~ app_folder.name
+ assert render(view) =~ slug
+
+ id = to_string(deployment_group.id)
+ app_folder_id = to_string(app_folder.id)
+
+ TeamsRPC.delete_app_folder(node, app_folder)
+ assert_receive {:app_folder_deleted, %{id: ^app_folder_id}}
+ assert_receive {:app_deployment_updated, %{slug: ^slug, app_folder_id: nil}}
+ assert_receive {:app_updated, %{slug: ^slug, app_spec: %{app_folder_id: ^app_folder_id}}}
+
+ # Once the folder is deleted, all apps are moved to a "Ungrouped apps" folder,
+ # which only users with full access will be able to see and access them.
+ refute render(view) =~ app_folder.name
+ refute render(view) =~ slug
+
+ # To validate this behaivour, updates the authorization group to be full access
+ {:ok, %{access_type: :app_server}} =
+ TeamsRPC.update_authorization_group(node, authorization_group, %{access_type: :app_server})
+
+ # Since we're updating the authorization group access type, the app deployment must receive the updated version
+ assert_receive {:server_authorization_updated, %{id: ^id}}, 3_000
+ assert_receive {:app_deployment_updated, %{slug: ^slug, app_folder_id: nil}}
+
+ refute render(view) =~ app_folder.name
+ assert render(view) =~ "Ungrouped apps"
+ assert render(view) =~ slug
+ end
+
+ test "filter the apps based on slug, name and app folder",
+ %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
+ {:ok, %{groups_auth: true} = deployment_group} =
+ TeamsRPC.toggle_groups_authorization(node, context.deployment_group)
+
+ id = to_string(deployment_group.id)
+ assert_receive {:deployment_group_updated, %{id: ^id, groups_auth: true}}
+
+ oidc_provider = TeamsRPC.create_oidc_provider(node, context.org)
+ app_folder = TeamsRPC.create_app_folder(node, org: context.org)
+ app_folder2 = TeamsRPC.create_app_folder(node, org: context.org)
+
+ authorization_group =
+ TeamsRPC.create_authorization_group(node,
+ access_type: :app_server,
+ oidc_provider: oidc_provider,
+ deployment_group: deployment_group
+ )
+
+ TeamsRPC.update_user_info_groups(
+ node,
+ code,
+ [
+ %{
+ "provider_id" => to_string(oidc_provider.id),
+ "group_name" => authorization_group.group_name
+ }
+ ]
+ )
+
+ apps_to_deploy = [
+ app_to_deploy1 = %{
+ slug: "app-from-folder1",
+ title: "Super Admin Tools",
+ app_folder: app_folder,
+ folder_id: "app-folder-#{app_folder.id}",
+ folder_name: app_folder.name
+ },
+ app_to_deploy2 = %{
+ slug: "app-from-folder2",
+ title: "Accounting daily report",
+ app_folder: app_folder2,
+ folder_id: "app-folder-#{app_folder2.id}",
+ folder_name: app_folder2.name
+ },
+ app_to_deploy3 = %{
+ slug: "app-from-ungrouped-folder",
+ title: "List of the chonkiest cats",
+ app_folder: nil,
+ folder_id: "ungrouped-apps",
+ folder_name: "Ungrouped apps"
+ }
+ ]
+
+ context = change_to_user_session(context)
+
+ for app_to_deploy <- apps_to_deploy do
+ deploy_app(
+ app_to_deploy.slug,
+ context.team,
+ context.org,
+ context.deployment_group,
+ tmp_dir,
+ node,
+ app_to_deploy.app_folder,
+ app_to_deploy.title
+ )
+ end
+
+ change_to_agent_session(context)
+
+ for %{slug: slug} <- apps_to_deploy do
+ wait_livebook_app_start(slug)
+ end
+
+ {:ok, view, _} = live(conn, ~p"/apps")
+ Enum.each(apps_to_deploy, &assert_app(view, &1))
+
+ # filter by slug
+ render_keyup(view, "search", %{value: app_to_deploy1.slug})
+ assert_app(view, app_to_deploy1)
+
+ apps_to_deploy
+ |> Enum.reject(&(&1 == app_to_deploy1))
+ |> Enum.each(&refute_app(view, &1))
+
+ # filter by title
+ render_keyup(view, "search", %{value: app_to_deploy3.title})
+ assert_app(view, app_to_deploy3)
+
+ apps_to_deploy
+ |> Enum.reject(&(&1 == app_to_deploy3))
+ |> Enum.each(&refute_app(view, &1))
+
+ # reset filter
+ render_keyup(view, "search", %{value: ""})
+
+ # filter by app folder
+ view
+ |> element("#select-app-folder-form")
+ |> render_change(%{app_folder: app_to_deploy2.app_folder.id})
+
+ assert_app(view, app_to_deploy2)
+
+ apps_to_deploy
+ |> Enum.reject(&(&1 == app_to_deploy2))
+ |> Enum.each(&refute_app(view, &1))
+ end
+ end
+
+ defp assert_app(view, app_to_deploy) do
+ assert view
+ |> element("##{app_to_deploy.folder_id}", app_to_deploy.folder_name)
+ |> has_element?()
+
+ assert view
+ |> element("#app-#{app_to_deploy.slug}", app_to_deploy.title)
+ |> has_element?()
+ end
+
+ defp refute_app(view, app_to_deploy) do
+ refute view
+ |> element("##{app_to_deploy.folder_id}", app_to_deploy.folder_name)
+ |> has_element?()
+
+ refute view
+ |> element("#app-#{app_to_deploy.slug}", app_to_deploy.title)
+ |> has_element?()
end
end
diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs
index d97530aef..ee4d1c4a7 100644
--- a/test/livebook_web/live/session_live_test.exs
+++ b/test/livebook_web/live/session_live_test.exs
@@ -2786,6 +2786,9 @@ defmodule LivebookWeb.SessionLiveTest do
|> element(~s/[data-el-app-info] a/, "Configure")
|> render_click()
+ # doesn't show the app folder select
+ refute has_element?(view, ~s/#app-settings-modal input[name="app_folder_id"]/)
+
view
|> element(~s/#app-settings-modal form/)
|> render_change(%{"app_settings" => %{"slug" => slug}})
diff --git a/test/support/app_helpers.ex b/test/support/app_helpers.ex
index 33946d470..aa812b8a5 100644
--- a/test/support/app_helpers.ex
+++ b/test/support/app_helpers.ex
@@ -26,14 +26,25 @@ defmodule Livebook.AppHelpers do
end
end
- def deploy_app(slug, team, org, deployment_group, tmp_dir, node) do
+ def deploy_app(
+ slug,
+ team,
+ org,
+ deployment_group,
+ tmp_dir,
+ node,
+ app_folder \\ nil,
+ title \\ nil
+ ) do
app_path = Path.join(tmp_dir, "#{slug}.livemd")
+ app_folder = if app_folder, do: ~s(,"app_folder_id":"#{app_folder.id}")
+ title = if title, do: title, else: "LivebookApp:#{slug}"
source =
stamp_notebook(app_path, """
-
+
- # LivebookApp:#{slug}
+ # #{title}
```elixir
IO.puts("Hi")
diff --git a/test/support/integration/teams_rpc.ex b/test/support/integration/teams_rpc.ex
index 4e292e713..a3e1c36f7 100644
--- a/test/support/integration/teams_rpc.ex
+++ b/test/support/integration/teams_rpc.ex
@@ -169,8 +169,12 @@ defmodule Livebook.TeamsRPC do
# Update resource
- def update_authorization_group(node, authorization_group, attrs) do
- :erpc.call(node, TeamsRPC, :update_authorization_group, [authorization_group, attrs])
+ def update_authorization_group(node, authorization_group, attrs, app_folders \\ []) do
+ :erpc.call(node, TeamsRPC, :update_authorization_group, [
+ authorization_group,
+ attrs,
+ app_folders
+ ])
end
def update_user_info_groups(node, code, groups) do