From 860a1e4bd9feab405b7a4e352b051e8d94d26d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 27 Feb 2023 17:05:36 +0100 Subject: [PATCH] Add source option (#1728) --- .../hooks/cell_editor/live_editor/monaco.js | 16 +++ lib/livebook/notebook/app_settings.ex | 15 +- lib/livebook_web/live/app_live.ex | 136 +++++++++++------- .../live/app_live/source_component.ex | 56 ++++++++ .../live/session_live/app_info_component.ex | 1 + lib/livebook_web/router.ex | 1 + 6 files changed, 173 insertions(+), 52 deletions(-) create mode 100644 lib/livebook_web/live/app_live/source_component.ex diff --git a/assets/js/hooks/cell_editor/live_editor/monaco.js b/assets/js/hooks/cell_editor/live_editor/monaco.js index c7806cbf0..bfcf0215e 100644 --- a/assets/js/hooks/cell_editor/live_editor/monaco.js +++ b/assets/js/hooks/cell_editor/live_editor/monaco.js @@ -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(/$/, ""); diff --git a/lib/livebook/notebook/app_settings.ex b/lib/livebook/notebook/app_settings.ex index aeec2f1b8..3875fff33 100644 --- a/lib/livebook/notebook/app_settings.ex +++ b/lib/livebook/notebook/app_settings.ex @@ -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" ) diff --git a/lib/livebook_web/live/app_live.ex b/lib/livebook_web/live/app_live.ex index 3f6d1b273..a2b493219 100644 --- a/lib/livebook_web/live/app_live.ex +++ b/lib/livebook_web/live/app_live.ex @@ -55,55 +55,90 @@ defmodule LivebookWeb.AppLive do @impl true def render(assigns) when assigns.app_authenticated? do ~H""" -
-
-
-
-

- <%= @data_view.notebook_name %> -

-
-
- - - - - -
- Booting -
-
-
- - - -
- Error -
-
-
-
- -
-
-
+
+
+ <.menu id="app-menu" position={:bottom_left}> + <:toggle> + + + <.menu_item> + <.link navigate={~p"/"} role="menuitem"> + <.remix_icon icon="home-6-line" /> + Home + + + <.menu_item :if={@data_view.show_source}> + <.link patch={~p"/apps/#{@data_view.slug}/source"} role="menuitem"> + <.remix_icon icon="code-line" /> + View source + + +
+
+
+
+
+

+ <%= @data_view.notebook_name %> +

+
+
+ + + + + +
+ Booting +
+
+
+ + + +
+ Error +
+
+
+
+ +
+
+
+
+
+
+ + <.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} /> + """ 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 diff --git a/lib/livebook_web/live/app_live/source_component.ex b/lib/livebook_web/live/app_live/source_component.ex new file mode 100644 index 000000000..9ef31ed5b --- /dev/null +++ b/lib/livebook_web/live/app_live/source_component.ex @@ -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""" +
+

+ App source +

+

+ This app is built from the following notebook source: +

+
+
+ + <%= Session.file_name_for_download(@session) <> ".livemd" %> + +
+ + + +
+
+
+ <.code_preview source_id="export-notebook-source" language="markdown" source={@source} /> +
+
+
+ """ + end +end diff --git a/lib/livebook_web/live/session_live/app_info_component.ex b/lib/livebook_web/live/session_live/app_info_component.ex index 8c8c1ad72..1001195de 100644 --- a/lib/livebook_web/live/session_live/app_info_component.ex +++ b/lib/livebook_web/live/session_live/app_info_component.ex @@ -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" />