Add source option (#1728)

This commit is contained in:
Jonatan Kłosko 2023-02-27 17:05:36 +01:00 committed by GitHub
parent b8c19475a3
commit 860a1e4bd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 173 additions and 52 deletions

View file

@ -5,6 +5,8 @@ import { theme, highContrast } from "./theme";
import { PieceTreeTextBufferBuilder } from "monaco-editor/esm/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder";
import { settingsStore } from "../../../lib/settings";
// Force LF for line ending.
//
// Monaco infers EOL based on the text content if any, otherwise uses
@ -132,6 +134,20 @@ export default monaco;
* Returns a promise resolving to HTML that renders as the highlighted code.
*/
export function highlight(code, language) {
// Currently monaco.editor.colorize doesn't support passing theme
// directly and uses the theme from last editor initialization, so
// we need to make sure there was at least one editor initialization
// with the configured theme.
//
// Tracked in https://github.com/microsoft/monaco-editor/issues/3302
if (!highlight.initialized) {
const settings = settingsStore.get();
monaco.editor.create(document.createElement("div"), {
theme: settings.editor_theme,
});
highlight.initialized = true;
}
return monaco.editor.colorize(code, language).then((result) => {
// `colorize` always adds additional newline, so we remove it
return result.replace(/<br\/>$/, "");

View file

@ -8,7 +8,8 @@ defmodule Livebook.Notebook.AppSettings do
@type t :: %__MODULE__{
slug: String.t() | nil,
access_type: access_type(),
password: String.t() | nil
password: String.t() | nil,
show_source: boolean()
}
@type access_type :: :public | :protected
@ -18,6 +19,7 @@ defmodule Livebook.Notebook.AppSettings do
field :slug, :string
field :access_type, Ecto.Enum, values: [:public, :protected]
field :password, :string
field :show_source, :boolean
end
@doc """
@ -25,7 +27,12 @@ defmodule Livebook.Notebook.AppSettings do
"""
@spec new() :: t()
def new() do
%__MODULE__{slug: nil, access_type: :protected, password: generate_password()}
%__MODULE__{
slug: nil,
access_type: :protected,
password: generate_password(),
show_source: false
}
end
defp generate_password() do
@ -51,8 +58,8 @@ defmodule Livebook.Notebook.AppSettings do
defp changeset(settings, attrs) do
settings
|> cast(attrs, [:slug, :access_type])
|> validate_required([:slug, :access_type])
|> cast(attrs, [:slug, :access_type, :show_source])
|> validate_required([:slug, :access_type, :show_source])
|> validate_format(:slug, ~r/^[a-zA-Z0-9-]+$/,
message: "slug can only contain alphanumeric characters and dashes"
)

View file

@ -55,55 +55,90 @@ defmodule LivebookWeb.AppLive do
@impl true
def render(assigns) when assigns.app_authenticated? do
~H"""
<div class="grow overflow-y-auto relative" data-el-notebook>
<div
class="w-full max-w-screen-lg px-4 sm:pl-8 sm:pr-16 md:pl-16 pt-4 sm:py-5 mx-auto"
data-el-notebook-content
>
<div data-el-js-view-iframes phx-update="ignore" id="js-view-iframes"></div>
<div class="flex items-center pb-4 mb-2 space-x-4 border-b border-gray-200">
<h1 class="text-3xl font-semibold text-gray-800">
<%= @data_view.notebook_name %>
</h1>
</div>
<div :if={@data_view.app_status == :booting} class="flex items-center space-x-2">
<span class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75">
</span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
</span>
<div class="text-gray-700 font-medium">
Booting
</div>
</div>
<div :if={@data_view.app_status == :error} class="flex items-center space-x-2">
<span class="relative flex h-3 w-3">
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>
<div class="text-gray-700 font-medium">
Error
</div>
</div>
<div
:if={@data_view.app_status in [:running, :shutting_down]}
class="pt-4 flex flex-col space-y-6"
data-el-outputs-container
id="outputs"
>
<div :for={output_view <- Enum.reverse(@data_view.output_views)}>
<LivebookWeb.Output.outputs
outputs={[output_view.output]}
dom_id_map={%{}}
session_id={@session.id}
session_pid={@session.pid}
client_id={@client_id}
input_values={output_view.input_values}
/>
</div>
</div>
<div style="height: 80vh"></div>
<div class="flex grow h-full">
<div class="px-3 py-5">
<.menu id="app-menu" position={:bottom_left}>
<:toggle>
<button class="flex items-center text-gray-900">
<img src={~p"/images/logo.png"} height="40" width="40" alt="logo livebook" />
<.remix_icon icon="arrow-down-s-line" />
</button>
</:toggle>
<.menu_item>
<.link navigate={~p"/"} role="menuitem">
<.remix_icon icon="home-6-line" />
<span>Home</span>
</.link>
</.menu_item>
<.menu_item :if={@data_view.show_source}>
<.link patch={~p"/apps/#{@data_view.slug}/source"} role="menuitem">
<.remix_icon icon="code-line" />
<span>View source</span>
</.link>
</.menu_item>
</.menu>
</div>
<div class="grow overflow-y-auto relative" data-el-notebook>
<div
class="w-full max-w-screen-lg px-4 sm:pl-8 sm:pr-16 md:pl-16 pt-4 sm:py-5 mx-auto"
data-el-notebook-content
>
<div data-el-js-view-iframes phx-update="ignore" id="js-view-iframes"></div>
<div class="flex items-center pb-4 mb-2 space-x-4 border-b border-gray-200">
<h1 class="text-3xl font-semibold text-gray-800">
<%= @data_view.notebook_name %>
</h1>
</div>
<div :if={@data_view.app_status == :booting} class="flex items-center space-x-2">
<span class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75">
</span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
</span>
<div class="text-gray-700 font-medium">
Booting
</div>
</div>
<div :if={@data_view.app_status == :error} class="flex items-center space-x-2">
<span class="relative flex h-3 w-3">
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>
<div class="text-gray-700 font-medium">
Error
</div>
</div>
<div
:if={@data_view.app_status in [:running, :shutting_down]}
class="pt-4 flex flex-col space-y-6"
data-el-outputs-container
id="outputs"
>
<div :for={output_view <- Enum.reverse(@data_view.output_views)}>
<LivebookWeb.Output.outputs
outputs={[output_view.output]}
dom_id_map={%{}}
session_id={@session.id}
session_pid={@session.pid}
client_id={@client_id}
input_values={output_view.input_values}
/>
</div>
</div>
<div style="height: 80vh"></div>
</div>
</div>
<div class="hidden md:flex invisible w-[80px]"></div>
</div>
<.modal
:if={@live_action == :source and @data_view.show_source}
id="source-modal"
show
class="w-full max-w-4xl"
patch={~p"/apps/#{@data_view.slug}"}
>
<.live_component module={LivebookWeb.AppLive.SourceComponent} id="source" session={@session} />
</.modal>
"""
end
@ -119,6 +154,9 @@ defmodule LivebookWeb.AppLive do
"Livebook - #{notebook_name}"
end
@impl true
def handle_params(_params, _url, socket), do: {:noreply, socket}
@impl true
def handle_info({:operation, operation}, socket) do
{:noreply, handle_operation(socket, operation)}
@ -190,7 +228,9 @@ defmodule LivebookWeb.AppLive do
input_values: input_values_for_output(output, data)
}
),
app_status: data.app_data.status
app_status: data.app_data.status,
show_source: data.notebook.app_settings.show_source,
slug: data.notebook.app_settings.slug
}
end

View file

@ -0,0 +1,56 @@
defmodule LivebookWeb.AppLive.SourceComponent do
use LivebookWeb, :live_component
alias Livebook.Session
@impl true
def update(assigns, socket) do
socket = assign(socket, assigns)
socket =
assign_new(socket, :source, fn ->
# Note: we need to load the notebook, so that we don't track
# the whole notebook in assigns
notebook = Session.get_notebook(socket.assigns.session.pid)
Livebook.LiveMarkdown.notebook_to_livemd(notebook, include_outputs: false)
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">
App source
</h3>
<p class="text-gray-700">
This app is built from the following 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">
<%= Session.file_name_for_download(@session) <> ".livemd" %>
</span>
<div class="flex justify-end space-x-2">
<span class="tooltip left" data-tooltip="Copy source">
<button
class="icon-button"
aria-label="copy source"
phx-click={JS.dispatch("lb:clipcopy", to: "#export-notebook-source")}
>
<.remix_icon icon="clipboard-line" class="text-lg" />
</button>
</span>
</div>
</div>
<div class="markdown">
<.code_preview source_id="export-notebook-source" language="markdown" source={@source} />
</div>
</div>
</div>
"""
end
end

View file

@ -80,6 +80,7 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
<%= if Ecto.Changeset.get_field(@changeset, :access_type) == :protected do %>
<.password_field field={f[:password]} spellcheck="false" phx-debounce="blur" />
<% end %>
<.switch_field field={f[:show_source]} label="Show source" />
</div>
<div class="mt-5 flex space-x-2">
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>

View file

@ -107,6 +107,7 @@ defmodule LivebookWeb.Router do
live "/apps/:slug", AppLive, :page
live "/apps/:slug/authenticate", AppAuthLive, :page
live "/apps/:slug/source", AppLive, :source
end
end