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, :app_folders]) Livebook.Apps.subscribe() end empty_apps_path? = Livebook.Apps.empty_apps_path?() {:ok, 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"""
<.menu id="apps-menu" position="bottom-right" md_position="bottom-left"> <:toggle> <.menu_item :if={@logout_enabled?}>
<.link navigate={~p"/apps-dashboard"} class="inline-flex items-center gap-1.5 text-gray-500 text-xs font-medium px-3 py-1.5 rounded-md hover:bg-gray-100/80 hover:text-gray-700 transition-all duration-200" > Dashboard <.remix_icon icon="arrow-right-line" class="text-xs text-gray-400" />

Apps

<%= if @apps != [] do %>
<.remix_icon icon="search-line" class="absolute left-3 bottom-2 text-gray-400" /> <.text_field id="search-app" name="search_term" placeholder="Search by app name..." value={@search_term} phx-keyup="search" phx-debounce="300" class="w-full pl-10 text-base" autofocus />
<.select_field id="select-app-folder" name="app_folder" prompt="All folders" value={@selected_app_folder} options={@app_folder_options} />
<.remix_icon icon="windy-line" />
No apps found
Try adjusting your search or filter
<%= if @show_app_folders? do %>
<.remix_icon icon={icon} class="text-gray-500/80 text-xs leading-none" /> {app_folder}
<% end %>
<.link :for={app <- apps_listing(apps)} id={"app-#{app.slug}"} navigate={~p"/apps/#{app.slug}"} class="group block border border-gray-200 rounded-lg px-4 py-4 bg-white shadow-sm hover:shadow-lg hover:border-gray-300 hover:-translate-y-1 transition-all duration-200 ease-out cursor-pointer" >
{app.notebook_name}
<.remix_icon :if={not app.public?} icon="lock-password-line" class="text-sm text-gray-300" />
<% else %>
<.remix_icon icon="windy-line" class="text-gray-300 text-5xl mb-4" />

No apps found

Follow these steps to list your apps here:

  1. 1 Open a notebook
  2. 2
    Click <.remix_icon icon="rocket-line" class="inline align-middle text-base" /> in the sidebar and configure the app as public
  3. 3
    Save the notebook to the {Livebook.Config.apps_path()} folder
  4. 4 Relaunch your Livebook app
<.remix_icon icon="windy-line" class="text-gray-300 text-3xl mb-4" />

No apps running

<% end %>
""" 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}, socket) when type in [:app_created, :app_updated, :app_closed] do {:noreply, socket |> assign(apps: Livebook.Apps.list_authorized_apps(socket.assigns.current_user)) |> apply_filters()} end def handle_info({:server_authorization_updated, _}, socket) do {: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} 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} -> {"No folder", "ungrouped-apps", "function-line", 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(fn {name, _, _, _} -> if name == "No folder", do: {1, name}, else: {0, name} end) 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