Packages
livebook
0.2.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/home_live.ex
defmodule LivebookWeb.HomeLive do
use LivebookWeb, :live_view
import LivebookWeb.UserHelpers
import LivebookWeb.SessionHelpers
alias Livebook.{SessionSupervisor, Session, LiveMarkdown, Notebook}
@impl true
def mount(_params, %{"current_user_id" => current_user_id} = session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions")
Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}")
end
current_user = build_current_user(session, socket)
session_summaries = sort_session_summaries(SessionSupervisor.get_session_summaries())
notebook_infos = Notebook.Explore.notebook_infos() |> Enum.take(3)
{:ok,
assign(socket,
current_user: current_user,
path: default_path(),
session_summaries: session_summaries,
notebook_infos: notebook_infos
)}
end
@impl true
def render(assigns) do
~L"""
<div class="flex flex-grow h-full">
<%= live_component LivebookWeb.SidebarComponent,
id: :sidebar,
items: [
%{type: :break},
%{type: :user, current_user: @current_user, path: Routes.home_path(@socket, :user)}
] %>
<div class="flex-grow px-6 py-8 overflow-y-auto">
<div class="max-w-screen-lg w-full mx-auto px-4 pb-8 space-y-4">
<div class="flex flex-col space-y-2 items-center sm:flex-row sm:space-y-0 sm:justify-between sm:pb-4 pb-8 border-b border-gray-200">
<div class="text-2xl text-gray-800 font-semibold">
<img src="/images/logo-with-text.png" class="h-[50px]" alt="Livebook" />
</div>
<div class="flex space-x-2 pt-2">
<%= live_patch to: Routes.home_path(@socket, :import, "url"),
class: "button button-outlined-gray whitespace-nowrap" do %>
Import
<% end %>
<button class="button button-blue"
phx-click="new">
New notebook
</button>
</div>
</div>
<div class="h-80">
<%= live_component LivebookWeb.PathSelectComponent,
id: "path_select",
path: @path,
extnames: [LiveMarkdown.extension()],
running_paths: paths(@session_summaries),
phx_target: nil,
phx_submit: nil do %>
<div class="flex justify-end space-x-2">
<%= content_tag :button,
class: "button button-outlined-gray whitespace-nowrap",
phx_click: "fork",
disabled: not path_forkable?(@path) do %>
<%= remix_icon("git-branch-line", class: "align-middle mr-1") %>
<span>Fork</span>
<% end %>
<%= if path_running?(@path, @session_summaries) do %>
<%= live_redirect "Join session", to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)),
class: "button button-blue" %>
<% else %>
<%= tag :span, if(File.regular?(@path) and not file_writable?(@path),
do: [class: "tooltip top", aria_label: "This file is write-protected, please fork instead"],
else: []
) %>
<%= content_tag :button, "Open",
class: "button button-blue",
phx_click: "open",
disabled: not path_openable?(@path, @session_summaries) %>
</span>
<% end %>
</div>
<% end %>
</div>
<div class="py-12">
<div class="mb-4 flex justify-between items-center">
<h2 class="text-xl font-semibold text-gray-800">
Explore
</h2>
<%= live_redirect to: Routes.explore_path(@socket, :page),
class: "flex items-center text-blue-600" do %>
<span class="font-semibold">See all</span>
<%= remix_icon("arrow-right-line", class: "align-middle ml-1") %>
<% end %>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<%= for {info, idx} <- Enum.with_index(@notebook_infos) do %>
<%= live_component LivebookWeb.NotebookCardComponent,
id: "notebook-card-#{idx}",
notebook_info: info %>
<% end %>
</div>
</div>
<div class="py-12">
<h2 class="mb-4 text-xl font-semibold text-gray-800">
Running sessions
</h2>
<%= if @session_summaries == [] do %>
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
<div>
<%= remix_icon("windy-line", class: "text-gray-400 text-xl") %>
</div>
<div class="text-gray-600">
You do not have any running sessions.
<br>
Please create a new one by clicking <span class="font-semibold">“New notebook”</span>
</div>
</div>
<% else %>
<%= live_component LivebookWeb.HomeLive.SessionsComponent,
id: "sessions_list",
session_summaries: @session_summaries %>
<% end %>
</div>
</div>
</div>
</div>
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: :user_modal,
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.home_path(@socket, :page) %>
<% end %>
<%= if @live_action == :close_session do %>
<%= live_modal LivebookWeb.HomeLive.CloseSessionComponent,
id: :close_session_modal,
modal_class: "w-full max-w-xl",
return_to: Routes.home_path(@socket, :page),
session_summary: @session_summary %>
<% end %>
<%= if @live_action == :import do %>
<%= live_modal LivebookWeb.HomeLive.ImportComponent,
id: :import_modal,
modal_class: "w-full max-w-xl",
return_to: Routes.home_path(@socket, :page),
tab: @tab %>
<% end %>
"""
end
@impl true
def handle_params(%{"session_id" => session_id}, _url, socket) do
session_summary = Enum.find(socket.assigns.session_summaries, &(&1.session_id == session_id))
{:noreply, assign(socket, session_summary: session_summary)}
end
def handle_params(%{"tab" => tab}, _url, socket) do
{:noreply, assign(socket, tab: tab)}
end
def handle_params(_params, _url, socket), do: {:noreply, socket}
@impl true
def handle_event("set_path", %{"path" => path}, socket) do
{:noreply, assign(socket, path: path)}
end
def handle_event("new", %{}, socket) do
{:noreply, create_session(socket)}
end
def handle_event("fork", %{}, socket) do
{notebook, messages} = import_notebook(socket.assigns.path)
socket = put_import_flash_messages(socket, messages)
notebook = Notebook.forked(notebook)
images_dir = Session.images_dir_for_notebook(socket.assigns.path)
{:noreply, create_session(socket, notebook: notebook, copy_images_from: images_dir)}
end
def handle_event("open", %{}, socket) do
{notebook, messages} = import_notebook(socket.assigns.path)
socket = put_import_flash_messages(socket, messages)
{:noreply, create_session(socket, notebook: notebook, path: socket.assigns.path)}
end
def handle_event("fork_session", %{"id" => session_id}, socket) do
data = Session.get_data(session_id)
notebook = Notebook.forked(data.notebook)
%{images_dir: images_dir} = Session.get_summary(session_id)
{:noreply, create_session(socket, notebook: notebook, copy_images_from: images_dir)}
end
@impl true
def handle_info({:session_created, id}, socket) do
summary = Session.get_summary(id)
session_summaries = sort_session_summaries([summary | socket.assigns.session_summaries])
{:noreply, assign(socket, session_summaries: session_summaries)}
end
def handle_info({:session_closed, id}, socket) do
session_summaries = Enum.reject(socket.assigns.session_summaries, &(&1.session_id == id))
{:noreply, assign(socket, session_summaries: session_summaries)}
end
def handle_info({:import_content, content}, socket) do
{notebook, messages} = Livebook.LiveMarkdown.Import.notebook_from_markdown(content)
socket = put_import_flash_messages(socket, messages)
{:noreply, create_session(socket, notebook: notebook)}
end
def handle_info(
{:user_change, %{id: id} = user},
%{assigns: %{current_user: %{id: id}}} = socket
) do
{:noreply, assign(socket, :current_user, user)}
end
def handle_info(_message, socket), do: {:noreply, socket}
defp default_path(), do: Livebook.Config.root_path() <> "/"
defp sort_session_summaries(session_summaries) do
Enum.sort_by(session_summaries, & &1.notebook_name)
end
defp paths(session_summaries) do
Enum.map(session_summaries, & &1.path)
end
defp path_forkable?(path) do
File.regular?(path)
end
defp path_openable?(path, session_summaries) do
File.regular?(path) and not path_running?(path, session_summaries) and file_writable?(path)
end
defp path_running?(path, session_summaries) do
running_paths = paths(session_summaries)
path in running_paths
end
defp file_writable?(path) do
case File.stat(path) do
{:ok, stat} -> stat.access in [:read_write, :write]
{:error, _} -> false
end
end
defp import_notebook(path) do
content = File.read!(path)
LiveMarkdown.Import.notebook_from_markdown(content)
end
defp put_import_flash_messages(socket, []), do: socket
defp put_import_flash_messages(socket, messages) do
list =
messages
|> Enum.map(fn message -> ["- ", message] end)
|> Enum.intersperse("\n")
flash =
IO.iodata_to_binary([
"We found problems while importing the file and tried to autofix them:\n" | list
])
put_flash(socket, :info, flash)
end
defp session_id_by_path(path, session_summaries) do
summary = Enum.find(session_summaries, &(&1.path == path))
summary.session_id
end
end