defmodule LivebookWeb.HomeLive do use LivebookWeb, :live_view import LivebookWeb.UserHelpers import LivebookWeb.SessionHelpers alias LivebookWeb.{SidebarHelpers, ExploreHelpers} alias Livebook.{Sessions, Session, LiveMarkdown, Notebook, FileSystem} @impl true def mount(_params, %{"current_user_id" => current_user_id} = session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions") Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}") end current_user = build_current_user(session, socket) sessions = sort_sessions(Sessions.list_sessions()) notebook_infos = Notebook.Explore.notebook_infos() |> Enum.take(3) {:ok, assign(socket, current_user: current_user, file: Livebook.Config.default_dir(), file_info: %{exists: true, access: :read_write}, sessions: sessions, notebook_infos: notebook_infos )} end @impl true def render(assigns) do ~H"""
Livebook
<%= live_patch "Import", to: Routes.home_path(@socket, :import, "url"), class: "button button-outlined-gray whitespace-nowrap" %>
<%= live_component LivebookWeb.FileSelectComponent, id: "home-file-select", file: @file, extnames: [LiveMarkdown.extension()], running_files: files(@sessions) do %>
<%= if file_running?(@file, @sessions) do %> <%= live_redirect "Join session", to: Routes.session_path(@socket, :page, session_id_by_file(@file, @sessions)), class: "button button-blue" %> <% else %> <% end %>
<% end %>

Explore

<%= live_redirect to: Routes.explore_path(@socket, :page), class: "flex items-center text-blue-600" do %> See all <.remix_icon icon="arrow-right-line" class="align-middle ml-1" /> <% end %>
<%# Note: it's fine to use stateless components in this comprehension, because @notebook_infos never change %> <%= for info <- @notebook_infos do %> <% end %>

Running sessions

<.sessions_list sessions={@sessions} socket={@socket} />
<%= if @live_action == :user do %> <%= live_modal LivebookWeb.UserComponent, id: "user", 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_class: "w-full max-w-xl", return_to: Routes.home_path(@socket, :page), session: @session %> <% end %> <%= if @live_action == :import do %> <%= live_modal LivebookWeb.HomeLive.ImportComponent, id: "import", modal_class: "w-full max-w-xl", return_to: Routes.home_path(@socket, :page), tab: @tab, import_opts: @import_opts %> <% end %> """ end defp open_button_tooltip_attrs(file, file_info) do if regular?(file, file_info) and not writable?(file_info) do [class: "tooltip top", aria_label: "This file is write-protected, please fork instead"] else [] end end defp sessions_list(%{sessions: []} = assigns) do ~H"""
<.remix_icon icon="windy-line" class="text-gray-400 text-xl" />
You do not have any running sessions.
Please create a new one by clicking “New notebook”
""" end defp sessions_list(assigns) do ~H"""
<%= for session <- @sessions do %>
<%= live_redirect session.notebook_name, to: Routes.session_path(@socket, :page, session.id), class: "font-semibold text-gray-800 hover:text-gray-900" %>
<%= if session.file, do: session.file.path, else: "No file" %>
Created <%= format_creation_date(session.created_at) %>
<% end %>
""" end @impl true def handle_params(%{"session_id" => session_id}, _url, socket) do session = Enum.find(socket.assigns.sessions, &(&1.id == session_id)) {:noreply, assign(socket, session: session)} end def handle_params(%{"tab" => tab} = params, _url, %{assigns: %{live_action: :import}} = socket) do import_opts = [url: params["url"]] {:noreply, assign(socket, tab: tab, import_opts: import_opts)} end def handle_params(%{"url" => url}, _url, %{assigns: %{live_action: :public_import}} = socket) do url |> Livebook.ContentLoader.rewrite_url() |> Livebook.ContentLoader.fetch_content() |> case do {:ok, content} -> socket = import_content(socket, content, origin: {:url, url}) {:noreply, socket} {:error, _message} -> {:noreply, push_patch(socket, to: Routes.home_path(socket, :import, "url", url: url))} end end def handle_params(_params, _url, socket), do: {:noreply, socket} @impl true def handle_event("new", %{}, socket) do {:noreply, create_session(socket)} end def handle_event("fork", %{}, socket) do file = socket.assigns.file socket = case import_notebook(file) do {:ok, {notebook, messages}} -> notebook = Notebook.forked(notebook) images_dir = Session.images_dir_for_notebook(file) socket |> put_import_flash_messages(messages) |> create_session( notebook: notebook, copy_images_from: images_dir, origin: {:file, file} ) {:error, error} -> put_flash(socket, :error, Livebook.Utils.upcase_first(error)) end {:noreply, socket} end def handle_event("open", %{}, socket) do file = socket.assigns.file socket = case import_notebook(file) do {:ok, {notebook, messages}} -> socket |> put_import_flash_messages(messages) |> create_session(notebook: notebook, file: file, origin: {:file, file}) {:error, error} -> put_flash(socket, :error, Livebook.Utils.upcase_first(error)) end {:noreply, socket} end def handle_event("fork_session", %{"id" => session_id}, socket) do session = Enum.find(socket.assigns.sessions, &(&1.id == session_id)) %{images_dir: images_dir} = session data = Session.get_data(session.pid) notebook = Notebook.forked(data.notebook) origin = if data.file do {:file, data.file} else data.origin end {:noreply, create_session(socket, notebook: notebook, copy_images_from: images_dir, origin: origin )} end @impl true def handle_info({:set_file, file, info}, socket) do file_info = %{ exists: info.exists, access: case FileSystem.File.access(file) do {:ok, access} -> access {:error, _} -> :none end } {:noreply, assign(socket, file: file, file_info: file_info)} end def handle_info({:session_created, session}, socket) do if session in socket.assigns.sessions do {:noreply, socket} else sessions = sort_sessions([session | socket.assigns.sessions]) {:noreply, assign(socket, sessions: sessions)} end end def handle_info({:session_updated, session}, socket) do sessions = Enum.map(socket.assigns.sessions, fn other -> if other.id == session.id, do: session, else: other end) {:noreply, assign(socket, sessions: sessions)} end def handle_info({:session_closed, session}, socket) do sessions = Enum.reject(socket.assigns.sessions, &(&1.id == session.id)) {:noreply, assign(socket, sessions: sessions)} end def handle_info({:import_content, content, session_opts}, socket) do socket = import_content(socket, content, session_opts) {:noreply, socket} 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 sort_sessions(sessions) do Enum.sort_by(sessions, & &1.created_at, {:desc, DateTime}) end defp files(sessions) do Enum.map(sessions, & &1.file) end defp path_forkable?(file, file_info) do regular?(file, file_info) end defp path_openable?(file, file_info, sessions) do regular?(file, file_info) and not file_running?(file, sessions) and writable?(file_info) end defp regular?(file, file_info) do file_info.exists and not FileSystem.File.dir?(file) end defp writable?(file_info) do file_info.access in [:read_write, :write] end defp file_running?(file, sessions) do running_files = files(sessions) file in running_files end defp import_notebook(file) do with {:ok, content} <- FileSystem.File.read(file) do {:ok, LiveMarkdown.Import.notebook_from_markdown(content)} end end defp session_id_by_file(file, sessions) do session = Enum.find(sessions, &(&1.file == file)) session.id end def format_creation_date(created_at) do time_words = created_at |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words() time_words <> " ago" end defp import_content(socket, content, session_opts) do {notebook, messages} = Livebook.LiveMarkdown.Import.notebook_from_markdown(content) socket = put_import_flash_messages(socket, messages) session_opts = Keyword.merge(session_opts, notebook: notebook) create_session(socket, session_opts) end end