Skip to content

Commit 82b2b28

Browse files
authored
Allow users to assign folders to their apps (#3088)
1 parent 6285a5f commit 82b2b28

File tree

29 files changed

+533
-26
lines changed

29 files changed

+533
-26
lines changed

lib/livebook/hubs/personal.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,6 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
281281
def deployment_groups(_personal), do: nil
282282

283283
def get_app_specs(_personal), do: []
284+
285+
def get_app_folders(_personal), do: []
284286
end

lib/livebook/hubs/provider.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,10 @@ defprotocol Livebook.Hubs.Provider do
155155
"""
156156
@spec get_app_specs(t()) :: list(Livebook.Apps.AppSpec.t())
157157
def get_app_specs(hub)
158+
159+
@doc """
160+
Gets the app folders from the given hub.
161+
"""
162+
@spec get_app_folders(t()) :: list(%{id: String.t(), name: String.t()})
163+
def get_app_folders(hub)
158164
end

lib/livebook/hubs/team.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,12 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
259259
end
260260
end
261261

262+
def get_app_folders(team) do
263+
team.id
264+
|> TeamClient.get_app_folders()
265+
|> Enum.sort_by(& &1.name)
266+
end
267+
262268
defp parse_secret_errors(errors_map) do
263269
Teams.Requests.to_error_list(Secret, errors_map)
264270
end

lib/livebook/hubs/team_client.ex

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule Livebook.Hubs.TeamClient do
2424
deployment_groups: [],
2525
app_deployments: [],
2626
agents: [],
27+
app_folders: [],
2728
app_deployment_statuses: nil
2829
]
2930

@@ -172,6 +173,14 @@ defmodule Livebook.Hubs.TeamClient do
172173
GenServer.call(registry_name(id), {:user_can_deploy?, user_id, deployment_group_id})
173174
end
174175

176+
@doc """
177+
Returns a list of cached app folders.
178+
"""
179+
@spec get_app_folders(String.t()) :: list(Teams.AppFolder.t())
180+
def get_app_folders(id) do
181+
GenServer.call(registry_name(id), :get_app_folders)
182+
end
183+
175184
@doc """
176185
Returns if the Team client is connected.
177186
"""
@@ -370,6 +379,10 @@ defmodule Livebook.Hubs.TeamClient do
370379
end
371380
end
372381

382+
def handle_call(:get_app_folders, _caller, state) do
383+
{:reply, state.app_folders, state}
384+
end
385+
373386
@impl true
374387
def handle_info(:connected, state) do
375388
Hubs.Broadcasts.hub_connected(state.hub.id)
@@ -611,7 +624,8 @@ defmodule Livebook.Hubs.TeamClient do
611624
file: nil,
612625
deployed_by: app_deployment.deployed_by,
613626
deployed_at: DateTime.from_gregorian_seconds(app_deployment.deployed_at),
614-
authorization_groups: authorization_groups
627+
authorization_groups: authorization_groups,
628+
app_folder_id: nullify(app_deployment.app_folder_id)
615629
}
616630
end
617631

@@ -630,7 +644,8 @@ defmodule Livebook.Hubs.TeamClient do
630644
for authorization_group <- authorization_groups do
631645
%Teams.AuthorizationGroup{
632646
provider_id: authorization_group.provider_id,
633-
group_name: authorization_group.group_name
647+
group_name: authorization_group.group_name,
648+
app_folder_id: nullify(authorization_group.app_folder_id)
634649
}
635650
end
636651
end
@@ -664,6 +679,24 @@ defmodule Livebook.Hubs.TeamClient do
664679
}
665680
end
666681

682+
defp put_app_folder(state, app_folder) do
683+
state = remove_app_folder(state, app_folder)
684+
685+
%{state | app_folders: [app_folder | state.app_folders]}
686+
end
687+
688+
defp remove_app_folder(state, app_folder) do
689+
%{state | app_folders: Enum.reject(state.app_folders, &(&1.id == app_folder.id))}
690+
end
691+
692+
defp build_app_folder(state, %LivebookProto.AppFolder{} = app_folder) do
693+
%Teams.AppFolder{
694+
id: app_folder.id,
695+
name: app_folder.name,
696+
hub_id: state.hub.id
697+
}
698+
end
699+
667700
defp handle_event(:secret_created, %Secrets.Secret{} = secret, state) do
668701
Hubs.Broadcasts.secret_created(secret)
669702

@@ -787,6 +820,7 @@ defmodule Livebook.Hubs.TeamClient do
787820
|> dispatch_deployment_groups(user_connected)
788821
|> dispatch_app_deployments(user_connected)
789822
|> dispatch_agents(user_connected)
823+
|> dispatch_app_folders(user_connected)
790824
|> dispatch_connection()
791825
end
792826

@@ -798,6 +832,7 @@ defmodule Livebook.Hubs.TeamClient do
798832
|> dispatch_deployment_groups(agent_connected)
799833
|> dispatch_app_deployments(agent_connected)
800834
|> dispatch_agents(agent_connected)
835+
|> dispatch_app_folders(agent_connected)
801836
|> dispatch_connection()
802837
end
803838

@@ -873,6 +908,43 @@ defmodule Livebook.Hubs.TeamClient do
873908
update_hub(state, org_updated)
874909
end
875910

911+
defp handle_event(:app_folder_created, %Teams.AppFolder{} = app_folder, state) do
912+
Teams.Broadcasts.app_folder_created(app_folder)
913+
put_app_folder(state, app_folder)
914+
end
915+
916+
defp handle_event(:app_folder_created, app_folder_created, state) do
917+
handle_event(
918+
:app_folder_created,
919+
build_app_folder(state, app_folder_created.app_folder),
920+
state
921+
)
922+
end
923+
924+
defp handle_event(:app_folder_updated, %Teams.AppFolder{} = app_folder, state) do
925+
Teams.Broadcasts.app_folder_updated(app_folder)
926+
put_app_folder(state, app_folder)
927+
end
928+
929+
defp handle_event(:app_folder_updated, app_folder_updated, state) do
930+
handle_event(
931+
:app_folder_updated,
932+
build_app_folder(state, app_folder_updated.app_folder),
933+
state
934+
)
935+
end
936+
937+
defp handle_event(:app_folder_deleted, %Teams.AppFolder{} = app_folder, state) do
938+
Teams.Broadcasts.app_folder_deleted(app_folder)
939+
remove_app_folder(state, app_folder)
940+
end
941+
942+
defp handle_event(:app_folder_deleted, %{id: id}, state) do
943+
with {:ok, app_folder} <- fetch_app_folder(id, state) do
944+
handle_event(:app_folder_deleted, app_folder, state)
945+
end
946+
end
947+
876948
defp dispatch_secrets(state, %{secrets: secrets}) do
877949
decrypted_secrets = Enum.map(secrets, &build_secret(state, &1))
878950

@@ -936,6 +1008,19 @@ defmodule Livebook.Hubs.TeamClient do
9361008
dispatch_events(state, agent_joined: joined, agent_left: left)
9371009
end
9381010

1011+
defp dispatch_app_folders(state, %{app_folders: app_folders}) do
1012+
app_folders = Enum.map(app_folders, &build_app_folder(state, &1))
1013+
1014+
{created, deleted, updated} =
1015+
diff(state.app_folders, app_folders, &(&1.id == &2.id))
1016+
1017+
dispatch_events(state,
1018+
app_folder_deleted: deleted,
1019+
app_folder_created: created,
1020+
app_folder_updated: updated
1021+
)
1022+
end
1023+
9391024
defp dispatch_connection(%{hub: %{id: id}} = state) do
9401025
Teams.Broadcasts.client_connected(id)
9411026
state
@@ -1064,6 +1149,8 @@ defmodule Livebook.Hubs.TeamClient do
10641149
defp fetch_app_deployment_from_slug(slug, state),
10651150
do: fetch_entry(state.app_deployments, &(&1.slug == slug), state)
10661151

1152+
defp fetch_app_folder(id, state), do: fetch_entry(state.app_folders, &(&1.id == id), state)
1153+
10671154
defp fetch_entry(entries, fun, state) do
10681155
if entry = Enum.find(entries, fun) do
10691156
{:ok, entry}

lib/livebook/live_markdown/export.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ defmodule Livebook.LiveMarkdown.Export do
113113
:auto_shutdown_ms,
114114
:access_type,
115115
:show_source,
116-
:output_type
116+
:output_type,
117+
:app_folder_id
117118
]
118119

119120
put_unless_default(

lib/livebook/live_markdown/import.ex

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@ defmodule Livebook.LiveMarkdown.Import do
495495
{"show_source", show_source}, attrs ->
496496
Map.put(attrs, :show_source, show_source)
497497

498+
{"app_folder_id", app_folder_id}, attrs ->
499+
Map.put(attrs, :app_folder_id, app_folder_id)
500+
498501
{"output_type", output_type}, attrs when output_type in ["all", "rich"] ->
499502
Map.put(attrs, :output_type, String.to_atom(output_type))
500503

@@ -664,7 +667,25 @@ defmodule Livebook.LiveMarkdown.Import do
664667
# validate it against the public key).
665668
teams_enabled = is_struct(hub, Livebook.Hubs.Team) and (hub.offline == nil or stamp_verified?)
666669

667-
{%{notebook | teams_enabled: teams_enabled}, stamp_verified?, messages}
670+
{app_settings, messages} =
671+
if app_folder_id = notebook.app_settings.app_folder_id do
672+
app_folders = Hubs.Provider.get_app_folders(hub)
673+
674+
if Enum.any?(app_folders, &(&1.id == app_folder_id)) do
675+
{notebook.app_settings, messages}
676+
else
677+
{Map.replace!(notebook.app_settings, :app_folder_id, nil),
678+
messages ++
679+
[
680+
"notebook is assigned to a non-existent app folder, defaulting to ungrouped app folder"
681+
]}
682+
end
683+
else
684+
{notebook.app_settings, messages}
685+
end
686+
687+
{%{notebook | app_settings: app_settings, teams_enabled: teams_enabled}, stamp_verified?,
688+
messages}
668689
end
669690

670691
defp safe_binary_split(binary, offset)

lib/livebook/notebook/app_settings.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ defmodule Livebook.Notebook.AppSettings do
1414
access_type: access_type(),
1515
password: String.t() | nil,
1616
show_source: boolean(),
17-
output_type: output_type()
17+
output_type: output_type(),
18+
app_folder_id: String.t() | nil
1819
}
1920

2021
@type access_type :: :public | :protected
@@ -33,6 +34,7 @@ defmodule Livebook.Notebook.AppSettings do
3334
field :password, :string
3435
field :show_source, :boolean
3536
field :output_type, Ecto.Enum, values: [:all, :rich]
37+
field :app_folder_id, :string
3638
end
3739

3840
@doc """
@@ -49,7 +51,8 @@ defmodule Livebook.Notebook.AppSettings do
4951
access_type: :protected,
5052
password: generate_password(),
5153
show_source: false,
52-
output_type: :all
54+
output_type: :all,
55+
app_folder_id: nil
5356
}
5457
end
5558

@@ -82,7 +85,8 @@ defmodule Livebook.Notebook.AppSettings do
8285
:auto_shutdown_ms,
8386
:access_type,
8487
:show_source,
85-
:output_type
88+
:output_type,
89+
:app_folder_id
8690
])
8791
|> validate_required([
8892
:slug,

lib/livebook/session.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,7 @@ defmodule Livebook.Session do
904904
def init({caller_pid, opts}) do
905905
Livebook.Settings.subscribe()
906906
Livebook.Hubs.Broadcasts.subscribe([:crud, :secrets, :file_systems])
907+
Livebook.Teams.Broadcasts.subscribe(:app_folders)
907908

908909
id = Keyword.fetch!(opts, :id)
909910

@@ -2028,6 +2029,13 @@ defmodule Livebook.Session do
20282029
{:noreply, handle_operation(state, operation)}
20292030
end
20302031

2032+
def handle_info({event, app_folder}, state)
2033+
when event in [:app_folder_created, :app_folder_updated, :app_folder_deleted] and
2034+
app_folder.hub_id == state.data.notebook.hub_id do
2035+
operation = {:sync_hub_app_folders, @client_id}
2036+
{:noreply, handle_operation(state, operation)}
2037+
end
2038+
20312039
def handle_info({:hub_deleted, id}, %{data: %{notebook: %{hub_id: id}}} = state) do
20322040
# Since the hub got deleted, we close all sessions using that hub.
20332041
# This way we clean up all secrets and other in-memory state that

lib/livebook/session/data.ex

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ defmodule Livebook.Session.Data do
3737
:secrets,
3838
:hub_secrets,
3939
:hub_file_systems,
40+
:hub_app_folders,
4041
:mode,
4142
:deployed_app_slug,
4243
:app_data
@@ -247,6 +248,7 @@ defmodule Livebook.Session.Data do
247248
| {:set_notebook_hub, client_id(), String.t()}
248249
| {:sync_hub_secrets, client_id()}
249250
| {:sync_hub_file_systems, client_id()}
251+
| {:sync_hub_app_folders, client_id()}
250252
| {:add_file_entries, client_id(), list(Notebook.file_entry())}
251253
| {:rename_file_entry, client_id(), name :: String.t(), new_name :: String.t()}
252254
| {:delete_file_entry, client_id(), String.t()}
@@ -305,6 +307,7 @@ defmodule Livebook.Session.Data do
305307
hub = Livebook.Hubs.fetch_hub!(notebook.hub_id)
306308
hub_secrets = Livebook.Hubs.get_secrets(hub)
307309
hub_file_systems = Livebook.Hubs.get_file_systems(hub)
310+
hub_app_folders = Livebook.Hubs.Provider.get_app_folders(hub)
308311

309312
startup_secrets =
310313
for secret <- Livebook.Secrets.get_startup_secrets(),
@@ -338,6 +341,7 @@ defmodule Livebook.Session.Data do
338341
secrets: secrets,
339342
hub_secrets: hub_secrets,
340343
hub_file_systems: hub_file_systems,
344+
hub_app_folders: hub_app_folders,
341345
mode: opts[:mode],
342346
deployed_app_slug: nil,
343347
app_data: app_data
@@ -1074,6 +1078,14 @@ defmodule Livebook.Session.Data do
10741078
|> wrap_ok()
10751079
end
10761080

1081+
def apply_operation(data, {:sync_hub_app_folders, _client_id}) do
1082+
data
1083+
|> with_actions()
1084+
|> sync_hub_app_folders()
1085+
|> set_dirty()
1086+
|> wrap_ok()
1087+
end
1088+
10771089
def apply_operation(data, {:add_file_entries, _client_id, file_entries}) do
10781090
data
10791091
|> with_actions()
@@ -1965,7 +1977,8 @@ defmodule Livebook.Session.Data do
19651977
teams_enabled: is_struct(hub, Livebook.Hubs.Team)
19661978
},
19671979
hub_secrets: Livebook.Hubs.get_secrets(hub),
1968-
hub_file_systems: Livebook.Hubs.get_file_systems(hub)
1980+
hub_file_systems: Livebook.Hubs.get_file_systems(hub),
1981+
hub_app_folders: Livebook.Hubs.Provider.get_app_folders(hub)
19691982
)
19701983
end
19711984

@@ -1985,6 +1998,12 @@ defmodule Livebook.Session.Data do
19851998
set!(data_actions, hub_file_systems: file_systems)
19861999
end
19872000

2001+
defp sync_hub_app_folders({data, _} = data_actions) do
2002+
hub = Livebook.Hubs.fetch_hub!(data.notebook.hub_id)
2003+
app_folders = Livebook.Hubs.Provider.get_app_folders(hub)
2004+
set!(data_actions, hub_app_folders: app_folders)
2005+
end
2006+
19882007
defp update_notebook_hub_secret_names({data, _} = data_actions) do
19892008
hub_secret_names =
19902009
for {_name, secret} <- data.secrets, secret.hub_id == data.notebook.hub_id, do: secret.name

lib/livebook/teams.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,12 @@ defmodule Livebook.Teams do
305305
def user_can_deploy?(%Team{} = team, %Teams.DeploymentGroup{} = deployment_group) do
306306
TeamClient.user_can_deploy?(team.id, team.user_id, deployment_group.id)
307307
end
308+
309+
@doc """
310+
Gets a list of app folders for a given Hub.
311+
"""
312+
@spec get_app_folders(Team.t()) :: list(Teams.AppFolder.t())
313+
def get_app_folders(team) do
314+
Hubs.Provider.get_app_folders(team)
315+
end
308316
end

0 commit comments

Comments
 (0)