mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-18 22:21:32 +08:00
Introducing the new Apps page (#3091)
This commit is contained in:
parent
c7762469f2
commit
e7098f51a9
15 changed files with 757 additions and 105 deletions
|
|
@ -3,7 +3,7 @@ defmodule Livebook.Apps.TeamsAppSpec do
|
|||
|
||||
@enforce_keys [:slug, :version, :hub_id, :app_deployment_id]
|
||||
|
||||
defstruct [:slug, :version, :hub_id, :app_deployment_id]
|
||||
defstruct [:slug, :version, :hub_id, :app_deployment_id, :app_folder_id]
|
||||
end
|
||||
|
||||
defimpl Livebook.Apps.AppSpec, for: Livebook.Apps.TeamsAppSpec do
|
||||
|
|
|
|||
|
|
@ -254,7 +254,8 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
|
|||
slug: app_deployment.slug,
|
||||
version: app_deployment.version,
|
||||
hub_id: app_deployment.hub_id,
|
||||
app_deployment_id: app_deployment.id
|
||||
app_deployment_id: app_deployment.id,
|
||||
app_folder_id: app_deployment.app_folder_id
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -179,6 +179,8 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
@spec get_app_folders(String.t()) :: list(Teams.AppFolder.t())
|
||||
def get_app_folders(id) do
|
||||
GenServer.call(registry_name(id), :get_app_folders)
|
||||
catch
|
||||
:exit, _ -> []
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -865,6 +867,19 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
defp handle_event(:app_deployment_updated, %Teams.AppDeployment{} = app_deployment, state) do
|
||||
manager_sync(app_deployment, state)
|
||||
Teams.Broadcasts.app_deployment_updated(app_deployment)
|
||||
|
||||
with {:ok, current_app_deployment} <- fetch_app_deployment(app_deployment.id, state) do
|
||||
if state.deployment_group_id &&
|
||||
(current_app_deployment.app_folder_id !=
|
||||
app_deployment.app_folder_id or
|
||||
current_app_deployment.authorization_groups != app_deployment.authorization_groups) do
|
||||
{:ok, deployment_group} =
|
||||
fetch_deployment_group(app_deployment.deployment_group_id, state)
|
||||
|
||||
Teams.Broadcasts.server_authorization_updated(deployment_group)
|
||||
end
|
||||
end
|
||||
|
||||
put_app_deployment(state, app_deployment)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -667,25 +667,23 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
# validate it against the public key).
|
||||
teams_enabled = is_struct(hub, Livebook.Hubs.Team) and (hub.offline == nil or stamp_verified?)
|
||||
|
||||
{app_settings, messages} =
|
||||
messages =
|
||||
if app_folder_id = notebook.app_settings.app_folder_id do
|
||||
app_folders = Hubs.Provider.get_app_folders(hub)
|
||||
|
||||
if Enum.any?(app_folders, &(&1.id == app_folder_id)) do
|
||||
{notebook.app_settings, messages}
|
||||
messages
|
||||
else
|
||||
{Map.replace!(notebook.app_settings, :app_folder_id, nil),
|
||||
messages ++
|
||||
[
|
||||
"notebook is assigned to a non-existent app folder, defaulting to ungrouped app folder"
|
||||
]}
|
||||
messages ++
|
||||
[
|
||||
"notebook is assigned to a non-existent app folder, defaulting to ungrouped app folder"
|
||||
]
|
||||
end
|
||||
else
|
||||
{notebook.app_settings, messages}
|
||||
messages
|
||||
end
|
||||
|
||||
{%{notebook | app_settings: app_settings, teams_enabled: teams_enabled}, stamp_verified?,
|
||||
messages}
|
||||
{%{notebook | teams_enabled: teams_enabled}, stamp_verified?, messages}
|
||||
end
|
||||
|
||||
defp safe_binary_split(binary, offset)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,40 @@
|
|||
defmodule LivebookWeb.AppsLive do
|
||||
use LivebookWeb, :live_view
|
||||
|
||||
@events [
|
||||
:app_folder_created,
|
||||
:app_folder_updated,
|
||||
:app_folder_deleted
|
||||
]
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
Livebook.Teams.Broadcasts.subscribe(:app_server)
|
||||
Livebook.Teams.Broadcasts.subscribe([:app_server, :app_folders])
|
||||
Livebook.Apps.subscribe()
|
||||
end
|
||||
|
||||
apps = Livebook.Apps.list_authorized_apps(socket.assigns.current_user)
|
||||
empty_apps_path? = Livebook.Apps.empty_apps_path?()
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
apps: apps,
|
||||
socket
|
||||
|> assign(
|
||||
search_term: "",
|
||||
selected_app_folder: "",
|
||||
apps: Livebook.Apps.list_authorized_apps(socket.assigns.current_user),
|
||||
empty_apps_path?: empty_apps_path?,
|
||||
logout_enabled?:
|
||||
Livebook.Config.logout_enabled?() and socket.assigns.current_user.email != nil
|
||||
)}
|
||||
)
|
||||
|> load_app_folders()
|
||||
|> apply_filters()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="h-full flex flex-col overflow-y-auto">
|
||||
<div class="px-4 py-3 flex items-center justify-between">
|
||||
<div class="px-6 py-4 bg-white border-b border-gray-200 flex items-center justify-between">
|
||||
<div class="w-10 h-10">
|
||||
<.menu id="apps-menu" position="bottom-right" md_position="bottom-left">
|
||||
<:toggle>
|
||||
|
|
@ -42,58 +52,158 @@ defmodule LivebookWeb.AppsLive do
|
|||
</.menu>
|
||||
</div>
|
||||
<div>
|
||||
<.link navigate={~p"/apps-dashboard"} class="flex items-center text-blue-600">
|
||||
<.link
|
||||
navigate={~p"/apps-dashboard"}
|
||||
class="flex items-center text-blue-600 hover:text-blue-700 transition-colors"
|
||||
>
|
||||
<span class="font-semibold">Dashboard</span>
|
||||
<.remix_icon icon="arrow-right-line" class="align-middle ml-1" />
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full max-w-screen-lg px-4 md:px-20 py-4 mx-auto">
|
||||
<div class="flex flex-col items-center">
|
||||
<h1 class="text-2xl text-gray-800 font-medium">
|
||||
Apps
|
||||
</h1>
|
||||
<div :if={@apps != []} class="w-full mt-5 max-w-[400px]">
|
||||
<div class="w-full flex flex-col space-y-4">
|
||||
<.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"
|
||||
>
|
||||
<span class="font-semibold">{app.notebook_name}</span>
|
||||
<.remix_icon :if={not app.public?} icon="lock-password-line" />
|
||||
</.link>
|
||||
<div class="flex-1 px-6 py-6">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex flex-col gap-y-4 w-full">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Apps</h1>
|
||||
<p class="text-gray-600">Find your applications</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:if={@apps == [] and not @empty_apps_path?}
|
||||
class="mt-5 flex flex-col w-full max-w-[400px]"
|
||||
>
|
||||
<.no_entries :if={@apps == []}>
|
||||
No apps running.
|
||||
</.no_entries>
|
||||
</div>
|
||||
<div :if={@apps == [] and @empty_apps_path?} class="mt-5 text-gray-600">
|
||||
<div>
|
||||
No app notebooks found. Follow these steps to list your apps here:
|
||||
</div>
|
||||
<ol class="mt-4 pl-4 flex flex-col space-y-1 list-decimal list-inside">
|
||||
<li>
|
||||
Open a notebook
|
||||
</li>
|
||||
<li>
|
||||
Click <.remix_icon icon="rocket-line" class="align-baseline text-lg" />
|
||||
in the sidebar and configure the app as public
|
||||
</li>
|
||||
<li>
|
||||
Save the notebook to the
|
||||
<span class="font-medium">{Livebook.Config.apps_path()}</span>
|
||||
folder
|
||||
</li>
|
||||
<li>
|
||||
Relaunch your Livebook app
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<%= if @apps != [] do %>
|
||||
<div class="flex flex-col gap-y-8">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="relative">
|
||||
<.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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div :if={@show_app_folders?} class="md:w-48">
|
||||
<form id="select-app-folder-form" phx-change="select_app_folder" phx-nosubmit>
|
||||
<.select_field
|
||||
id="select-app-folder"
|
||||
name="app_folder"
|
||||
prompt="Select a folder..."
|
||||
value={@selected_app_folder}
|
||||
options={@app_folder_options}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@filtered_apps == []}
|
||||
class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center"
|
||||
>
|
||||
<.remix_icon icon="windy-line" class="text-gray-400 text-2xl" />
|
||||
<h3 class="text-lg font-medium text-gray-900">No apps found</h3>
|
||||
<p class="text-gray-600">Try adjusting your search or filter criteria</p>
|
||||
</div>
|
||||
<div class="flex flex-col h-full gap-y-8 pr-2">
|
||||
<div
|
||||
:for={{app_folder, id, icon, apps} <- @grouped_apps}
|
||||
:if={@filtered_apps != []}
|
||||
id={id}
|
||||
class="flex flex-col gap-y-4"
|
||||
>
|
||||
<h2 class="flex items-center gap-x-3 text-xl font-semibold text-gray-900">
|
||||
<.remix_icon icon={icon} />
|
||||
{app_folder}
|
||||
<span class="text-sm font-normal text-gray-500">({length(apps)})</span>
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<.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"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-900 group-hover:text-blue-600 transition-colors truncate">
|
||||
{app.notebook_name}
|
||||
</h3>
|
||||
<div class="space-x-1 ml-2">
|
||||
<.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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex flex-col bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
|
||||
<div :if={@empty_apps_path?} class="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<.remix_icon icon="windy-line" class="size-16 text-gray-400 text-2xl" />
|
||||
<h3 class="text-lg font-medium text-gray-900">No apps found</h3>
|
||||
<p class="text-gray-600">Follow these steps to list your apps here:</p>
|
||||
</div>
|
||||
<div class="p-6 text-left max-w-md mx-auto">
|
||||
<ol class="space-y-3 text-sm text-gray-700">
|
||||
<li class="flex items-center">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 bg-blue-100 text-blue-600 rounded-full text-xs font-medium mr-3 mt-0.5">
|
||||
1
|
||||
</span>
|
||||
Open a notebook
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 bg-blue-100 text-blue-600 rounded-full text-xs font-medium mr-3 mt-0.5">
|
||||
2
|
||||
</span>
|
||||
<div class="flex gap-x-1 items-center">
|
||||
Click
|
||||
<.remix_icon icon="rocket-line" class="inline align-baseline text-base" />
|
||||
in the sidebar and configure the app as public
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 bg-blue-100 text-blue-600 rounded-full text-xs font-medium mr-3 mt-0.5">
|
||||
3
|
||||
</span>
|
||||
<div class="flex gap-x-1 items-center">
|
||||
Save the notebook to the
|
||||
<span class="font-medium bg-gray-100 px-1 rounded text-xs">
|
||||
{Livebook.Config.apps_path()}
|
||||
</span>
|
||||
folder
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 bg-blue-100 text-blue-600 rounded-full text-xs font-medium mr-3 mt-0.5">
|
||||
4
|
||||
</span>
|
||||
Relaunch your Livebook app
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div :if={not @empty_apps_path?}>
|
||||
<.remix_icon icon="windy-line" class="size-16 text-gray-400 text-2xl" />
|
||||
<h3 class="text-lg font-medium text-gray-900">No apps running</h3>
|
||||
<p class="text-gray-600">Start some apps to see them listed here</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<div class="flex flex-col space-y-4">
|
||||
<.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..."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}})
|
||||
|
|
|
|||
|
|
@ -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, """
|
||||
<!-- livebook:{"app_settings":{"access_type":"public","slug":"#{slug}"},"hub_id":"#{team.id}","deployment_group_id":"#{deployment_group.id}"} -->
|
||||
<!-- livebook:{"app_settings":{"access_type":"public"#{app_folder},"slug":"#{slug}"},"hub_id":"#{team.id}","deployment_group_id":"#{deployment_group.id}"} -->
|
||||
|
||||
# LivebookApp:#{slug}
|
||||
# #{title}
|
||||
|
||||
```elixir
|
||||
IO.puts("Hi")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue