mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-10 22:23:32 +08:00
Add configuration for additional explore notebooks (#670)
* Add configuration for additional explore notebooks * Update config/config.exs Co-authored-by: José Valim <jose.valim@dashbit.co> Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
c1779fcb07
commit
3afa81f454
6 changed files with 146 additions and 42 deletions
|
|
@ -38,6 +38,39 @@ config :livebook, :default_runtime, {Livebook.Runtime.ElixirStandalone, []}
|
||||||
# The plugs are called directly before the Livebook router.
|
# The plugs are called directly before the Livebook router.
|
||||||
config :livebook, :plugs, []
|
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
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{Mix.env()}.exs"
|
import_config "#{Mix.env()}.exs"
|
||||||
|
|
|
||||||
|
|
@ -15,87 +15,138 @@ defmodule Livebook.Notebook.Explore do
|
||||||
slug: String.t(),
|
slug: String.t(),
|
||||||
livemd: String.t(),
|
livemd: String.t(),
|
||||||
title: String.t(),
|
title: String.t(),
|
||||||
description: String.t(),
|
images: images(),
|
||||||
cover_url: String.t(),
|
details: details() | nil
|
||||||
images: images()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@type images :: %{String.t() => binary()}
|
@type images :: %{String.t() => binary()}
|
||||||
|
|
||||||
infos = [
|
@type details :: %{
|
||||||
%{
|
description: String.t(),
|
||||||
path: Path.join(__DIR__, "explore/intro_to_livebook.livemd"),
|
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.",
|
description: "Get to know Livebook, see how it works and explore its features.",
|
||||||
cover_url: "/images/logo.png"
|
cover_url: "/images/logo.png"
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
other_configs = [
|
||||||
%{
|
%{
|
||||||
path: Path.join(__DIR__, "explore/distributed_portals_with_elixir.livemd"),
|
path: Path.join(__DIR__, "explore/distributed_portals_with_elixir.livemd"),
|
||||||
description:
|
image_paths: [
|
||||||
"A fast-paced introduction to the Elixir language by building distributed data-transfer portals.",
|
Path.join(images_dir, "portal-drop.jpeg"),
|
||||||
cover_url: "/images/elixir-portal.jpeg",
|
Path.join(images_dir, "portal-list.jpeg")
|
||||||
image_names: ["portal-drop.jpeg", "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"),
|
path: Path.join(__DIR__, "explore/elixir_and_livebook.livemd"),
|
||||||
description: "Learn how to use some of Elixir and Livebook's unique features together.",
|
details: %{
|
||||||
cover_url: "/images/elixir.png"
|
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"),
|
path: Path.join(__DIR__, "explore/intro_to_vega_lite.livemd"),
|
||||||
description: "Learn how to quickly create numerous plots for your data.",
|
details: %{
|
||||||
cover_url: "/images/vega_lite.png"
|
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"),
|
path: Path.join(__DIR__, "explore/intro_to_kino.livemd"),
|
||||||
description: "Display and control rich and interactive widgets in Livebook.",
|
details: %{
|
||||||
cover_url: "/images/kino.png"
|
description: "Display and control rich and interactive widgets in Livebook.",
|
||||||
|
cover_url: "/images/kino.png"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
path: Path.join(__DIR__, "explore/intro_to_nx.livemd"),
|
path: Path.join(__DIR__, "explore/intro_to_nx.livemd"),
|
||||||
description:
|
details: %{
|
||||||
"Enter Numerical Elixir, experience the power of multi-dimensional arrays of numbers.",
|
description:
|
||||||
cover_url: "/images/nx.png"
|
"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"),
|
# path: Path.join(__DIR__, "explore/intro_to_axon.livemd"),
|
||||||
# description: "Build Neural Networks in Elixir using a high-level, composable API.",
|
# details: %{
|
||||||
# cover_url: "/images/axon.png"
|
# 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"),
|
path: Path.join(__DIR__, "explore/vm_introspection.livemd"),
|
||||||
description: "Extract and visualize information about a remote running node.",
|
details: %{
|
||||||
cover_url: "/images/vm_introspection.png"
|
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 =
|
notebook_infos =
|
||||||
for info <- infos do
|
for config <- notebook_configs do
|
||||||
path = Map.fetch!(info, :path)
|
path =
|
||||||
|
config[:path] ||
|
||||||
|
raise "missing required :path attribute in notebook configuration: #{inspect(config)}"
|
||||||
|
|
||||||
@external_resource path
|
@external_resource path
|
||||||
|
|
||||||
markdown = File.read!(path)
|
markdown = File.read!(path)
|
||||||
# Parse the file to ensure no warnings and read the title.
|
# 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.
|
# 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 =
|
images =
|
||||||
info
|
config
|
||||||
|> Map.get(:image_names, [])
|
|> Map.get(:image_paths, [])
|
||||||
|> Map.new(fn image_name ->
|
|> Map.new(fn image_path ->
|
||||||
path = Path.join([Path.dirname(path), "images", image_name])
|
image_name = Path.basename(image_path)
|
||||||
content = File.read!(path)
|
content = File.read!(image_path)
|
||||||
{image_name, content}
|
{image_name, content}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
slug = info[:slug] || path |> Path.basename() |> Path.rootname() |> String.replace("_", "-")
|
slug =
|
||||||
|
config[:slug] || path |> Path.basename() |> Path.rootname() |> String.replace("_", "-")
|
||||||
|
|
||||||
%{
|
%{
|
||||||
slug: slug,
|
slug: slug,
|
||||||
livemd: markdown,
|
livemd: markdown,
|
||||||
title: notebook.name,
|
title: notebook.name,
|
||||||
description: Map.fetch!(info, :description),
|
images: images,
|
||||||
cover_url: Map.fetch!(info, :cover_url),
|
details:
|
||||||
images: images
|
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
|
end
|
||||||
|
|
||||||
|
|
@ -105,6 +156,15 @@ defmodule Livebook.Notebook.Explore do
|
||||||
@spec notebook_infos() :: list(notebook_info())
|
@spec notebook_infos() :: list(notebook_info())
|
||||||
def notebook_infos(), do: unquote(Macro.escape(notebook_infos))
|
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 """
|
@doc """
|
||||||
Finds explore notebook by slug and returns the parsed data structure.
|
Finds explore notebook by slug and returns the parsed data structure.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,4 +281,15 @@ defmodule Livebook.Utils do
|
||||||
|> Enum.intersperse("\n")
|
|> Enum.intersperse("\n")
|
||||||
|> IO.iodata_to_binary()
|
|> IO.iodata_to_binary()
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ defmodule LivebookWeb.ExploreHelpers do
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<%= live_redirect to: Routes.explore_path(@socket, :notebook, @notebook_info.slug),
|
<%= 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 %>
|
class: "flex items-center justify-center p-6 border-2 border-gray-100 rounded-t-2xl h-[150px]" do %>
|
||||||
<img src={@notebook_info.cover_url} class="max-h-full max-w-[75%]" />
|
<img src={@notebook_info.details.cover_url} class="max-h-full max-w-[75%]" />
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="px-6 py-4 bg-gray-100 rounded-b-2xl flex-grow">
|
<div class="px-6 py-4 bg-gray-100 rounded-b-2xl flex-grow">
|
||||||
<%= live_redirect @notebook_info.title,
|
<%= live_redirect @notebook_info.title,
|
||||||
to: Routes.explore_path(@socket, :notebook, @notebook_info.slug),
|
to: Routes.explore_path(@socket, :notebook, @notebook_info.slug),
|
||||||
class: "text-gray-800 font-semibold cursor-pointer" %>
|
class: "text-gray-800 font-semibold cursor-pointer" %>
|
||||||
<p class="mt-2 text-sm text-gray-600">
|
<p class="mt-2 text-sm text-gray-600">
|
||||||
<%= @notebook_info.description %>
|
<%= @notebook_info.details.description %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule LivebookWeb.ExploreLive do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
[lead_notebook_info | notebook_infos] = Explore.notebook_infos()
|
[lead_notebook_info | notebook_infos] = Explore.visible_notebook_infos()
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
assign(socket,
|
assign(socket,
|
||||||
|
|
@ -46,7 +46,7 @@ defmodule LivebookWeb.ExploreLive do
|
||||||
<%= @lead_notebook_info.title %>
|
<%= @lead_notebook_info.title %>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mt-2 text-sm text-gray-300">
|
<p class="mt-2 text-sm text-gray-300">
|
||||||
<%= @lead_notebook_info.description %>
|
<%= @lead_notebook_info.details.description %>
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<%= live_patch "Let's go",
|
<%= live_patch "Let's go",
|
||||||
|
|
@ -55,7 +55,7 @@ defmodule LivebookWeb.ExploreLive do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow hidden md:flex flex items-center justify-center">
|
<div class="flex-grow hidden md:flex flex items-center justify-center">
|
||||||
<img src={@lead_notebook_info.cover_url} height="120" width="120" alt="livebook" />
|
<img src={@lead_notebook_info.details.cover_url} height="120" width="120" alt="livebook" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defmodule LivebookWeb.HomeLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
sessions = sort_sessions(Sessions.list_sessions())
|
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,
|
{:ok,
|
||||||
assign(socket,
|
assign(socket,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue