Show notifications for new app version and import warnings (#1955)

This commit is contained in:
Jonatan Kłosko 2023-06-02 21:32:44 +02:00 committed by GitHub
parent cc9731c428
commit 75614e7885
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 125 additions and 29 deletions

View file

@ -13,12 +13,22 @@ defmodule Livebook.App do
# always taken from the most recently deployed notebook (e.g. access # always taken from the most recently deployed notebook (e.g. access
# type, automatic shutdown, deployment strategy). # type, automatic shutdown, deployment strategy).
defstruct [:slug, :pid, :version, :notebook_name, :public?, :multi_session, :sessions] defstruct [
:slug,
:pid,
:version,
:warnings,
:notebook_name,
:public?,
:multi_session,
:sessions
]
@type t :: %{ @type t :: %{
slug: slug(), slug: slug(),
pid: pid(), pid: pid(),
version: pos_integer(), version: pos_integer(),
warnings: list(String.t()),
notebook_name: String.t(), notebook_name: String.t(),
public?: boolean(), public?: boolean(),
multi_session: boolean(), multi_session: boolean(),
@ -46,12 +56,15 @@ defmodule Livebook.App do
* `:notebook` (required) - the notebook for initial deployment * `:notebook` (required) - the notebook for initial deployment
* `:warnings` - a list of warnings to show for the initial deployment
""" """
@spec start_link(keyword()) :: {:ok, pid} | {:error, any()} @spec start_link(keyword()) :: {:ok, pid} | {:error, any()}
def start_link(opts) do def start_link(opts) do
notebook = Keyword.fetch!(opts, :notebook) notebook = Keyword.fetch!(opts, :notebook)
warnings = Keyword.get(opts, :warnings, [])
GenServer.start_link(__MODULE__, {notebook}) GenServer.start_link(__MODULE__, {notebook, warnings})
end end
@doc """ @doc """
@ -100,9 +113,10 @@ defmodule Livebook.App do
@doc """ @doc """
Deploys a new notebook into the app. Deploys a new notebook into the app.
""" """
@spec deploy(pid(), Livebook.Notebook.t()) :: :ok @spec deploy(pid(), Livebook.Notebook.t(), keyword()) :: :ok
def deploy(pid, notebook) do def deploy(pid, notebook, opts \\ []) do
GenServer.cast(pid, {:deploy, notebook}) warnings = Keyword.get(opts, :warnings, [])
GenServer.cast(pid, {:deploy, notebook, warnings})
end end
@doc """ @doc """
@ -136,11 +150,12 @@ defmodule Livebook.App do
end end
@impl true @impl true
def init({notebook}) do def init({notebook, warnings}) do
{:ok, {:ok,
%{ %{
version: 1, version: 1,
notebook: notebook, notebook: notebook,
warnings: warnings,
sessions: [], sessions: [],
users: %{} users: %{}
} }
@ -176,11 +191,11 @@ defmodule Livebook.App do
end end
@impl true @impl true
def handle_cast({:deploy, notebook}, state) do def handle_cast({:deploy, notebook, warnings}, state) do
true = notebook.app_settings.slug == state.notebook.app_settings.slug true = notebook.app_settings.slug == state.notebook.app_settings.slug
{:noreply, {:noreply,
%{state | notebook: notebook, version: state.version + 1} %{state | notebook: notebook, version: state.version + 1, warnings: warnings}
|> start_eagerly() |> start_eagerly()
|> shutdown_old_versions() |> shutdown_old_versions()
|> notify_update()} |> notify_update()}
@ -221,6 +236,7 @@ defmodule Livebook.App do
slug: state.notebook.app_settings.slug, slug: state.notebook.app_settings.slug,
pid: self(), pid: self(),
version: state.version, version: state.version,
warnings: state.warnings,
notebook_name: state.notebook.name, notebook_name: state.notebook.name,
public?: state.notebook.app_settings.access_type == :public, public?: state.notebook.app_settings.access_type == :public,
multi_session: state.notebook.app_settings.multi_session, multi_session: state.notebook.app_settings.multi_session,

View file

@ -16,9 +16,16 @@ defmodule Livebook.Apps do
If there is no app process under the corresponding slug, it is started. If there is no app process under the corresponding slug, it is started.
Otherwise the notebook is deployed as a new version into the existing Otherwise the notebook is deployed as a new version into the existing
app. app.
## Options
* `:warnings` - a list of warnings to show for the new deployment
""" """
@spec deploy(Livebook.Notebook.t()) :: {:ok, pid()} | {:error, term()} @spec deploy(Livebook.Notebook.t(), keyword()) :: {:ok, pid()} | {:error, term()}
def deploy(notebook) do def deploy(notebook, opts \\ []) do
opts = Keyword.validate!(opts, warnings: [])
slug = notebook.app_settings.slug slug = notebook.app_settings.slug
name = name(slug) name = name(slug)
@ -27,25 +34,25 @@ defmodule Livebook.Apps do
:global.trans({{:app_registration, name}, node()}, fn -> :global.trans({{:app_registration, name}, node()}, fn ->
case :global.whereis_name(name) do case :global.whereis_name(name) do
:undefined -> :undefined ->
with {:ok, pid} <- start_app(notebook) do with {:ok, pid} <- start_app(notebook, opts[:warnings]) do
:yes = :global.register_name(name, pid) :yes = :global.register_name(name, pid)
{:ok, pid} {:ok, pid}
end end
pid -> pid ->
App.deploy(pid, notebook) App.deploy(pid, notebook, warnings: opts[:warnings])
{:ok, pid} {:ok, pid}
end end
end) end)
pid -> pid ->
App.deploy(pid, notebook) App.deploy(pid, notebook, warnings: opts[:warnings])
{:ok, pid} {:ok, pid}
end end
end end
defp start_app(notebook) do defp start_app(notebook, warnings) do
opts = [notebook: notebook] opts = [notebook: notebook, warnings: warnings]
case DynamicSupervisor.start_child(Livebook.AppSupervisor, {App, opts}) do case DynamicSupervisor.start_child(Livebook.AppSupervisor, {App, opts}) do
{:ok, pid} -> {:ok, pid} ->
@ -178,7 +185,8 @@ defmodule Livebook.Apps do
end end
if Livebook.Notebook.AppSettings.valid?(notebook.app_settings) do if Livebook.Notebook.AppSettings.valid?(notebook.app_settings) do
deploy(notebook) warnings = Enum.map(warnings, &("Import: " <> &1))
deploy(notebook, warnings: warnings)
else else
Logger.warning("Skipping app deployment at #{path} due to invalid settings") Logger.warning("Skipping app deployment at #{path} due to invalid settings")
end end

View file

@ -21,7 +21,7 @@ defmodule Livebook.LiveMarkdown.Import do
end end
defp earmark_message_to_string({_severity, line_number, message}) do defp earmark_message_to_string({_severity, line_number, message}) do
"Line #{line_number}: #{message}" "line #{line_number} - #{Livebook.Utils.downcase_first(message)}"
end end
# Does initial pre-processing of the AST, so that it conforms to the expected form. # Does initial pre-processing of the AST, so that it conforms to the expected form.
@ -46,7 +46,7 @@ defmodule Livebook.LiveMarkdown.Import do
ast = Enum.map(ast, &downgrade_heading/1) ast = Enum.map(ast, &downgrade_heading/1)
message = message =
"Downgrading all headings, because #{primary_headings} instances of heading 1 were found" "downgrading all headings, because #{primary_headings} instances of heading 1 were found"
{ast, [message]} {ast, [message]}
else else
@ -75,7 +75,7 @@ defmodule Livebook.LiveMarkdown.Import do
{ast, []} {ast, []}
else else
ast = comments ++ [heading] ++ leading ++ rest ast = comments ++ [heading] ++ leading ++ rest
message = "Moving heading 1 to the top of the notebook" message = "moving heading 1 to the top of the notebook"
{ast, [message]} {ast, [message]}
end end
end end

View file

@ -2040,9 +2040,15 @@ defmodule Livebook.Session do
locator = {container_ref_for_section(section), cell.id} locator = {container_ref_for_section(section), cell.id}
parent_locators = parent_locators_for_cell(state.data, cell) parent_locators = parent_locators_for_cell(state.data, cell)
language =
case cell do
%Cell.Code{} -> cell.language
_ -> :elixir
end
Runtime.evaluate_code( Runtime.evaluate_code(
state.data.runtime, state.data.runtime,
cell.language, language,
cell.source, cell.source,
locator, locator,
parent_locators, parent_locators,

View file

@ -85,6 +85,34 @@ defmodule LivebookWeb.CoreComponents do
""" """
end end
@doc """
Renders a message notice.
Similar to `flash/1`, but for permanent messages on the page.
## Examples
<.message_box kind={:info} message="🦊 in a 📦" />
"""
attr :message, :string, required: true
attr :kind, :atom, values: [:info, :success, :warning, :error]
def message_box(assigns) do
~H"""
<div class={[
"shadow text-sm flex items-center space-x-3 rounded-lg px-4 py-2 border-l-4 rounded-l-none bg-white text-gray-700",
@kind == :info && "border-blue-500",
@kind == :success && "border-blue-500",
@kind == :warning && "border-yellow-300",
@kind == :error && "border-red-500"
]}>
<div class="whitespace-pre-wrap pr-2 max-h-52 overflow-y-auto tiny-scrollbar" phx-no-format><%= @message %></div>
</div>
"""
end
@doc """ @doc """
Creates a live region with the given role. Creates a live region with the given role.

View file

@ -251,6 +251,14 @@ defmodule LivebookWeb.AppSessionLive do
redirect_on_closed(socket) redirect_on_closed(socket)
end end
defp after_operation(socket, _prev_socket, {:app_shutdown, _client_id}) do
put_flash(
socket,
:info,
"A new version has been deployed, this session will close once everybody leaves"
)
end
defp after_operation(socket, _prev_socket, _operation), do: socket defp after_operation(socket, _prev_socket, _operation), do: socket
defp redirect_on_closed(socket) do defp redirect_on_closed(socket) do

View file

@ -53,6 +53,9 @@ defmodule LivebookWeb.AppsLive do
<div class="mb-2 text-gray-800 font-medium text-xl"> <div class="mb-2 text-gray-800 font-medium text-xl">
<%= "/" <> app.slug %> <%= "/" <> app.slug %>
</div> </div>
<div class="mt-4 flex flex-col gap-3">
<.message_box :for={warning <- app.warnings} kind={:warning} message={warning} />
</div>
<div class="mt-4 mb-2 text-gray-600 font-medium text-sm"> <div class="mt-4 mb-2 text-gray-600 font-medium text-sm">
App info App info
</div> </div>

View file

@ -14,11 +14,12 @@ defmodule LivebookWeb.SessionLive.AppInfoComponent do
<.app_info_icon /> <.app_info_icon />
</div> </div>
<%= if @session.mode == :app do %> <%= if @session.mode == :app do %>
<div class="mt-5 flex flex-col space-y-6"> <div class="mt-5 flex flex-col">
<span class="text-gray-700 text-sm"> <.message_box
This session is a running app. To deploy a modified version, you can fork it. kind={:info}
</span> message="This session is a running app. To deploy a modified version, you can fork it."
<div> />
<div class="mt-6">
<button class="button-base button-blue" phx-click="fork_session"> <button class="button-base button-blue" phx-click="fork_session">
<.remix_icon icon="git-branch-line" /> <.remix_icon icon="git-branch-line" />
<span>Fork</span> <span>Fork</span>

View file

@ -72,5 +72,31 @@ defmodule Livebook.AppsTest do
Livebook.App.close(app.pid) Livebook.App.close(app.pid)
end end
@tag :capture_log
@tag :tmp_dir
test "deploys with import warnings", %{tmp_dir: tmp_dir} do
app_path = Path.join(tmp_dir, "app.livemd")
File.write!(app_path, """
<!-- livebook:{"app_settings":{"slug":"app"}} -->
# App
```elixir
""")
Livebook.Apps.subscribe()
Livebook.Apps.deploy_apps_in_dir(tmp_dir)
assert_receive {:app_created, %{slug: "app", warnings: warnings} = app}
assert warnings == [
"Import: line 5 - fenced Code Block opened with ``` not closed at end of input"
]
Livebook.App.close(app.pid)
end
end end
end end

View file

@ -241,7 +241,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
] ]
} = notebook } = notebook
assert ["Downgrading all headings, because 2 instances of heading 1 were found"] == messages assert ["downgrading all headings, because 2 instances of heading 1 were found"] == messages
end end
test "preserves markdown modifiers in notebok/section names" do test "preserves markdown modifiers in notebok/section names" do
@ -355,7 +355,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
] ]
} = notebook } = notebook
assert ["Moving heading 1 to the top of the notebook"] == messages assert ["moving heading 1 to the top of the notebook"] == messages
end end
test "includes parsing warnings in the returned message list" do test "includes parsing warnings in the returned message list" do
@ -369,7 +369,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
{_notebook, messages} = Import.notebook_from_livemd(markdown) {_notebook, messages} = Import.notebook_from_livemd(markdown)
assert ["Line 3: Closing unclosed backquotes ` at end of input"] == messages assert ["line 3 - closing unclosed backquotes ` at end of input"] == messages
end end
test "imports non-elixir code snippets as part of markdown cells" do test "imports non-elixir code snippets as part of markdown cells" do

View file

@ -153,7 +153,7 @@ defmodule LivebookWeb.OpenLiveTest do
{path, flash} = assert_redirect(view, 5000) {path, flash} = assert_redirect(view, 5000)
assert flash["warning"] =~ assert flash["warning"] =~
"We found problems while importing the file and tried to autofix them:\n- Downgrading all headings, because 3 instances of heading 1 were found" "We found problems while importing the file and tried to autofix them:\n- downgrading all headings, because 3 instances of heading 1 were found"
close_session_by_path(path) close_session_by_path(path)
end end