mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-09 14:34:42 +08:00
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:
parent
5b5a70d928
commit
6575791bed
6 changed files with 135 additions and 2 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
69
lib/livebook_web/live/session_live/export_component.ex
Normal file
69
lib/livebook_web/live/session_live/export_component.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue