mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-26 09:22:00 +08:00
Add "Hello Livebook" notebook (#123)
* Limit module result to a single line * Add introductory notebook to get started with * Don't show tooltip on focused elements * Update lib/livebook/notebook/hello_livebook.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update lib/livebook/notebook/hello_livebook.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update lib/livebook/notebook/hello_livebook.ex Co-authored-by: José Valim <jose.valim@dashbit.co> * Update notebook settings reference * Add note on package authors * Add tests * Update Phoenix version to git master Co-authored-by: José Valim <jose.valim@dashbit.co>
This commit is contained in:
parent
83a092fa38
commit
5c5b4ece26
9 changed files with 259 additions and 14 deletions
|
@ -36,6 +36,14 @@
|
|||
@apply px-2 py-1 bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100 focus:bg-gray-100;
|
||||
}
|
||||
|
||||
.button-square-icon {
|
||||
@apply p-0 flex items-center justify-center h-10 w-10;
|
||||
}
|
||||
|
||||
.button-square-icon i {
|
||||
@apply text-xl leading-none;
|
||||
}
|
||||
|
||||
.choice-button {
|
||||
@apply px-5 py-2 rounded-lg border text-gray-700 bg-white border-gray-200;
|
||||
}
|
||||
|
|
|
@ -55,14 +55,12 @@ Example usage:
|
|||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.tooltip:hover:before,
|
||||
.tooltip:focus-within:before {
|
||||
.tooltip:hover:before {
|
||||
visibility: visible;
|
||||
transition-delay: var(--show-delay);
|
||||
}
|
||||
|
||||
.tooltip:hover:after,
|
||||
.tooltip:focus-within:after {
|
||||
.tooltip:hover:after {
|
||||
visibility: visible;
|
||||
transition-delay: var(--show-delay);
|
||||
}
|
||||
|
|
|
@ -13,8 +13,13 @@ defmodule Livebook.Evaluator.StringFormatter do
|
|||
:ignored
|
||||
end
|
||||
|
||||
def format({:ok, {:module, _, _, _} = value}) do
|
||||
inspected = inspect(value, inspect_opts(limit: 10))
|
||||
{:inspect, inspected}
|
||||
end
|
||||
|
||||
def format({:ok, value}) do
|
||||
inspected = inspect(value, pretty: true, width: 100, syntax_colors: syntax_colors())
|
||||
inspected = inspect(value, inspect_opts())
|
||||
{:inspect, inspected}
|
||||
end
|
||||
|
||||
|
@ -23,6 +28,11 @@ defmodule Livebook.Evaluator.StringFormatter do
|
|||
{:error, formatted}
|
||||
end
|
||||
|
||||
defp inspect_opts(opts \\ []) do
|
||||
default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()]
|
||||
Keyword.merge(default_opts, opts)
|
||||
end
|
||||
|
||||
defp syntax_colors() do
|
||||
# Note: we intentionally don't specify colors
|
||||
# for `:binary`, `:list`, `:map` and `:tuple`
|
||||
|
|
191
lib/livebook/notebook/hello_livebook.ex
Normal file
191
lib/livebook/notebook/hello_livebook.ex
Normal file
|
@ -0,0 +1,191 @@
|
|||
defmodule Livebook.Notebook.HelloLivebook do
|
||||
livemd = ~s'''
|
||||
# Hello Livebook
|
||||
|
||||
## Introduction
|
||||
|
||||
We are happy you decided to give Livebook a try, hopefully it empowers
|
||||
you to build great stuff 🚀
|
||||
|
||||
Livebook is a tool for crafting **interactive** and **collaborative** code notebooks.
|
||||
It is primarily meant as a tool rapid prototyping - think of it as an IEx session
|
||||
combined with your editor.
|
||||
You can also use it for authoring shareable articles that people can easily
|
||||
run and play around with.
|
||||
Package authors can write notebooks as interactive tutorials
|
||||
and include them in their repository, so that users can easily download
|
||||
and run them locally.
|
||||
|
||||
## Basic usage
|
||||
|
||||
Each notebook consists of a number of cells, which serve as primary building blocks.
|
||||
There are **Markdown** cells (such as this one) that allow you to describe your work
|
||||
and **Elixir** cells where the magic takes place!
|
||||
|
||||
To insert a new cell move your between cells and click one of the revealed buttons. 👇
|
||||
|
||||
```elixir
|
||||
# This is an Elixir cell - as the name suggests that's where the code goes.
|
||||
# To evaluate this cell, you can either press the "Evaluate" button above
|
||||
# or use `ctrl + enter` like a boss!
|
||||
|
||||
message = "hey, grab yourself a cup of 🍵"
|
||||
```
|
||||
|
||||
Subsequent cells have access to the bindings you defined!
|
||||
|
||||
```elixir
|
||||
String.replace(message, "🍵", "☕")
|
||||
```
|
||||
|
||||
Note however that bindings are not global, so each cell *sees* only stuff that goes
|
||||
above itself. This approach helps to keep the notebook clean and predictable
|
||||
as you keep working on it!
|
||||
|
||||
## Sections
|
||||
|
||||
You can leverage so called **sections** to nicely group related cells together.
|
||||
Click on the book icon in the left sidebar to reveal a list of all sections.
|
||||
As you can see, this approach helps to easily jump around the notebook,
|
||||
especially once it gets grows.
|
||||
|
||||
Let's make use of this section to see how output is captured!
|
||||
|
||||
```elixir
|
||||
cats = ~w(😼 😹 😻 😺 😸 😽)
|
||||
|
||||
for _ <- 1..3 do
|
||||
cats
|
||||
|> Enum.take_random(3)
|
||||
|> Enum.join(" ")
|
||||
|> IO.puts()
|
||||
end
|
||||
```
|
||||
|
||||
## Notebook files
|
||||
|
||||
My default notebooks are kept in memory, which is fine for interactive hacking,
|
||||
but oftentimes you will want to save your work for later. Fortunately notebooks
|
||||
can be persisted by clicking on the "Settings" icon in the left sidebar
|
||||
and selecting the file location.
|
||||
|
||||
Notebooks are stored in **live markdown** format, which is essentially the markdown you know,
|
||||
with just a few assumptions on how particular elements are represented. Thanks to this
|
||||
approach you can easily keep notebooks under version control and get readable diffs.
|
||||
You can also easily preview those files, reuse for blog posts and even edit in a text editor.
|
||||
|
||||
## Modules
|
||||
|
||||
As we already saw, Elixir cells can be used for working on tiny snippets,
|
||||
but you may as well define a module!
|
||||
|
||||
```elixir
|
||||
defmodule Utils do
|
||||
@doc """
|
||||
Generates a random binary id.
|
||||
"""
|
||||
@spec random_id() :: binary()
|
||||
def random_id() do
|
||||
:crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
If you're surprised by the above output, keep in mind that
|
||||
every Elixir expression evaluates to some value and as so does module compilation!
|
||||
|
||||
Having the module defined, let's take it for a spin.
|
||||
|
||||
```elixir
|
||||
Utils.random_id()
|
||||
```
|
||||
|
||||
## Imports
|
||||
|
||||
You can import modules as normally to make the imported functions visible
|
||||
to all subsequent cells. Usually you want to keep `import`, `alias` and `require`
|
||||
in the first section, as part of the notebook setup.
|
||||
|
||||
```elixir
|
||||
import IEx.Helpers
|
||||
```
|
||||
|
||||
```elixir
|
||||
h(Enum.map())
|
||||
```
|
||||
|
||||
```elixir
|
||||
# Sidenote: http://www.numbat.org.au/thenumbat
|
||||
i("I ❤️ Numbats")
|
||||
```
|
||||
|
||||
## Runtimes
|
||||
|
||||
Livebook has a concept of **runtime**, which in practice is an Elixir node responsible
|
||||
for evaluating your code.
|
||||
|
||||
By default a new Elixir node is started (similarly to starting `iex`),
|
||||
but you can also choose to run inside a Mix project (as you would with `iex -S mix`)
|
||||
or even manually attach to an existing distributed node!
|
||||
You can configure the runtime under "Notebook settings".
|
||||
|
||||
## Using packages
|
||||
|
||||
Sometimes you need a dependency or two and notebooks are no exception to this.
|
||||
|
||||
One way to work with packages is to create a Mix project and configure the notebook
|
||||
to run its context (as pointed out above). This approach makes sense if you already have
|
||||
a Mix project you are working on, especially because this makes all project's
|
||||
modules available as well.
|
||||
|
||||
But there are cases when you just want to play around with a new package
|
||||
or quickly prototype some code that relies on such. Fortunately, starting
|
||||
version `v1.12` Elixir ships with `Mix.install/2` that allows for installing
|
||||
dependencies into Elixir runtime! This approach is especially useful for sharing notebooks,
|
||||
because everyone will be able to get the same dependencies. Let's try this out:
|
||||
|
||||
```elixir
|
||||
# Note: this requires Elixir version >= 1.12
|
||||
Mix.install([
|
||||
{:jason, "~> 1.2"}
|
||||
])
|
||||
```
|
||||
|
||||
```elixir
|
||||
%{elixir: "rulez"}
|
||||
|> Jason.encode!()
|
||||
|> IO.puts()
|
||||
```
|
||||
|
||||
It is a good idea to specify versions of the installed packages,
|
||||
so that the notebook is easily reproducible later on.
|
||||
|
||||
Also keep in mind that `Mix.install/2` can be called only once
|
||||
per runtime, so if you need to modify the dependencies, you should
|
||||
go to the notebook runtime configuration and **reconnect** the current runtime.
|
||||
|
||||
## Stepping up your workflow
|
||||
|
||||
Once you start using notebooks more, it's gonna be beneficial
|
||||
to optimise how you move around. Livebook leverages the concept of
|
||||
**navigation**/**insert** modes and offers many shortcuts for common operations.
|
||||
Make sure to check out the shortcuts by clicking the "Keyboard" icon in
|
||||
the sidebar panel or by typing `?`.
|
||||
|
||||
## Final notes
|
||||
|
||||
Livebook is an open source project, so feel free to look into
|
||||
[the repository](https://github.com/elixir-nx/livebook)
|
||||
to contribute, report bugs, suggest features or just skim over the codebase.
|
||||
|
||||
Now go ahead and build something cool! 🚢
|
||||
'''
|
||||
|
||||
{notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(livemd)
|
||||
|
||||
@notebook notebook
|
||||
|
||||
def new() do
|
||||
@notebook
|
||||
end
|
||||
end
|
|
@ -36,10 +36,18 @@ defmodule LivebookWeb.HomeLive do
|
|||
<div class="text-2xl text-gray-800 font-semibold">
|
||||
Livebook
|
||||
</div>
|
||||
<button class="button button-blue"
|
||||
phx-click="new">
|
||||
New notebook
|
||||
</button>
|
||||
<div class="flex space-x-2">
|
||||
<span class="tooltip top" aria-label="Introduction">
|
||||
<button class="button button-outlined-gray button-square-icon"
|
||||
phx-click="hello_livebook">
|
||||
<%= remix_icon("compass-line") %>
|
||||
</button>
|
||||
</span>
|
||||
<button class="button button-blue"
|
||||
phx-click="new">
|
||||
New notebook
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-80">
|
||||
<%= live_component @socket, LivebookWeb.PathSelectComponent,
|
||||
|
@ -108,6 +116,10 @@ defmodule LivebookWeb.HomeLive do
|
|||
{:noreply, assign(socket, path: path)}
|
||||
end
|
||||
|
||||
def handle_event("hello_livebook", %{}, socket) do
|
||||
create_session(socket, notebook: Livebook.Notebook.HelloLivebook.new())
|
||||
end
|
||||
|
||||
def handle_event("new", %{}, socket) do
|
||||
create_session(socket)
|
||||
end
|
||||
|
|
4
mix.exs
4
mix.exs
|
@ -36,7 +36,9 @@ defmodule Livebook.MixProject do
|
|||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.5.7"},
|
||||
{:phoenix_live_view, "~> 0.15.0"},
|
||||
# TODO: remove reference to the Git repo once LV 0.15.5 is released
|
||||
{:phoenix_live_view, "~> 0.15.0",
|
||||
github: "phoenixframework/phoenix_live_view", branch: "master"},
|
||||
{:floki, ">= 0.27.0", only: :test},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
|
|
8
mix.lock
8
mix.lock
|
@ -8,14 +8,14 @@
|
|||
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
|
||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
|
||||
"phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"},
|
||||
"phoenix": {:hex, :phoenix, "1.5.8", "71cfa7a9bb9a37af4df98939790642f210e35f696b935ca6d9d9c55a884621a4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35ded0a32f4836168c7ab6c33b88822eccd201bcd9492125a9bea4c54332d955"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.15.4", "86908dc9603cc81c07e84725ee42349b5325cb250c9c20d3533856ff18dbb7dc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35d78f3c35fe10a995dca5f4ab50165b7a90cbe02e23de245381558f821e9462"},
|
||||
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "ffa5d555368f2ca42d57bc573bba63e01c218226", [branch: "master"]},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
||||
"plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
|
||||
"plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.1", "5c854427528bf61d159855cedddffc0625e2228b5f30eff76d5a4de42d896ef4", [:mix], [], "hexpm", "6961c0e17febd9d0bfa89632d391d2545d2e0eb73768f5f50305a23961d8782c"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
||||
|
|
10
test/livebook/notebook/hello_livebook_test.exs
Normal file
10
test/livebook/notebook/hello_livebook_test.exs
Normal file
|
@ -0,0 +1,10 @@
|
|||
defmodule Livebook.Notebook.HelloLivebookTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Livebook.Notebook
|
||||
alias Livebook.Notebook.HelloLivebook
|
||||
|
||||
test "new/0 correctly builds a new notebook" do
|
||||
assert %Notebook{} = HelloLivebook.new()
|
||||
end
|
||||
end
|
|
@ -136,6 +136,20 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "link to introductory notebook correctly creates a new session", %{conn: conn} do
|
||||
{:ok, view, _} = live(conn, "/")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> element(~s{[aria-label="Introduction"] button})
|
||||
|> render_click()
|
||||
|
||||
assert to =~ "/sessions/"
|
||||
|
||||
{:ok, view, _} = live(conn, to)
|
||||
assert render(view) =~ "Hello Livebook"
|
||||
end
|
||||
|
||||
# Helpers
|
||||
|
||||
defp test_notebook_path(name) do
|
||||
|
|
Loading…
Reference in a new issue