Packages
livebook
0.19.2
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/apps_dashboard_live.ex
defmodule LivebookWeb.AppsDashboardLive do
use LivebookWeb, :live_view
import LivebookWeb.AppComponents
alias LivebookWeb.LayoutComponents
on_mount LivebookWeb.SidebarHook
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Livebook.Apps.subscribe()
end
apps = Livebook.Apps.list_apps()
{:ok, assign(socket, apps: apps, page_title: "Apps - Livebook")}
end
@impl true
def render(assigns) do
~H"""
<LayoutComponents.layout
current_page={~p"/apps-dashboard"}
current_user={@current_user}
teams_auth={@teams_auth}
saved_hubs={@saved_hubs}
>
<div class="space-y-2 p-4 md:px-12 md:py-7 max-w-(--breakpoint-lg) mx-auto">
<div class="flex items-center justify-between">
<LayoutComponents.title text="Local apps" />
<.link navigate={~p"/apps"} class="flex items-center text-blue-600">
<span class="font-semibold">Listing</span>
<.remix_icon icon="arrow-right-line" class="align-middle ml-1" />
</.link>
</div>
<p class="text-gray-700 text-sm">
An overview of all deployed applications and previews running on this instance.
</p>
<div class="pt-6">
<.app_list apps={@apps} />
</div>
</div>
</LayoutComponents.layout>
"""
end
defp app_list(%{apps: []} = assigns) do
~H"""
<.no_entries>
You do not have any apps running. <br />
You can preview and deploy new apps by opening a notebook and clicking
<.remix_icon icon="rocket-line" class="align-top text-lg" /> in the sidebar.
</.no_entries>
"""
end
defp app_list(assigns) do
~H"""
<div class="flex flex-col space-y-4">
<div :for={app <- Enum.sort_by(@apps, & &1.slug)} data-app-slug={app.slug}>
<a
phx-click={JS.toggle(to: "[data-app-slug=#{app.slug}] [data-toggle]")}
class="flex items-center justify-between mb-2 hover:cursor-pointer"
>
<span class="text-gray-800 font-medium text-xl break-all">
{"/" <> app.slug}
</span>
<div class="flex items-center gap-2">
<.app_group_tag app_spec={app.app_spec} />
<.remix_icon icon="arrow-down-s-line" class="text-xl text-gray-700" data-toggle />
<.remix_icon icon="arrow-right-s-line" class="text-xl text-gray-700 hidden" data-toggle />
</div>
</a>
<div data-toggle>
<div :if={app.warnings != []} class="my-3 flex flex-col gap-3">
<.message_box :for={warning <- app.warnings} kind="warning" message={warning} />
</div>
<div class="flex-col mb-8">
<div class="p-4 border-x border-t border-gray-200 rounded-t-lg ">
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-[minmax(0,2fr)_minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)] gap-4">
<div class="wrap-break-word">
<.labeled_text label="Name">
{app.notebook_name}
<span
:if={not app.public?}
class="inline! tooltip top"
data-tooltip="Password protected"
>
<.remix_icon icon="lock-line" />
</span>
</.labeled_text>
</div>
<div class="break-all">
<.labeled_text label="URL">
<a href={~p"/apps/#{app.slug}"}>
{~p"/apps/#{app.slug}"}
</a>
</.labeled_text>
</div>
<div>
<.labeled_text label="Latest version" one_line>
v{app.version}
</.labeled_text>
</div>
<div>
<.labeled_text label="Session type" one_line>
{if(app.multi_session, do: "Multi", else: "Single")}
</.labeled_text>
</div>
<div class="flex flex-col md:flex-row md:items-center justify-start lg:justify-end">
<%= if app.permanent do %>
<span class="tooltip top" data-tooltip="Permanent apps cannot be terminated">
<.icon_button disabled>
<.remix_icon icon="delete-bin-6-line" />
</.icon_button>
</span>
<% else %>
<span class="tooltip top" data-tooltip="Terminate">
<.icon_button
aria-label="terminate app"
phx-click={JS.push("terminate_app", value: %{slug: app.slug})}
>
<.remix_icon icon="delete-bin-6-line" />
</.icon_button>
</span>
<% end %>
</div>
</div>
</div>
<div class="border border-gray-200 rounded-b-lg overflow-auto tiny-scrollbar whitespace-none">
<%= if Enum.any?(app.sessions) do %>
<div class="uppercase text-gray-500 text-sm font-medium leading-normal tracking-wider px-4 pt-4 pb-3">
Running sessions
</div>
<.grid rows={app.sessions}>
<:col :let={app_session} label="Status">
<a
aria-label="debug app"
href={app_session.app_status == :error && ~p"/sessions/#{app_session.id}"}
target="_blank"
>
<.app_status status={app_session.app_status} />
</a>
</:col>
<:col :let={app_session} label="Uptime">
{LivebookWeb.HTMLHelpers.format_datetime_relatively(app_session.created_at)}
</:col>
<:col :let={app_session} label="Version">
v{app_session.version}
</:col>
<:col :let={app_session} label="Clients">
{app_session.client_count}
</:col>
<:actions :let={app_session}>
<span class="tooltip left" data-tooltip="Open">
<.icon_button
disabled={app_session.app_status.lifecycle != :active}
aria-label="open app"
href={~p"/apps/#{app.slug}/sessions/#{app_session.id}"}
>
<.remix_icon icon="link" />
</.icon_button>
</span>
<span class="tooltip left" data-tooltip="Debug">
<.icon_button aria-label="debug app" href={~p"/sessions/#{app_session.id}"}>
<.remix_icon icon="terminal-line" />
</.icon_button>
</span>
<%= if app_session.app_status.lifecycle == :active do %>
<span class="tooltip left" data-tooltip="Deactivate">
<.icon_button
aria-label="deactivate app session"
phx-click={
JS.push("deactivate_app_session",
value: %{slug: app.slug, session_id: app_session.id}
)
}
>
<.remix_icon icon="stop-circle-line" />
</.icon_button>
</span>
<% else %>
<span class="tooltip left" data-tooltip="Terminate">
<.icon_button
aria-label="terminate app session"
phx-click={
JS.push("terminate_app_session",
value: %{slug: app.slug, session_id: app_session.id}
)
}
>
<.remix_icon icon="delete-bin-6-line" />
</.icon_button>
</span>
<% end %>
</:actions>
</.grid>
<% else %>
<div class="p-4 uppercase text-gray-500 text-sm font-medium leading-normal tracking-wider">
No running sessions
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
"""
end
defp app_group_tag(%{app_spec: %Livebook.Apps.PreviewAppSpec{}} = assigns) do
~H"""
<span
class="tooltip top"
data-tooltip={
~S'''
This is an app preview, it has
been started manually
'''
}
>
<span class="font-medium bg-gray-100 text-gray-800 text-xs px-2.5 py-0.5 rounded cursor-default">
Preview
</span>
</span>
"""
end
defp app_group_tag(%{app_spec: %Livebook.Apps.PathAppSpec{}} = assigns) do
~H"""
<span
class="tooltip top"
data-tooltip={
~S'''
This is a permanent app started
from LIVEBOOK_APPS_PATH
'''
}
>
<span class="font-medium bg-blue-100 text-blue-800 text-xs px-2.5 py-0.5 rounded cursor-default">
Apps directory
</span>
</span>
"""
end
defp app_group_tag(%{app_spec: %Livebook.Apps.TeamsAppSpec{}} = assigns) do
~H"""
<span
class="tooltip top"
data-tooltip={
~S'''
This is a permanent app started
deployed via Livebook Teams
'''
}
>
<span class="font-medium bg-green-100 text-green-800 text-xs px-2.5 py-0.5 rounded cursor-default">
Livebook Teams
</span>
</span>
"""
end
defp app_group_tag(assigns), do: ~H""
defp grid(assigns) do
~H"""
<div class="min-w-[650px]">
<div class="px-2 pb-2">
<div class="grid grid-cols-[minmax(0,0.5fr)_minmax(0,0.75fr)_minmax(0,0.5fr)_minmax(0,0.5fr)_minmax(0,0.5fr)] md:grid-cols-[minmax(0,2fr)_minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)] gap-4 px-2">
<div
:for={col <- @col}
class={[
"text-gray-500 text-sm font-normal flex items-center",
align_to_class(col[:align])
]}
>
{col[:label]}
</div>
</div>
<div :for={row <- @rows} class="whitespace-nowrap px-2 hover:bg-gray-50 hover:rounded-md">
<div class="grid grid-cols-[minmax(0,0.5fr)_minmax(0,0.75fr)_minmax(0,0.5fr)_minmax(0,0.5fr)_minmax(0,0.5fr)] md:grid-cols-[minmax(0,2fr)_minmax(0,2fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)] gap-4">
<div
:for={col <- @col}
class={[
"py-2 text-gray-800 text-sm font-semibold flex items-center",
align_to_class(col[:align])
]}
>
{render_slot(col, row)}
</div>
<div class="py-2 flex flex-row items-center justify-end gap-2">
{render_slot(@actions, row)}
</div>
</div>
</div>
</div>
</div>
"""
end
defp align_to_class(:right), do: "text-right"
defp align_to_class(:center), do: "text-center"
defp align_to_class(_), do: "text-left"
@impl true
def handle_info({type, _app} = event, socket)
when type in [:app_created, :app_updated, :app_closed] do
{:noreply, update(socket, :apps, &update_app_list(&1, event))}
end
def handle_info(_message, socket), do: {:noreply, socket}
@impl true
def handle_event("terminate_app", %{"slug" => slug}, socket) do
app = Enum.find(socket.assigns.apps, &(&1.slug == slug))
{:noreply, confirm_app_termination(socket, app.pid)}
end
def handle_event("terminate_app_session", %{"slug" => slug, "session_id" => session_id}, socket) do
app_session = find_app_session(socket.assigns.apps, slug, session_id)
Livebook.Session.close(app_session.pid)
{:noreply, socket}
end
def handle_event(
"deactivate_app_session",
%{"slug" => slug, "session_id" => session_id},
socket
) do
app_session = find_app_session(socket.assigns.apps, slug, session_id)
Livebook.Session.app_deactivate(app_session.pid)
{:noreply, socket}
end
defp find_app_session(apps, slug, session_id) do
app = Enum.find(apps, &(&1.slug == slug))
Enum.find(app.sessions, &(&1.id == session_id))
end
end