mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-17 17:06:05 +08:00
Add source option (#1728)
This commit is contained in:
parent
b8c19475a3
commit
860a1e4bd9
6 changed files with 173 additions and 52 deletions
|
@ -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\/>$/, "");
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
56
lib/livebook_web/live/app_live/source_component.ex
Normal file
56
lib/livebook_web/live/app_live/source_component.ex
Normal 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
|
|
@ -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?}>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue