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:
Jonatan Kłosko 2021-03-30 21:42:02 +02:00 committed by GitHub
parent 83a092fa38
commit 5c5b4ece26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 259 additions and 14 deletions

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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`

View 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

View file

@ -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

View file

@ -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},

View file

@ -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"},

View 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

View file

@ -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