mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-09 13:16:08 +08:00
Allow users to assign folders to their apps (#3088)
This commit is contained in:
parent
6285a5f395
commit
82b2b285d8
29 changed files with 533 additions and 26 deletions
|
|
@ -281,4 +281,6 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
|
|||
def deployment_groups(_personal), do: nil
|
||||
|
||||
def get_app_specs(_personal), do: []
|
||||
|
||||
def get_app_folders(_personal), do: []
|
||||
end
|
||||
|
|
|
|||
|
|
@ -155,4 +155,10 @@ defprotocol Livebook.Hubs.Provider do
|
|||
"""
|
||||
@spec get_app_specs(t()) :: list(Livebook.Apps.AppSpec.t())
|
||||
def get_app_specs(hub)
|
||||
|
||||
@doc """
|
||||
Gets the app folders from the given hub.
|
||||
"""
|
||||
@spec get_app_folders(t()) :: list(%{id: String.t(), name: String.t()})
|
||||
def get_app_folders(hub)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -259,6 +259,12 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
|
|||
end
|
||||
end
|
||||
|
||||
def get_app_folders(team) do
|
||||
team.id
|
||||
|> TeamClient.get_app_folders()
|
||||
|> Enum.sort_by(& &1.name)
|
||||
end
|
||||
|
||||
defp parse_secret_errors(errors_map) do
|
||||
Teams.Requests.to_error_list(Secret, errors_map)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
deployment_groups: [],
|
||||
app_deployments: [],
|
||||
agents: [],
|
||||
app_folders: [],
|
||||
app_deployment_statuses: nil
|
||||
]
|
||||
|
||||
|
|
@ -172,6 +173,14 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
GenServer.call(registry_name(id), {:user_can_deploy?, user_id, deployment_group_id})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of cached app folders.
|
||||
"""
|
||||
@spec get_app_folders(String.t()) :: list(Teams.AppFolder.t())
|
||||
def get_app_folders(id) do
|
||||
GenServer.call(registry_name(id), :get_app_folders)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns if the Team client is connected.
|
||||
"""
|
||||
|
|
@ -370,6 +379,10 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_call(:get_app_folders, _caller, state) do
|
||||
{:reply, state.app_folders, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:connected, state) do
|
||||
Hubs.Broadcasts.hub_connected(state.hub.id)
|
||||
|
|
@ -611,7 +624,8 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
file: nil,
|
||||
deployed_by: app_deployment.deployed_by,
|
||||
deployed_at: DateTime.from_gregorian_seconds(app_deployment.deployed_at),
|
||||
authorization_groups: authorization_groups
|
||||
authorization_groups: authorization_groups,
|
||||
app_folder_id: nullify(app_deployment.app_folder_id)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -630,7 +644,8 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
for authorization_group <- authorization_groups do
|
||||
%Teams.AuthorizationGroup{
|
||||
provider_id: authorization_group.provider_id,
|
||||
group_name: authorization_group.group_name
|
||||
group_name: authorization_group.group_name,
|
||||
app_folder_id: nullify(authorization_group.app_folder_id)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -664,6 +679,24 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
}
|
||||
end
|
||||
|
||||
defp put_app_folder(state, app_folder) do
|
||||
state = remove_app_folder(state, app_folder)
|
||||
|
||||
%{state | app_folders: [app_folder | state.app_folders]}
|
||||
end
|
||||
|
||||
defp remove_app_folder(state, app_folder) do
|
||||
%{state | app_folders: Enum.reject(state.app_folders, &(&1.id == app_folder.id))}
|
||||
end
|
||||
|
||||
defp build_app_folder(state, %LivebookProto.AppFolder{} = app_folder) do
|
||||
%Teams.AppFolder{
|
||||
id: app_folder.id,
|
||||
name: app_folder.name,
|
||||
hub_id: state.hub.id
|
||||
}
|
||||
end
|
||||
|
||||
defp handle_event(:secret_created, %Secrets.Secret{} = secret, state) do
|
||||
Hubs.Broadcasts.secret_created(secret)
|
||||
|
||||
|
|
@ -787,6 +820,7 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
|> dispatch_deployment_groups(user_connected)
|
||||
|> dispatch_app_deployments(user_connected)
|
||||
|> dispatch_agents(user_connected)
|
||||
|> dispatch_app_folders(user_connected)
|
||||
|> dispatch_connection()
|
||||
end
|
||||
|
||||
|
|
@ -798,6 +832,7 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
|> dispatch_deployment_groups(agent_connected)
|
||||
|> dispatch_app_deployments(agent_connected)
|
||||
|> dispatch_agents(agent_connected)
|
||||
|> dispatch_app_folders(agent_connected)
|
||||
|> dispatch_connection()
|
||||
end
|
||||
|
||||
|
|
@ -873,6 +908,43 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
update_hub(state, org_updated)
|
||||
end
|
||||
|
||||
defp handle_event(:app_folder_created, %Teams.AppFolder{} = app_folder, state) do
|
||||
Teams.Broadcasts.app_folder_created(app_folder)
|
||||
put_app_folder(state, app_folder)
|
||||
end
|
||||
|
||||
defp handle_event(:app_folder_created, app_folder_created, state) do
|
||||
handle_event(
|
||||
:app_folder_created,
|
||||
build_app_folder(state, app_folder_created.app_folder),
|
||||
state
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_event(:app_folder_updated, %Teams.AppFolder{} = app_folder, state) do
|
||||
Teams.Broadcasts.app_folder_updated(app_folder)
|
||||
put_app_folder(state, app_folder)
|
||||
end
|
||||
|
||||
defp handle_event(:app_folder_updated, app_folder_updated, state) do
|
||||
handle_event(
|
||||
:app_folder_updated,
|
||||
build_app_folder(state, app_folder_updated.app_folder),
|
||||
state
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_event(:app_folder_deleted, %Teams.AppFolder{} = app_folder, state) do
|
||||
Teams.Broadcasts.app_folder_deleted(app_folder)
|
||||
remove_app_folder(state, app_folder)
|
||||
end
|
||||
|
||||
defp handle_event(:app_folder_deleted, %{id: id}, state) do
|
||||
with {:ok, app_folder} <- fetch_app_folder(id, state) do
|
||||
handle_event(:app_folder_deleted, app_folder, state)
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch_secrets(state, %{secrets: secrets}) do
|
||||
decrypted_secrets = Enum.map(secrets, &build_secret(state, &1))
|
||||
|
||||
|
|
@ -936,6 +1008,19 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
dispatch_events(state, agent_joined: joined, agent_left: left)
|
||||
end
|
||||
|
||||
defp dispatch_app_folders(state, %{app_folders: app_folders}) do
|
||||
app_folders = Enum.map(app_folders, &build_app_folder(state, &1))
|
||||
|
||||
{created, deleted, updated} =
|
||||
diff(state.app_folders, app_folders, &(&1.id == &2.id))
|
||||
|
||||
dispatch_events(state,
|
||||
app_folder_deleted: deleted,
|
||||
app_folder_created: created,
|
||||
app_folder_updated: updated
|
||||
)
|
||||
end
|
||||
|
||||
defp dispatch_connection(%{hub: %{id: id}} = state) do
|
||||
Teams.Broadcasts.client_connected(id)
|
||||
state
|
||||
|
|
@ -1064,6 +1149,8 @@ defmodule Livebook.Hubs.TeamClient do
|
|||
defp fetch_app_deployment_from_slug(slug, state),
|
||||
do: fetch_entry(state.app_deployments, &(&1.slug == slug), state)
|
||||
|
||||
defp fetch_app_folder(id, state), do: fetch_entry(state.app_folders, &(&1.id == id), state)
|
||||
|
||||
defp fetch_entry(entries, fun, state) do
|
||||
if entry = Enum.find(entries, fun) do
|
||||
{:ok, entry}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,8 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
:auto_shutdown_ms,
|
||||
:access_type,
|
||||
:show_source,
|
||||
:output_type
|
||||
:output_type,
|
||||
:app_folder_id
|
||||
]
|
||||
|
||||
put_unless_default(
|
||||
|
|
|
|||
|
|
@ -495,6 +495,9 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
{"show_source", show_source}, attrs ->
|
||||
Map.put(attrs, :show_source, show_source)
|
||||
|
||||
{"app_folder_id", app_folder_id}, attrs ->
|
||||
Map.put(attrs, :app_folder_id, app_folder_id)
|
||||
|
||||
{"output_type", output_type}, attrs when output_type in ["all", "rich"] ->
|
||||
Map.put(attrs, :output_type, String.to_atom(output_type))
|
||||
|
||||
|
|
@ -664,7 +667,25 @@ 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?)
|
||||
|
||||
{%{notebook | teams_enabled: teams_enabled}, stamp_verified?, messages}
|
||||
{app_settings, 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}
|
||||
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"
|
||||
]}
|
||||
end
|
||||
else
|
||||
{notebook.app_settings, messages}
|
||||
end
|
||||
|
||||
{%{notebook | app_settings: app_settings, teams_enabled: teams_enabled}, stamp_verified?,
|
||||
messages}
|
||||
end
|
||||
|
||||
defp safe_binary_split(binary, offset)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ defmodule Livebook.Notebook.AppSettings do
|
|||
access_type: access_type(),
|
||||
password: String.t() | nil,
|
||||
show_source: boolean(),
|
||||
output_type: output_type()
|
||||
output_type: output_type(),
|
||||
app_folder_id: String.t() | nil
|
||||
}
|
||||
|
||||
@type access_type :: :public | :protected
|
||||
|
|
@ -33,6 +34,7 @@ defmodule Livebook.Notebook.AppSettings do
|
|||
field :password, :string
|
||||
field :show_source, :boolean
|
||||
field :output_type, Ecto.Enum, values: [:all, :rich]
|
||||
field :app_folder_id, :string
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -49,7 +51,8 @@ defmodule Livebook.Notebook.AppSettings do
|
|||
access_type: :protected,
|
||||
password: generate_password(),
|
||||
show_source: false,
|
||||
output_type: :all
|
||||
output_type: :all,
|
||||
app_folder_id: nil
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -82,7 +85,8 @@ defmodule Livebook.Notebook.AppSettings do
|
|||
:auto_shutdown_ms,
|
||||
:access_type,
|
||||
:show_source,
|
||||
:output_type
|
||||
:output_type,
|
||||
:app_folder_id
|
||||
])
|
||||
|> validate_required([
|
||||
:slug,
|
||||
|
|
|
|||
|
|
@ -904,6 +904,7 @@ defmodule Livebook.Session do
|
|||
def init({caller_pid, opts}) do
|
||||
Livebook.Settings.subscribe()
|
||||
Livebook.Hubs.Broadcasts.subscribe([:crud, :secrets, :file_systems])
|
||||
Livebook.Teams.Broadcasts.subscribe(:app_folders)
|
||||
|
||||
id = Keyword.fetch!(opts, :id)
|
||||
|
||||
|
|
@ -2028,6 +2029,13 @@ defmodule Livebook.Session do
|
|||
{:noreply, handle_operation(state, operation)}
|
||||
end
|
||||
|
||||
def handle_info({event, app_folder}, state)
|
||||
when event in [:app_folder_created, :app_folder_updated, :app_folder_deleted] and
|
||||
app_folder.hub_id == state.data.notebook.hub_id do
|
||||
operation = {:sync_hub_app_folders, @client_id}
|
||||
{:noreply, handle_operation(state, operation)}
|
||||
end
|
||||
|
||||
def handle_info({:hub_deleted, id}, %{data: %{notebook: %{hub_id: id}}} = state) do
|
||||
# Since the hub got deleted, we close all sessions using that hub.
|
||||
# This way we clean up all secrets and other in-memory state that
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ defmodule Livebook.Session.Data do
|
|||
:secrets,
|
||||
:hub_secrets,
|
||||
:hub_file_systems,
|
||||
:hub_app_folders,
|
||||
:mode,
|
||||
:deployed_app_slug,
|
||||
:app_data
|
||||
|
|
@ -247,6 +248,7 @@ defmodule Livebook.Session.Data do
|
|||
| {:set_notebook_hub, client_id(), String.t()}
|
||||
| {:sync_hub_secrets, client_id()}
|
||||
| {:sync_hub_file_systems, client_id()}
|
||||
| {:sync_hub_app_folders, client_id()}
|
||||
| {:add_file_entries, client_id(), list(Notebook.file_entry())}
|
||||
| {:rename_file_entry, client_id(), name :: String.t(), new_name :: String.t()}
|
||||
| {:delete_file_entry, client_id(), String.t()}
|
||||
|
|
@ -305,6 +307,7 @@ defmodule Livebook.Session.Data do
|
|||
hub = Livebook.Hubs.fetch_hub!(notebook.hub_id)
|
||||
hub_secrets = Livebook.Hubs.get_secrets(hub)
|
||||
hub_file_systems = Livebook.Hubs.get_file_systems(hub)
|
||||
hub_app_folders = Livebook.Hubs.Provider.get_app_folders(hub)
|
||||
|
||||
startup_secrets =
|
||||
for secret <- Livebook.Secrets.get_startup_secrets(),
|
||||
|
|
@ -338,6 +341,7 @@ defmodule Livebook.Session.Data do
|
|||
secrets: secrets,
|
||||
hub_secrets: hub_secrets,
|
||||
hub_file_systems: hub_file_systems,
|
||||
hub_app_folders: hub_app_folders,
|
||||
mode: opts[:mode],
|
||||
deployed_app_slug: nil,
|
||||
app_data: app_data
|
||||
|
|
@ -1074,6 +1078,14 @@ defmodule Livebook.Session.Data do
|
|||
|> wrap_ok()
|
||||
end
|
||||
|
||||
def apply_operation(data, {:sync_hub_app_folders, _client_id}) do
|
||||
data
|
||||
|> with_actions()
|
||||
|> sync_hub_app_folders()
|
||||
|> set_dirty()
|
||||
|> wrap_ok()
|
||||
end
|
||||
|
||||
def apply_operation(data, {:add_file_entries, _client_id, file_entries}) do
|
||||
data
|
||||
|> with_actions()
|
||||
|
|
@ -1965,7 +1977,8 @@ defmodule Livebook.Session.Data do
|
|||
teams_enabled: is_struct(hub, Livebook.Hubs.Team)
|
||||
},
|
||||
hub_secrets: Livebook.Hubs.get_secrets(hub),
|
||||
hub_file_systems: Livebook.Hubs.get_file_systems(hub)
|
||||
hub_file_systems: Livebook.Hubs.get_file_systems(hub),
|
||||
hub_app_folders: Livebook.Hubs.Provider.get_app_folders(hub)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -1985,6 +1998,12 @@ defmodule Livebook.Session.Data do
|
|||
set!(data_actions, hub_file_systems: file_systems)
|
||||
end
|
||||
|
||||
defp sync_hub_app_folders({data, _} = data_actions) do
|
||||
hub = Livebook.Hubs.fetch_hub!(data.notebook.hub_id)
|
||||
app_folders = Livebook.Hubs.Provider.get_app_folders(hub)
|
||||
set!(data_actions, hub_app_folders: app_folders)
|
||||
end
|
||||
|
||||
defp update_notebook_hub_secret_names({data, _} = data_actions) do
|
||||
hub_secret_names =
|
||||
for {_name, secret} <- data.secrets, secret.hub_id == data.notebook.hub_id, do: secret.name
|
||||
|
|
|
|||
|
|
@ -305,4 +305,12 @@ defmodule Livebook.Teams do
|
|||
def user_can_deploy?(%Team{} = team, %Teams.DeploymentGroup{} = deployment_group) do
|
||||
TeamClient.user_can_deploy?(team.id, team.user_id, deployment_group.id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a list of app folders for a given Hub.
|
||||
"""
|
||||
@spec get_app_folders(Team.t()) :: list(Teams.AppFolder.t())
|
||||
def get_app_folders(team) do
|
||||
Hubs.Provider.get_app_folders(team)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ defmodule Livebook.Teams.AppDeployment do
|
|||
access_type: Livebook.Notebook.AppSettings.access_type(),
|
||||
hub_id: String.t() | nil,
|
||||
deployment_group_id: String.t() | nil,
|
||||
app_folder_id: String.t() | nil,
|
||||
file: binary() | nil,
|
||||
deployed_by: String.t() | nil,
|
||||
deployed_at: DateTime.t() | nil,
|
||||
|
|
@ -32,6 +33,7 @@ defmodule Livebook.Teams.AppDeployment do
|
|||
field :access_type, Ecto.Enum, values: @access_types
|
||||
field :hub_id, :string
|
||||
field :deployment_group_id, :string
|
||||
field :app_folder_id, :string
|
||||
field :file, :string
|
||||
field :deployed_by, :string
|
||||
field :deployed_at, :utc_datetime
|
||||
|
|
@ -75,6 +77,7 @@ defmodule Livebook.Teams.AppDeployment do
|
|||
title: notebook.name,
|
||||
multi_session: notebook.app_settings.multi_session,
|
||||
access_type: notebook.app_settings.access_type,
|
||||
app_folder_id: notebook.app_settings.app_folder_id,
|
||||
hub_id: notebook.hub_id,
|
||||
deployment_group_id: notebook.deployment_group_id,
|
||||
file: zip_content
|
||||
|
|
|
|||
15
lib/livebook/teams/app_folder.ex
Normal file
15
lib/livebook/teams/app_folder.ex
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
defmodule Livebook.Teams.AppFolder do
|
||||
use Ecto.Schema
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t() | nil,
|
||||
name: String.t() | nil,
|
||||
hub_id: String.t() | nil
|
||||
}
|
||||
|
||||
@primary_key {:id, :string, autogenerate: false}
|
||||
embedded_schema do
|
||||
field :name, :string
|
||||
field :hub_id, :string
|
||||
end
|
||||
end
|
||||
|
|
@ -3,12 +3,14 @@ defmodule Livebook.Teams.AuthorizationGroup do
|
|||
|
||||
@type t :: %__MODULE__{
|
||||
provider_id: String.t() | nil,
|
||||
group_name: String.t() | nil
|
||||
group_name: String.t() | nil,
|
||||
app_folder_id: String.t() | nil
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :provider_id, :string
|
||||
field :group_name, :string
|
||||
field :app_folder_id, :string
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Livebook.Teams.Broadcasts do
|
|||
@app_deployments_topic "teams:app_deployments"
|
||||
@clients_topic "teams:clients"
|
||||
@deployment_groups_topic "teams:deployment_groups"
|
||||
@app_folders_topic "teams:app_folders"
|
||||
@app_server_topic "teams:app_server"
|
||||
|
||||
@doc """
|
||||
|
|
@ -40,6 +41,12 @@ defmodule Livebook.Teams.Broadcasts do
|
|||
|
||||
* `{:server_authorization_updated, DeploymentGroup.t()}`
|
||||
|
||||
Topic `#{@app_folders_topic}`:
|
||||
|
||||
* `{:app_folder_created, AppFolder.t()}`
|
||||
* `{:app_folder_updated, AppFolder.t()}`
|
||||
* `{:app_folder_deleted, AppFolder.t()}`
|
||||
|
||||
"""
|
||||
@spec subscribe(atom() | list(atom())) :: :ok | {:error, term()}
|
||||
def subscribe(topics) when is_list(topics) do
|
||||
|
|
@ -154,6 +161,30 @@ defmodule Livebook.Teams.Broadcasts do
|
|||
broadcast(@app_server_topic, {:server_authorization_updated, deployment_group})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts under `#{@app_folders_topic}` topic when hub received a new app folder.
|
||||
"""
|
||||
@spec app_folder_created(Teams.AppFolder.t()) :: broadcast()
|
||||
def app_folder_created(%Teams.AppFolder{} = app_folder) do
|
||||
broadcast(@app_folders_topic, {:app_folder_created, app_folder})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts under `#{@app_folders_topic}` topic when hub received an updated app folder.
|
||||
"""
|
||||
@spec app_folder_updated(Teams.AppFolder.t()) :: broadcast()
|
||||
def app_folder_updated(%Teams.AppFolder{} = app_folder) do
|
||||
broadcast(@app_folders_topic, {:app_folder_updated, app_folder})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts under `#{@app_folders_topic}` topic when hub received a deleted app folder.
|
||||
"""
|
||||
@spec app_folder_deleted(Teams.AppFolder.t()) :: broadcast()
|
||||
def app_folder_deleted(%Teams.AppFolder{} = app_folder) do
|
||||
broadcast(@app_folders_topic, {:app_folder_deleted, app_folder})
|
||||
end
|
||||
|
||||
defp broadcast(topic, message) do
|
||||
Phoenix.PubSub.broadcast(Livebook.PubSub, topic, message)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ defmodule Livebook.Teams.Requests do
|
|||
slug: app_deployment.slug,
|
||||
multi_session: app_deployment.multi_session,
|
||||
access_type: app_deployment.access_type,
|
||||
app_folder_id: app_deployment.app_folder_id,
|
||||
deployment_group_id: app_deployment.deployment_group_id,
|
||||
sha: app_deployment.sha
|
||||
}
|
||||
|
|
@ -249,6 +250,7 @@ defmodule Livebook.Teams.Requests do
|
|||
slug: app_deployment.slug,
|
||||
multi_session: app_deployment.multi_session,
|
||||
access_type: app_deployment.access_type,
|
||||
app_folder_id: app_deployment.app_folder_id,
|
||||
deployment_group_id: deployment_group_id,
|
||||
sha: app_deployment.sha
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1865,6 +1865,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
hub: Livebook.Hubs.fetch_hub!(data.notebook.hub_id),
|
||||
hub_secrets: data.hub_secrets,
|
||||
hub_file_systems: data.hub_file_systems,
|
||||
hub_app_folders: data.hub_app_folders,
|
||||
any_session_secrets?:
|
||||
Session.Data.session_secrets(data.secrets, data.notebook.hub_id) != [],
|
||||
file_entries: Enum.sort_by(data.notebook.file_entries, & &1.name),
|
||||
|
|
|
|||
|
|
@ -12,10 +12,15 @@ defmodule LivebookWeb.SessionLive.AppSettingsComponent do
|
|||
_ -> AppSettings.change(assigns.settings)
|
||||
end
|
||||
|
||||
app_folder_options =
|
||||
for app_folder <- assigns.app_folders do
|
||||
{app_folder.name, app_folder.id}
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(changeset: changeset)}
|
||||
|> assign(app_folder_options: app_folder_options, changeset: changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -42,6 +47,17 @@ 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
|
||||
field={f[:app_folder_id]}
|
||||
label="Folder"
|
||||
prompt="Select a folder..."
|
||||
options={@app_folder_options}
|
||||
help={
|
||||
~S'''
|
||||
You can create folders inside Teams to organize how apps are displayed.
|
||||
'''
|
||||
}
|
||||
/>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<.checkbox_field
|
||||
field={f[:access_type]}
|
||||
|
|
|
|||
|
|
@ -212,7 +212,11 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
|||
|
||||
<div :if={@app_deployment} class="space-y-3">
|
||||
<p class="text-gray-700">Current version:</p>
|
||||
<.app_deployment_card app_deployment={@app_deployment} deployment_group={@deployment_group} />
|
||||
<.app_deployment_card
|
||||
app_deployment={@app_deployment}
|
||||
deployment_group={@deployment_group}
|
||||
hub={@hub}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.message_box :if={@num_agents[@deployment_group.id] == nil} kind="warning">
|
||||
|
|
@ -293,6 +297,7 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
|||
:if={@app_deployment}
|
||||
app_deployment={@app_deployment}
|
||||
deployment_group={@deployment_group}
|
||||
hub={@hub}
|
||||
/>
|
||||
<div>
|
||||
<.button color="gray" outlined phx-click="go_deployment_groups">
|
||||
|
|
@ -392,6 +397,9 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
|||
<.labeled_text label="Title">
|
||||
{@app_deployment.title}
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Folder">
|
||||
{app_folder_name(@hub, @app_deployment.app_folder_id)}
|
||||
</.labeled_text>
|
||||
<.labeled_text label="Deployed by">
|
||||
{@app_deployment.deployed_by}
|
||||
</.labeled_text>
|
||||
|
|
@ -593,4 +601,12 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
|||
String.replace(acc, "%{#{key}}", to_string(value))
|
||||
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))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ defmodule LivebookWeb.SessionLive.Render do
|
|||
settings={@data_view.app_settings}
|
||||
context={@action_assigns.context}
|
||||
deployed_app_slug={@data_view.deployed_app_slug}
|
||||
app_folders={@data_view.hub_app_folders}
|
||||
/>
|
||||
</.modal>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ defmodule LivebookProto.AuthorizationGroup do
|
|||
|
||||
field :provider_id, 1, type: :string, json_name: "providerId"
|
||||
field :group_name, 2, type: :string, json_name: "groupName"
|
||||
field :app_folder_ids, 3, repeated: true, type: :string, json_name: "appFolderIds"
|
||||
field :app_folder_id, 3, type: :string, json_name: "appFolderId"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ message EnvironmentVariable {
|
|||
message AuthorizationGroup {
|
||||
string provider_id = 1;
|
||||
string group_name = 2;
|
||||
repeated string app_folder_ids = 3;
|
||||
string app_folder_id = 3;
|
||||
}
|
||||
|
||||
message DeploymentUser {
|
||||
|
|
|
|||
|
|
@ -1152,12 +1152,13 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
auto_shutdown_ms: 5_000,
|
||||
access_type: :public,
|
||||
show_source: true,
|
||||
output_type: :rich
|
||||
output_type: :rich,
|
||||
app_folder_id: "123"
|
||||
}
|
||||
}
|
||||
|
||||
expected_document = """
|
||||
<!-- livebook:{"app_settings":{"access_type":"public","auto_shutdown_ms":5000,"multi_session":true,"output_type":"rich","show_existing_sessions":true,"show_source":true,"slug":"app"}} -->
|
||||
<!-- livebook:{"app_settings":{"access_type":"public","app_folder_id":"123","auto_shutdown_ms":5000,"multi_session":true,"output_type":"rich","show_existing_sessions":true,"show_source":true,"slug":"app"}} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -785,7 +785,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
describe "app settings" do
|
||||
test "imports settings" do
|
||||
markdown = """
|
||||
<!-- livebook:{"app_settings":{"access_type":"public","auto_shutdown_ms":5000,"multi_session":true,"output_type":"rich","show_existing_sessions":false,"show_source":true,"slug":"app"}} -->
|
||||
<!-- livebook:{"app_settings":{"access_type":"public","app_folder_id":"123","auto_shutdown_ms":5000,"multi_session":true,"output_type":"rich","show_existing_sessions":false,"show_source":true,"slug":"app"}} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
|
@ -802,7 +802,8 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
auto_shutdown_ms: 5_000,
|
||||
access_type: :public,
|
||||
show_source: true,
|
||||
output_type: :rich
|
||||
output_type: :rich,
|
||||
app_folder_id: "123"
|
||||
}
|
||||
} = notebook
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,11 +23,14 @@ defmodule LivebookCLI.Integration.DeployTest do
|
|||
app_path = Path.join(tmp_dir, "#{slug}.livemd")
|
||||
{key, _} = TeamsRPC.create_org_token(node, org: org)
|
||||
deployment_group = TeamsRPC.create_deployment_group(node, org: org, url: @url)
|
||||
app_folder = TeamsRPC.create_app_folder(node, org: org)
|
||||
|
||||
hub_id = team.id
|
||||
deployment_group_id = to_string(deployment_group.id)
|
||||
app_folder_id = to_string(app_folder.id)
|
||||
|
||||
stamp_notebook(app_path, """
|
||||
<!-- livebook:{"app_settings":{"access_type":"public","slug":"#{slug}"},"hub_id":"#{hub_id}"} -->
|
||||
<!-- livebook:{"app_settings":{"access_type":"public","app_folder_id":"#{app_folder_id}","slug":"#{slug}"},"hub_id":"#{hub_id}"} -->
|
||||
|
||||
# #{title}
|
||||
|
||||
|
|
@ -56,6 +59,7 @@ defmodule LivebookCLI.Integration.DeployTest do
|
|||
title: ^title,
|
||||
slug: ^slug,
|
||||
deployment_group_id: ^deployment_group_id,
|
||||
app_folder_id: ^app_folder_id,
|
||||
hub_id: ^hub_id,
|
||||
deployed_by: "CLI"
|
||||
}}
|
||||
|
|
@ -106,6 +110,7 @@ defmodule LivebookCLI.Integration.DeployTest do
|
|||
title: ^title,
|
||||
slug: ^slug,
|
||||
deployment_group_id: ^deployment_group_id,
|
||||
app_folder_id: nil,
|
||||
hub_id: ^hub_id,
|
||||
deployed_by: "CLI"
|
||||
}}
|
||||
|
|
@ -168,6 +173,7 @@ defmodule LivebookCLI.Integration.DeployTest do
|
|||
title: ^title,
|
||||
slug: ^slug,
|
||||
deployment_group_id: ^deployment_group_id,
|
||||
app_folder_id: nil,
|
||||
hub_id: ^hub_id,
|
||||
deployed_by: "CLI"
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ defmodule Livebook.Hubs.TeamClientTest do
|
|||
setup :teams
|
||||
|
||||
@moduletag subscribe_to_hubs_topics: [:crud, :connection, :file_systems, :secrets]
|
||||
@moduletag subscribe_to_teams_topics: [:clients, :deployment_groups, :app_deployments, :agents]
|
||||
@moduletag subscribe_to_teams_topics: [
|
||||
:clients,
|
||||
:deployment_groups,
|
||||
:app_deployments,
|
||||
:agents,
|
||||
:app_folders
|
||||
]
|
||||
|
||||
describe "connect" do
|
||||
@describetag teams_for: :user
|
||||
|
|
@ -303,6 +309,44 @@ defmodule Livebook.Hubs.TeamClientTest do
|
|||
assert_receive {:agent_left, ^agent}
|
||||
refute agent in TeamClient.get_agents(team.id)
|
||||
end
|
||||
|
||||
test "dispatches the app folders list",
|
||||
%{team: team, pid: pid, user_connected: user_connected} do
|
||||
app_folder = build(:app_folder, hub_id: team.id)
|
||||
|
||||
livebook_proto_app_folder =
|
||||
%LivebookProto.AppFolder{
|
||||
id: app_folder.id,
|
||||
name: app_folder.name
|
||||
}
|
||||
|
||||
# creates the app folder
|
||||
user_connected = %{user_connected | app_folders: [livebook_proto_app_folder]}
|
||||
refute_received {:app_folder_created, ^app_folder}
|
||||
send(pid, {:event, :user_connected, user_connected})
|
||||
assert_receive {:app_folder_created, ^app_folder}
|
||||
assert app_folder in TeamClient.get_app_folders(team.id)
|
||||
|
||||
# updates the app folder
|
||||
updated_app_folder = %{app_folder | name: "ChonkiestCat"}
|
||||
|
||||
updated_livebook_proto_app_folder = %{
|
||||
livebook_proto_app_folder
|
||||
| name: updated_app_folder.name
|
||||
}
|
||||
|
||||
user_connected = %{user_connected | app_folders: [updated_livebook_proto_app_folder]}
|
||||
send(pid, {:event, :user_connected, user_connected})
|
||||
assert_receive {:app_folder_updated, ^updated_app_folder}
|
||||
refute app_folder in TeamClient.get_app_folders(team.id)
|
||||
assert updated_app_folder in TeamClient.get_app_folders(team.id)
|
||||
|
||||
# deletes the app folder
|
||||
user_connected = %{user_connected | app_folders: []}
|
||||
send(pid, {:event, :user_connected, user_connected})
|
||||
assert_receive {:app_folder_deleted, ^updated_app_folder}
|
||||
refute updated_app_folder in TeamClient.get_app_folders(team.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle agent_connected event" do
|
||||
|
|
@ -809,5 +853,43 @@ defmodule Livebook.Hubs.TeamClientTest do
|
|||
assert_receive {:agent_left, ^agent}
|
||||
refute agent in TeamClient.get_agents(team.id)
|
||||
end
|
||||
|
||||
test "dispatches the app folders list",
|
||||
%{team: team, pid: pid, agent_connected: agent_connected} do
|
||||
app_folder = build(:app_folder, hub_id: team.id)
|
||||
|
||||
livebook_proto_app_folder =
|
||||
%LivebookProto.AppFolder{
|
||||
id: app_folder.id,
|
||||
name: app_folder.name
|
||||
}
|
||||
|
||||
# creates the app folder
|
||||
agent_connected = %{agent_connected | app_folders: [livebook_proto_app_folder]}
|
||||
refute_received {:app_folder_created, ^app_folder}
|
||||
send(pid, {:event, :agent_connected, agent_connected})
|
||||
assert_receive {:app_folder_created, ^app_folder}
|
||||
assert app_folder in TeamClient.get_app_folders(team.id)
|
||||
|
||||
# updates the app folder
|
||||
updated_app_folder = %{app_folder | name: "ChonkiestCat"}
|
||||
|
||||
updated_livebook_proto_app_folder = %{
|
||||
livebook_proto_app_folder
|
||||
| name: updated_app_folder.name
|
||||
}
|
||||
|
||||
agent_connected = %{agent_connected | app_folders: [updated_livebook_proto_app_folder]}
|
||||
send(pid, {:event, :agent_connected, agent_connected})
|
||||
assert_receive {:app_folder_updated, ^updated_app_folder}
|
||||
refute app_folder in TeamClient.get_app_folders(team.id)
|
||||
assert updated_app_folder in TeamClient.get_app_folders(team.id)
|
||||
|
||||
# deletes the app folder
|
||||
agent_connected = %{agent_connected | app_folders: []}
|
||||
send(pid, {:event, :agent_connected, agent_connected})
|
||||
assert_receive {:app_folder_deleted, ^updated_app_folder}
|
||||
refute updated_app_folder in TeamClient.get_app_folders(team.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
48
test/livebook_teams/live_markdown/import_test.exs
Normal file
48
test/livebook_teams/live_markdown/import_test.exs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
defmodule Livebook.Integration.LiveMarkdown.ImportTest do
|
||||
use Livebook.TeamsIntegrationCase, async: true
|
||||
|
||||
alias Livebook.Notebook
|
||||
alias Livebook.LiveMarkdown
|
||||
|
||||
@moduletag teams_for: :user
|
||||
setup :teams
|
||||
|
||||
@moduletag subscribe_to_hubs_topics: [:connection]
|
||||
@moduletag subscribe_to_teams_topics: [:clients, :app_folders]
|
||||
|
||||
describe "app settings" do
|
||||
test "don't import app folder if does not exists anymore",
|
||||
%{node: node, team: team, org: org} do
|
||||
app_folder = TeamsRPC.create_app_folder(node, name: "delete me", org: org)
|
||||
|
||||
app_folder_id = to_string(app_folder.id)
|
||||
hub_id = team.id
|
||||
|
||||
assert_receive {:app_folder_created, %{id: ^app_folder_id, hub_id: ^hub_id}}
|
||||
|
||||
notebook = %{
|
||||
Notebook.new()
|
||||
| name: "Deleted from folder",
|
||||
hub_id: hub_id,
|
||||
app_settings: %{Notebook.AppSettings.new() | app_folder_id: app_folder_id},
|
||||
sections: [
|
||||
%{
|
||||
Notebook.Section.new()
|
||||
| name: "Section 1",
|
||||
cells: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
{markdown, []} = LiveMarkdown.Export.notebook_to_livemd(notebook)
|
||||
|
||||
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 is assigned to a non-existent app folder, defaulting to ungrouped app folder" in warnings
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -440,7 +440,8 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
:agents,
|
||||
:app_deployments,
|
||||
:deployment_groups,
|
||||
:app_server
|
||||
:app_server,
|
||||
:app_folders
|
||||
]
|
||||
|
||||
test "shows a message when non-teams hub is selected", %{conn: conn, session: session} do
|
||||
|
|
@ -491,7 +492,6 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
|
||||
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)
|
||||
|
|
@ -505,9 +505,8 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
|> element("button", "Deploy")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~
|
||||
"App deployment created successfully"
|
||||
|
||||
assert render(view) =~ "App deployment created successfully"
|
||||
assert render(view) =~ "Ungrouped apps"
|
||||
assert render(view) =~ "#{Livebook.Config.teams_url()}/orgs/#{team.org_id}"
|
||||
end
|
||||
|
||||
|
|
@ -567,8 +566,71 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
|> element("button", "Deploy")
|
||||
|> render_click()
|
||||
|
||||
assert render(view) =~
|
||||
"App deployment created successfully"
|
||||
assert render(view) =~ "App deployment created successfully"
|
||||
assert render(view) =~ "Ungrouped apps"
|
||||
end
|
||||
|
||||
test "deployment flow with existing app folders in the hub",
|
||||
%{team: team, conn: conn, node: node, session: session, org: org} do
|
||||
Session.set_notebook_hub(session.pid, team.id)
|
||||
|
||||
id = insert_deployment_group(mode: :online, hub_id: team.id).id
|
||||
assert_receive {:deployment_group_created, %{id: ^id} = deployment_group}
|
||||
|
||||
app_folder_id = to_string(TeamsRPC.create_app_folder(node, org: org).id)
|
||||
assert_receive {:app_folder_created, %{id: ^app_folder_id} = app_folder}
|
||||
|
||||
{: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, "app_folder_id" => app_folder_id}})
|
||||
|
||||
# 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()
|
||||
|
||||
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 created successfully"
|
||||
assert render(view) =~ app_folder.name
|
||||
end
|
||||
|
||||
test "shows tooltip message if user is unauthorized to deploy apps",
|
||||
|
|
@ -740,4 +802,43 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
|||
refute has_element?(view, ~s{button[id*="file-system-#{file_system.id}"]})
|
||||
end
|
||||
end
|
||||
|
||||
describe "app settings" do
|
||||
@describetag subscribe_to_teams_topics: [:clients, :app_folders]
|
||||
|
||||
test "updates the list of app folders",
|
||||
%{team: team, conn: conn, node: node, session: session, org: org} do
|
||||
Session.set_notebook_hub(session.pid, team.id)
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
|
||||
assert view
|
||||
|> element(~s/[data-el-app-info] a/, "Configure")
|
||||
|> render_click() =~ ~s(name="app_settings[app_folder_id]")
|
||||
|
||||
assert render(view) =~ ~s(<option value="">Select a folder...</option></select>)
|
||||
|
||||
app_folder = TeamsRPC.create_app_folder(node, name: "Tidewave", org: org)
|
||||
id = to_string(app_folder.id)
|
||||
|
||||
assert_receive {:app_folder_created, %{id: ^id, name: "Tidewave"}}
|
||||
assert_receive {:operation, {:sync_hub_app_folders, _}}
|
||||
|
||||
assert render(view) =~
|
||||
~s(<option value="">Select a folder...</option><option value="#{id}">Tidewave</option></select>)
|
||||
|
||||
{:ok, %{name: "Wavetide"}} = TeamsRPC.update_app_folder(node, app_folder, name: "Wavetide")
|
||||
|
||||
assert_receive {:app_folder_updated, %{id: ^id, name: "Wavetide"}}
|
||||
assert_receive {:operation, {:sync_hub_app_folders, _}}
|
||||
refute render(view) =~ ~s(<option value="#{id}">Tidewave</option>)
|
||||
assert render(view) =~ ~s(<option value="#{id}">Wavetide</option>)
|
||||
|
||||
TeamsRPC.delete_app_folder(node, app_folder)
|
||||
|
||||
assert_receive {:app_folder_deleted, %{id: ^id, name: "Wavetide"}}
|
||||
assert_receive {:operation, {:sync_hub_app_folders, _}}
|
||||
refute render(view) =~ ~s(<option value="#{id}">Tidewave</option>)
|
||||
refute render(view) =~ ~s(<option value="#{id}">Wavetide</option>)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -162,6 +162,13 @@ defmodule Livebook.Factory do
|
|||
}
|
||||
end
|
||||
|
||||
def build(:app_folder) do
|
||||
%Livebook.Teams.AppFolder{
|
||||
id: "#{unique_integer()}",
|
||||
name: unique_value("app_folder")
|
||||
}
|
||||
end
|
||||
|
||||
def build(factory_name, attrs) do
|
||||
factory_name |> build() |> struct!(attrs)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -163,6 +163,10 @@ defmodule Livebook.TeamsRPC do
|
|||
{key, :erpc.call(node, TeamsRPC, :create_org_token, [key, attrs])}
|
||||
end
|
||||
|
||||
def create_app_folder(node, attrs \\ []) do
|
||||
:erpc.call(node, TeamsRPC, :create_app_folder, [attrs])
|
||||
end
|
||||
|
||||
# Update resource
|
||||
|
||||
def update_authorization_group(node, authorization_group, attrs) do
|
||||
|
|
@ -191,6 +195,10 @@ defmodule Livebook.TeamsRPC do
|
|||
:erpc.call(node, TeamsRPC, :update_file_system, [file_system.external_id, org_key, attrs])
|
||||
end
|
||||
|
||||
def update_app_folder(node, app_folder, attrs \\ []) do
|
||||
:erpc.call(node, TeamsRPC, :update_app_folder, [app_folder, attrs])
|
||||
end
|
||||
|
||||
# Delete resource
|
||||
|
||||
def delete_user_org(node, user_id, org_id) do
|
||||
|
|
@ -206,6 +214,10 @@ defmodule Livebook.TeamsRPC do
|
|||
:erpc.call(node, TeamsRPC, :delete_file_system, [id, org_key, livebook_version])
|
||||
end
|
||||
|
||||
def delete_app_folder(node, app_folder) do
|
||||
:erpc.call(node, TeamsRPC, :delete_app_folder, [app_folder])
|
||||
end
|
||||
|
||||
# Actions
|
||||
|
||||
def upload_app_deployment(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue