Add notebook source preview and export (#457)

* Add notebook source preview and export

* Build live markdown source outside the session process
This commit is contained in:
Jonatan Kłosko 2021-07-23 01:18:40 +02:00 committed by GitHub
parent 5b5a70d928
commit 6575791bed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 135 additions and 2 deletions

View file

@ -142,6 +142,14 @@ defmodule Livebook.Session do
GenServer.call(name(session_id), :get_summary)
end
@doc """
Returns the current notebook structure.
"""
@spec get_notebook(id()) :: Notebook.t()
def get_notebook(session_id) do
GenServer.call(name(session_id), :get_notebook)
end
@doc """
Asynchronously sends section insertion request to the server.
"""
@ -410,6 +418,10 @@ defmodule Livebook.Session do
{:reply, summary_from_state(state), state}
end
def handle_call(:get_notebook, _from, state) do
{:reply, state.data.notebook, state}
end
def handle_call(:save, _from, state) do
{:reply, :ok, maybe_save_notebook(state)}
end

View file

@ -10,8 +10,21 @@ defmodule LivebookWeb.SessionController do
true <- File.exists?(path) do
serve_static(conn, path)
else
_ ->
send_resp(conn, 404, "Not found")
_ -> send_resp(conn, 404, "Not found")
end
end
def download_source(conn, %{"id" => id}) do
if SessionSupervisor.session_exists?(id) do
notebook = Session.get_notebook(id)
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook)
send_download(conn, {:binary, source},
filename: "notebook.livemd",
content_type: "text/plain"
)
else
send_resp(conn, 404, "Not found")
end
end

View file

@ -201,6 +201,11 @@ defmodule LivebookWeb.SessionLive do
<.remix_icon icon="dashboard-2-line" />
<span class="font-medium">See on Dashboard</span>
</a>
<%= live_patch to: Routes.session_path(@socket, :export, @session_id),
class: "menu__item text-gray-500" do %>
<.remix_icon icon="download-2-line" />
<span class="font-medium">Export</span>
<% end %>
<%= live_patch to: Routes.home_path(@socket, :close_session, @session_id),
class: "menu__item text-red-600" do %>
<.remix_icon icon="close-circle-line" />
@ -311,6 +316,14 @@ defmodule LivebookWeb.SessionLive do
bin_entries: @data_view.bin_entries,
return_to: Routes.session_path(@socket, :page, @session_id) %>
<% end %>
<%= if @live_action == :export do %>
<%= live_modal LivebookWeb.SessionLive.ExportComponent,
id: "export",
modal_class: "w-full max-w-4xl",
session_id: @session_id,
return_to: Routes.session_path(@socket, :page, @session_id) %>
<% end %>
"""
end

View file

@ -0,0 +1,69 @@
defmodule LivebookWeb.SessionLive.ExportComponent do
use LivebookWeb, :live_component
alias Livebook.Session
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
socket =
if socket.assigns[:source] do
socket
else
# Note: we need to load the notebook, because the local data
# has cell contents stripped out
notebook = Session.get_notebook(socket.assigns.session_id)
source = Livebook.LiveMarkdown.Export.notebook_to_markdown(notebook)
assign(socket, :source, source)
end
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div class="p-6 max-w-4xl flex flex-col space-y-3">
<h3 class="text-2xl font-semibold text-gray-800">
Export
</h3>
<div class="w-full flex-col space-y-5">
<p class="text-gray-700">
Here you can preview and directly export the notebook source.
</p>
<div class="flex flex-col space-y-1">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-700 font-semibold">
.livemd
</span>
<div class="flex justify-end space-x-2">
<span class="tooltip left" aria-label="Copy source">
<button class="icon-button"
id="export-notebook-source-clipcopy"
phx-hook="ClipCopy"
data-target-id="export-notebook-source">
<.remix_icon icon="clipboard-line" class="text-lg" />
</button>
</span>
<span class="tooltip left" aria-label="Download source">
<a class="icon-button"
href={Routes.session_path(@socket, :download_source, @session_id)}>
<.remix_icon icon="download-2-line" class="text-lg" />
</a>
</span>
</div>
</div>
<div class="markdown">
<pre><code
class="tiny-scrollbar"
id="export-notebook-source"
phx-hook="Highlight"
data-language="markdown"><%= @source %></code></pre>
</div>
</div>
</div>
</div>
"""
end
end

View file

@ -34,6 +34,8 @@ defmodule LivebookWeb.Router do
live "/sessions/:id/settings/runtime", SessionLive, :runtime_settings
live "/sessions/:id/settings/file", SessionLive, :file_settings
live "/sessions/:id/bin", SessionLive, :bin
live "/sessions/:id/export", SessionLive, :export
get "/sessions/:id/export/download", SessionController, :download_source
live "/sessions/:id/cell-settings/:cell_id", SessionLive, :cell_settings
live "/sessions/:id/cell-upload/:cell_id", SessionLive, :cell_upload
live "/sessions/:id/delete-section/:section_id", SessionLive, :delete_section

View file

@ -36,4 +36,28 @@ defmodule LivebookWeb.SessionControllerTest do
SessionSupervisor.close_session(session_id)
end
end
describe "download_source" do
test "returns not found when the given session does not exist", %{conn: conn} do
conn = get(conn, Routes.session_path(conn, :download_source, "nonexistent"))
assert conn.status == 404
assert conn.resp_body == "Not found"
end
test "returns live markdown notebook source", %{conn: conn} do
{:ok, session_id} = SessionSupervisor.create_session()
conn = get(conn, Routes.session_path(conn, :download_source, session_id))
assert conn.status == 200
assert get_resp_header(conn, "content-type") == ["text/plain"]
assert conn.resp_body == """
# Untitled notebook
"""
SessionSupervisor.close_session(session_id)
end
end
end