From 3afa81f454a26aaf30c7d04bde16e76b48fae6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Tue, 2 Nov 2021 22:32:58 +0100 Subject: [PATCH] Add configuration for additional explore notebooks (#670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add configuration for additional explore notebooks * Update config/config.exs Co-authored-by: José Valim Co-authored-by: José Valim --- config/config.exs | 33 ++++++ lib/livebook/notebook/explore.ex | 132 ++++++++++++++++------- lib/livebook/utils.ex | 11 ++ lib/livebook_web/live/explore_helpers.ex | 4 +- lib/livebook_web/live/explore_live.ex | 6 +- lib/livebook_web/live/home_live.ex | 2 +- 6 files changed, 146 insertions(+), 42 deletions(-) diff --git a/config/config.exs b/config/config.exs index 537f4bc9b..d47d0f4d5 100644 --- a/config/config.exs +++ b/config/config.exs @@ -38,6 +38,39 @@ config :livebook, :default_runtime, {Livebook.Runtime.ElixirStandalone, []} # The plugs are called directly before the Livebook router. config :livebook, :plugs, [] +# A list of additional notebooks to include in the Explore sections. +# +# Note that the notebooks are loaded and embedded in a compiled module, +# so the paths are accessed at compile time only. +# +# ## Example +# +# config :livebook, :explore_notebooks, [ +# %{ +# # Required notebook path +# path: "/path/to/notebook.livemd", +# # Optional notebook identifier for URLs, as in /explore/notebooks/{slug} +# # By default the slug is inferred from file name, so there is no need to set it +# slug: "my-notebook" +# # Optional list of images +# image_paths: [ +# # This image can be sourced as images/myimage.jpg in the notebook +# "/path/to/myimage.jpg" +# ], +# # Optional details for the notebook card. If omitted, the notebook +# # is hidden in the UI, but still accessible under /explore/notebooks/{slug} +# details: %{ +# cover_path: "/path/to/logo.png", +# description: "My custom notebook that showcases some amazing stuff." +# } +# }, +# %{ +# path: "/path/to/other_notebook.livemd" +# } +# ] +# +config :livebook, :explore_notebooks, [] + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/lib/livebook/notebook/explore.ex b/lib/livebook/notebook/explore.ex index 9c8c854af..5bff8dfa4 100644 --- a/lib/livebook/notebook/explore.ex +++ b/lib/livebook/notebook/explore.ex @@ -15,87 +15,138 @@ defmodule Livebook.Notebook.Explore do slug: String.t(), livemd: String.t(), title: String.t(), - description: String.t(), - cover_url: String.t(), - images: images() + images: images(), + details: details() | nil } @type images :: %{String.t() => binary()} - infos = [ - %{ - path: Path.join(__DIR__, "explore/intro_to_livebook.livemd"), + @type details :: %{ + description: String.t(), + cover_url: String.t() + } + + images_dir = Path.expand("explore/images", __DIR__) + + welcome_config = %{ + path: Path.join(__DIR__, "explore/intro_to_livebook.livemd"), + details: %{ description: "Get to know Livebook, see how it works and explore its features.", cover_url: "/images/logo.png" - }, + } + } + + other_configs = [ %{ path: Path.join(__DIR__, "explore/distributed_portals_with_elixir.livemd"), - description: - "A fast-paced introduction to the Elixir language by building distributed data-transfer portals.", - cover_url: "/images/elixir-portal.jpeg", - image_names: ["portal-drop.jpeg", "portal-list.jpeg"] + image_paths: [ + Path.join(images_dir, "portal-drop.jpeg"), + Path.join(images_dir, "portal-list.jpeg") + ], + details: %{ + description: + "A fast-paced introduction to the Elixir language by building distributed data-transfer portals.", + cover_url: "/images/elixir-portal.jpeg" + } }, %{ path: Path.join(__DIR__, "explore/elixir_and_livebook.livemd"), - description: "Learn how to use some of Elixir and Livebook's unique features together.", - cover_url: "/images/elixir.png" + details: %{ + description: "Learn how to use some of Elixir and Livebook's unique features together.", + cover_url: "/images/elixir.png" + } }, %{ path: Path.join(__DIR__, "explore/intro_to_vega_lite.livemd"), - description: "Learn how to quickly create numerous plots for your data.", - cover_url: "/images/vega_lite.png" + details: %{ + description: "Learn how to quickly create numerous plots for your data.", + cover_url: "/images/vega_lite.png" + } }, %{ path: Path.join(__DIR__, "explore/intro_to_kino.livemd"), - description: "Display and control rich and interactive widgets in Livebook.", - cover_url: "/images/kino.png" + details: %{ + description: "Display and control rich and interactive widgets in Livebook.", + cover_url: "/images/kino.png" + } }, %{ path: Path.join(__DIR__, "explore/intro_to_nx.livemd"), - description: - "Enter Numerical Elixir, experience the power of multi-dimensional arrays of numbers.", - cover_url: "/images/nx.png" + details: %{ + description: + "Enter Numerical Elixir, experience the power of multi-dimensional arrays of numbers.", + cover_url: "/images/nx.png" + } }, # %{ # path: Path.join(__DIR__, "explore/intro_to_axon.livemd"), - # description: "Build Neural Networks in Elixir using a high-level, composable API.", - # cover_url: "/images/axon.png" + # details: %{ + # description: "Build Neural Networks in Elixir using a high-level, composable API.", + # cover_url: "/images/axon.png" + # } # }, %{ path: Path.join(__DIR__, "explore/vm_introspection.livemd"), - description: "Extract and visualize information about a remote running node.", - cover_url: "/images/vm_introspection.png" + details: %{ + description: "Extract and visualize information about a remote running node.", + cover_url: "/images/vm_introspection.png" + } } ] + user_configs = Application.fetch_env!(:livebook, :explore_notebooks) + + notebook_configs = [welcome_config] ++ user_configs ++ other_configs + notebook_infos = - for info <- infos do - path = Map.fetch!(info, :path) + for config <- notebook_configs do + path = + config[:path] || + raise "missing required :path attribute in notebook configuration: #{inspect(config)}" + @external_resource path markdown = File.read!(path) # Parse the file to ensure no warnings and read the title. # However, in the info we keep just the file contents to save on memory. - {notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(markdown) + {notebook, warnings} = Livebook.LiveMarkdown.Import.notebook_from_markdown(markdown) + + if warnings != [] do + items = Enum.map(warnings, &("- " <> &1)) + raise "found warnings while importing #{path}:\n\n" <> Enum.join(items, "\n") + end images = - info - |> Map.get(:image_names, []) - |> Map.new(fn image_name -> - path = Path.join([Path.dirname(path), "images", image_name]) - content = File.read!(path) + config + |> Map.get(:image_paths, []) + |> Map.new(fn image_path -> + image_name = Path.basename(image_path) + content = File.read!(image_path) {image_name, content} end) - slug = info[:slug] || path |> Path.basename() |> Path.rootname() |> String.replace("_", "-") + slug = + config[:slug] || path |> Path.basename() |> Path.rootname() |> String.replace("_", "-") %{ slug: slug, livemd: markdown, title: notebook.name, - description: Map.fetch!(info, :description), - cover_url: Map.fetch!(info, :cover_url), - images: images + images: images, + details: + if config_details = config[:details] do + description = + config_details[:description] || + raise "missing required :description attribute in notebook details: #{inspect(config_details)}" + + cover_url = + config_details[:cover_url] || + (config_details[:cover_path] && + Livebook.Utils.read_as_data_url!(config_details.cover_path)) || + raise "expected either :cover_path or :cover_url in notebooks details: #{inspect(config_details)}" + + %{description: description, cover_url: cover_url} + end } end @@ -105,6 +156,15 @@ defmodule Livebook.Notebook.Explore do @spec notebook_infos() :: list(notebook_info()) def notebook_infos(), do: unquote(Macro.escape(notebook_infos)) + @doc """ + Same as `notebook_infos/0`, but returns only notebooks that have + additional details. + """ + @spec visible_notebook_infos() :: list(notebook_info()) + def visible_notebook_infos() do + notebook_infos() |> Enum.filter(& &1.details) + end + @doc """ Finds explore notebook by slug and returns the parsed data structure. diff --git a/lib/livebook/utils.ex b/lib/livebook/utils.ex index 7d41cd9a8..f72af92f0 100644 --- a/lib/livebook/utils.ex +++ b/lib/livebook/utils.ex @@ -281,4 +281,15 @@ defmodule Livebook.Utils do |> Enum.intersperse("\n") |> IO.iodata_to_binary() end + + @doc """ + Reads file contents and encodes it into a data URL. + """ + @spec read_as_data_url!(Path.t()) :: binary() + def read_as_data_url!(path) do + content = File.read!(path) + mime = MIME.from_path(path) + data = Base.encode64(content) + "data:#{mime};base64,#{data}" + end end diff --git a/lib/livebook_web/live/explore_helpers.ex b/lib/livebook_web/live/explore_helpers.ex index a9b418db9..c586215d9 100644 --- a/lib/livebook_web/live/explore_helpers.ex +++ b/lib/livebook_web/live/explore_helpers.ex @@ -11,14 +11,14 @@ defmodule LivebookWeb.ExploreHelpers do
<%= live_redirect to: Routes.explore_path(@socket, :notebook, @notebook_info.slug), class: "flex items-center justify-center p-6 border-2 border-gray-100 rounded-t-2xl h-[150px]" do %> - + <% end %>
<%= live_redirect @notebook_info.title, to: Routes.explore_path(@socket, :notebook, @notebook_info.slug), class: "text-gray-800 font-semibold cursor-pointer" %>

- <%= @notebook_info.description %> + <%= @notebook_info.details.description %>

diff --git a/lib/livebook_web/live/explore_live.ex b/lib/livebook_web/live/explore_live.ex index c16715810..b8aac9174 100644 --- a/lib/livebook_web/live/explore_live.ex +++ b/lib/livebook_web/live/explore_live.ex @@ -8,7 +8,7 @@ defmodule LivebookWeb.ExploreLive do @impl true def mount(_params, _session, socket) do - [lead_notebook_info | notebook_infos] = Explore.notebook_infos() + [lead_notebook_info | notebook_infos] = Explore.visible_notebook_infos() {:ok, assign(socket, @@ -46,7 +46,7 @@ defmodule LivebookWeb.ExploreLive do <%= @lead_notebook_info.title %>

- <%= @lead_notebook_info.description %> + <%= @lead_notebook_info.details.description %>

<%= live_patch "Let's go", @@ -55,7 +55,7 @@ defmodule LivebookWeb.ExploreLive do
diff --git a/lib/livebook_web/live/home_live.ex b/lib/livebook_web/live/home_live.ex index 0ceb49f3e..c589083d6 100644 --- a/lib/livebook_web/live/home_live.ex +++ b/lib/livebook_web/live/home_live.ex @@ -13,7 +13,7 @@ defmodule LivebookWeb.HomeLive do end sessions = sort_sessions(Sessions.list_sessions()) - notebook_infos = Notebook.Explore.notebook_infos() |> Enum.take(3) + notebook_infos = Notebook.Explore.visible_notebook_infos() |> Enum.take(3) {:ok, assign(socket,