Introduce an Explore section (#310)
* Add explore page * Move sidebar to a configurable component * Fix homepage test * Add images * Store example notebooks in files and make explore notebooks linkable * Fix tests * Raise on invalid notebook slug * Keep just the file contents in notebook info * Move notebook lookup to Explore * Exclude notebooks in progress
|
@ -1,4 +1,4 @@
|
|||
<h1><img src="https://github.com/elixir-nx/livebook/raw/main/priv/static/logo-with-text.png" alt="Livebook" width="400"></h1>
|
||||
<h1><img src="https://github.com/elixir-nx/livebook/raw/main/priv/static/images/logo-with-text.png" alt="Livebook" width="400"></h1>
|
||||
|
||||
Livebook is a web application for writing interactive and collaborative code notebooks. It features:
|
||||
|
||||
|
|
114
lib/livebook/notebook/explore.ex
Normal file
|
@ -0,0 +1,114 @@
|
|||
defmodule Livebook.Notebook.Explore.Utils do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Defines a module attribute `attr` with notebook info.
|
||||
"""
|
||||
defmacro defnotebook(attr, props) do
|
||||
quote bind_quoted: [attr: attr, props: props] do
|
||||
{path, notebook_info} = Livebook.Notebook.Explore.Utils.fetch_notebook!(attr, props)
|
||||
|
||||
@external_resource path
|
||||
|
||||
Module.put_attribute(__MODULE__, attr, notebook_info)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_notebook!(attr, props) do
|
||||
name = Atom.to_string(attr)
|
||||
path = Path.join([__DIR__, "explore", name <> ".livemd"])
|
||||
|
||||
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_info = %{
|
||||
slug: String.replace(name, "_", "-"),
|
||||
livemd: markdown,
|
||||
title: notebook.name,
|
||||
description: Keyword.fetch!(props, :description),
|
||||
image_url: Keyword.fetch!(props, :image_url)
|
||||
}
|
||||
|
||||
{path, notebook_info}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Livebook.Notebook.Explore do
|
||||
@moduledoc false
|
||||
|
||||
defmodule NotFoundError do
|
||||
@moduledoc false
|
||||
|
||||
defexception [:slug, plug_status: 404]
|
||||
|
||||
def message(%{slug: slug}) do
|
||||
"could not find an example notebook matching #{inspect(slug)}"
|
||||
end
|
||||
end
|
||||
|
||||
import Livebook.Notebook.Explore.Utils
|
||||
|
||||
defnotebook(:intro_to_livebook,
|
||||
description: "Get to know Livebook, see how it works and explore its features.",
|
||||
image_url: "/images/logo.png"
|
||||
)
|
||||
|
||||
defnotebook(:intro_to_elixir,
|
||||
description: "New to Elixir? Learn about the language and its core concepts.",
|
||||
image_url: "/images/elixir.png"
|
||||
)
|
||||
|
||||
defnotebook(:intro_to_nx,
|
||||
description:
|
||||
"Enter numerical Elixir, experience the power of multi-dimensional arrays of numbers.",
|
||||
image_url: "/images/nx.png"
|
||||
)
|
||||
|
||||
defnotebook(:intro_to_axon,
|
||||
description: "Build Neural Networks in Elixir using a high-level, composable API.",
|
||||
image_url: "/images/axon.png"
|
||||
)
|
||||
|
||||
defnotebook(:intro_to_vega_lite,
|
||||
description: "Learn how to quickly create numerous plots for your data.",
|
||||
image_url: "/images/vega_lite.png"
|
||||
)
|
||||
|
||||
@type notebook_info :: %{
|
||||
slug: String.t(),
|
||||
livemd: String.t(),
|
||||
title: String.t(),
|
||||
description: String.t(),
|
||||
image_url: String.t()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Returns a list of example notebooks with metadata.
|
||||
"""
|
||||
@spec notebook_infos() :: list(notebook_info())
|
||||
def notebook_infos() do
|
||||
[
|
||||
@intro_to_livebook
|
||||
# @intro_to_elixir, @intro_to_nx, @intro_to_axon, @intro_to_vega_lite
|
||||
]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Finds explore notebook by slug and returns the parsed data structure.
|
||||
"""
|
||||
@spec notebook_by_slug!(String.t()) :: Livebook.Notebook.t()
|
||||
def notebook_by_slug!(slug) do
|
||||
notebook_infos()
|
||||
|> Enum.find(&(&1.slug == slug))
|
||||
|> case do
|
||||
nil ->
|
||||
raise NotFoundError, slug: slug
|
||||
|
||||
notebook_info ->
|
||||
{notebook, []} = Livebook.LiveMarkdown.Import.notebook_from_markdown(notebook_info.livemd)
|
||||
notebook
|
||||
end
|
||||
end
|
||||
end
|
3
lib/livebook/notebook/explore/intro_to_axon.livemd
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Neural Networks with Axon
|
||||
|
||||
TODO: content 🐈
|
3
lib/livebook/notebook/explore/intro_to_elixir.livemd
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Introduction to Elixir
|
||||
|
||||
TODO: content 🐈
|
213
lib/livebook/notebook/explore/intro_to_livebook.livemd
Normal file
|
@ -0,0 +1,213 @@
|
|||
# Welcome to 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 for 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 cursor 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` (or Cmd + Enter on a Mac)!
|
||||
|
||||
message = "hey, grab yourself a cup of 🍵"
|
||||
```
|
||||
|
||||
Subsequent cells have access to the bindings you've 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 sidebar to reveal a list of all sections.
|
||||
As you can see, this approach helps to easily jump around the notebook,
|
||||
especially once it 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
|
||||
|
||||
By 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 "Disk" icon in the bottom-right corner
|
||||
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 have seen, 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 by clicking the "Runtime" icon on the sidebar.
|
||||
|
||||
## 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 in its context (as pointed out above). This approach makes sense if you already have
|
||||
a Mix project that 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, Elixir v1.12+ ships with
|
||||
[`Mix.install/2`](https://hexdocs.pm/mix/Mix.html#install/2) that allows you to install
|
||||
dependencies into your Elixir runtime! This approach is especially useful when sharing notebooks
|
||||
because everyone will be able to get the same dependencies. Let's try this out:
|
||||
|
||||
**Note:** compiling dependencies may use a reasonable amount of memory. If you are
|
||||
hosting Livebook, make sure you have enough memory allocated to the Livebook
|
||||
instance, otherwise the command below will fail.
|
||||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:jason, "~> 1.2"}
|
||||
])
|
||||
```
|
||||
|
||||
```elixir
|
||||
%{elixir: "is awesome"}
|
||||
|> 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.
|
||||
|
||||
## Running tests
|
||||
|
||||
It is also possible to run tests directly from your notebooks.
|
||||
The key is to disable `ExUnit`'s autorun feature and then explicitly
|
||||
run the test suite after all test cases have been defined:
|
||||
|
||||
```elixir
|
||||
ExUnit.start(autorun: false)
|
||||
|
||||
defmodule MyTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "it works" do
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
ExUnit.run()
|
||||
```
|
||||
|
||||
## Math
|
||||
|
||||
Livebook uses $\\TeX$ syntax for math.
|
||||
It supports both inline math like $e^{\\pi i} + 1 = 0$, as well as display math:
|
||||
|
||||
$$
|
||||
S(x) = \\frac{1}{1 + e^{-x}} = \\frac{e^{x}}{e^{x} + 1}
|
||||
$$
|
||||
|
||||
You can explore all supported expressions [here](https://katex.org/docs/supported.html).
|
||||
|
||||
## 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 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! 🚢
|
3
lib/livebook/notebook/explore/intro_to_nx.livemd
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Introduction to Nx
|
||||
|
||||
TODO: content 🐈
|
3
lib/livebook/notebook/explore/intro_to_vega_lite.livemd
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Plotting with VegaLite and Kino
|
||||
|
||||
TODO: content 🐈
|
|
@ -1,225 +0,0 @@
|
|||
defmodule Livebook.Notebook.Welcome do
|
||||
livemd = ~s'''
|
||||
# Welcome to 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 for 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 cursor 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` (or Cmd + Enter on a Mac)!
|
||||
|
||||
message = "hey, grab yourself a cup of 🍵"
|
||||
```
|
||||
|
||||
Subsequent cells have access to the bindings you've 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 sidebar to reveal a list of all sections.
|
||||
As you can see, this approach helps to easily jump around the notebook,
|
||||
especially once it 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
|
||||
|
||||
By 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 "Disk" icon in the bottom-right corner
|
||||
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 have seen, 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 by clicking the "Runtime" icon on the sidebar.
|
||||
|
||||
## 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 in its context (as pointed out above). This approach makes sense if you already have
|
||||
a Mix project that 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, Elixir v1.12+ ships with
|
||||
[`Mix.install/2`](https://hexdocs.pm/mix/Mix.html#install/2) that allows you to install
|
||||
dependencies into your Elixir runtime! This approach is especially useful when sharing notebooks
|
||||
because everyone will be able to get the same dependencies. Let's try this out:
|
||||
|
||||
**Note:** compiling dependencies may use a reasonable amount of memory. If you are
|
||||
hosting Livebook, make sure you have enough memory allocated to the Livebook
|
||||
instance, otherwise the command below will fail.
|
||||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:jason, "~> 1.2"}
|
||||
])
|
||||
```
|
||||
|
||||
```elixir
|
||||
%{elixir: "is awesome"}
|
||||
|> 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.
|
||||
|
||||
## Running tests
|
||||
|
||||
It is also possible to run tests directly from your notebooks.
|
||||
The key is to disable `ExUnit`'s autorun feature and then explicitly
|
||||
run the test suite after all test cases have been defined:
|
||||
|
||||
```elixir
|
||||
ExUnit.start(autorun: false)
|
||||
|
||||
defmodule MyTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "it works" do
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
ExUnit.run()
|
||||
```
|
||||
|
||||
## Math
|
||||
|
||||
Livebook uses $\TeX$ syntax for math.
|
||||
It supports both inline math like $e^{\\pi i} + 1 = 0$, as well as display math:
|
||||
|
||||
$$
|
||||
S(x) = \\frac{1}{1 + e^{-x}} = \\frac{e^{x}}{e^{x} + 1}
|
||||
$$
|
||||
|
||||
You can explore all supported expressions [here](https://katex.org/docs/supported.html).
|
||||
|
||||
## 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 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
|
108
lib/livebook_web/live/explore_live.ex
Normal file
|
@ -0,0 +1,108 @@
|
|||
defmodule LivebookWeb.ExploreLive do
|
||||
use LivebookWeb, :live_view
|
||||
|
||||
import LivebookWeb.UserHelpers
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
alias Livebook.Notebook.Explore
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"current_user_id" => current_user_id}, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}")
|
||||
end
|
||||
|
||||
current_user = build_current_user(current_user_id, socket)
|
||||
|
||||
[lead_notebook_info | notebook_infos] = Explore.notebook_infos()
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
current_user: current_user,
|
||||
lead_notebook_info: lead_notebook_info,
|
||||
notebook_infos: notebook_infos
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="flex flex-grow h-full">
|
||||
<%= live_component @socket, LivebookWeb.SidebarComponent,
|
||||
id: :sidebar,
|
||||
items: [
|
||||
%{type: :logo},
|
||||
%{type: :break},
|
||||
%{type: :user, current_user: @current_user, path: Routes.explore_path(@socket, :user)}
|
||||
] %>
|
||||
<div class="flex-grow px-6 py-8 overflow-y-auto">
|
||||
<div class="max-w-screen-md w-full mx-auto px-4 pb-8 space-y-8">
|
||||
<div>
|
||||
<div class="relative">
|
||||
<%= live_patch to: Routes.home_path(@socket, :page),
|
||||
class: "hidden md:block absolute top-[50%] left-[-12px] transform -translate-y-1/2 -translate-x-full" do %>
|
||||
<%= remix_icon("arrow-left-line", class: "text-2xl align-middle") %>
|
||||
<% end %>
|
||||
<h1 class="text-3xl text-gray-800 font-semibold">
|
||||
Explore
|
||||
</h1>
|
||||
</div>
|
||||
<p class="mt-4 text-gray-700">
|
||||
Check out a number of examples showcasing various parts of the Elixir ecosystem.
|
||||
Click on any notebook you like and start playing around with it!
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-8 bg-gray-900 rounded-2xl flex space-x-4 shadow-xl">
|
||||
<div class="self-end max-w-sm">
|
||||
<h3 class="text-xl text-gray-50 font-semibold">
|
||||
<%= @lead_notebook_info.title %>
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-300">
|
||||
<%= @lead_notebook_info.description %>
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<%= live_patch "Let's go", to: Routes.explore_path(@socket, :notebook, @lead_notebook_info.slug),
|
||||
class: "button button-blue" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow hidden md:flex flex items-center justify-center">
|
||||
<img src="<%= @lead_notebook_info.image_url %>" height="120" width="120" alt="livebook" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<%= for {info, idx} <- Enum.with_index(@notebook_infos) do %>
|
||||
<%= live_component @socket, LivebookWeb.NotebookCardComponent,
|
||||
id: "notebook-card-#{idx}",
|
||||
notebook_info: info %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @live_action == :user do %>
|
||||
<%= live_modal @socket, LivebookWeb.UserComponent,
|
||||
id: :user_modal,
|
||||
modal_class: "w-full max-w-sm",
|
||||
user: @current_user,
|
||||
return_to: Routes.explore_path(@socket, :page) %>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"slug" => slug}, _url, socket) do
|
||||
notebook = Explore.notebook_by_slug!(slug)
|
||||
{:noreply, create_session(socket, notebook: notebook)}
|
||||
end
|
||||
|
||||
def handle_params(_params, _url, socket), do: {:noreply, socket}
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
{:user_change, %{id: id} = user},
|
||||
%{assigns: %{current_user: %{id: id}}} = socket
|
||||
) do
|
||||
{:noreply, assign(socket, :current_user, user)}
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
use LivebookWeb, :live_view
|
||||
|
||||
import LivebookWeb.UserHelpers
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
alias Livebook.{SessionSupervisor, Session, LiveMarkdown, Notebook}
|
||||
|
||||
|
@ -14,12 +15,14 @@ defmodule LivebookWeb.HomeLive do
|
|||
|
||||
current_user = build_current_user(current_user_id, socket)
|
||||
session_summaries = sort_session_summaries(SessionSupervisor.get_session_summaries())
|
||||
notebook_infos = Notebook.Explore.notebook_infos() |> Enum.take(3)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
current_user: current_user,
|
||||
path: default_path(),
|
||||
session_summaries: session_summaries
|
||||
session_summaries: session_summaries,
|
||||
notebook_infos: notebook_infos
|
||||
)}
|
||||
end
|
||||
|
||||
|
@ -27,28 +30,19 @@ defmodule LivebookWeb.HomeLive do
|
|||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="flex flex-grow h-full">
|
||||
<div class="w-16 flex flex-col items-center space-y-5 px-3 py-7 bg-gray-900">
|
||||
<div class="flex-grow"></div>
|
||||
<span class="tooltip right distant" aria-label="User profile">
|
||||
<%= live_patch to: Routes.home_path(@socket, :user),
|
||||
class: "text-gray-400 rounded-xl h-8 w-8 flex items-center justify-center" do %>
|
||||
<%= render_user_avatar(@current_user, class: "h-full w-full", text_class: "text-xs") %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
<%= live_component @socket, LivebookWeb.SidebarComponent,
|
||||
id: :sidebar,
|
||||
items: [
|
||||
%{type: :break},
|
||||
%{type: :user, current_user: @current_user, path: Routes.home_path(@socket, :user)}
|
||||
] %>
|
||||
<div class="flex-grow px-6 py-8 overflow-y-auto">
|
||||
<div class="max-w-screen-lg w-full mx-auto p-4 pt-0 pb-8 flex flex-col items-center space-y-4">
|
||||
<div class="w-full flex flex-col space-y-2 items-center sm:flex-row sm:space-y-0 sm:justify-between sm:pb-4 pb-8 border-b border-gray-200">
|
||||
<div class="max-w-screen-lg w-full mx-auto px-4 pb-8 space-y-4">
|
||||
<div class="flex flex-col space-y-2 items-center sm:flex-row sm:space-y-0 sm:justify-between sm:pb-4 pb-8 border-b border-gray-200">
|
||||
<div class="text-2xl text-gray-800 font-semibold">
|
||||
<img src="/logo-with-text.png" class="h-[50px]" alt="Livebook" />
|
||||
<img src="/images/logo-with-text.png" class="h-[50px]" alt="Livebook" />
|
||||
</div>
|
||||
<div class="flex space-x-2 pt-2">
|
||||
<span class="tooltip top" aria-label="Introduction">
|
||||
<button class="button button-outlined-gray button-square-icon"
|
||||
phx-click="open_welcome">
|
||||
<%= remix_icon("compass-line") %>
|
||||
</button>
|
||||
</span>
|
||||
<%= live_patch to: Routes.home_path(@socket, :import, "url"),
|
||||
class: "button button-outlined-gray whitespace-nowrap" do %>
|
||||
Import
|
||||
|
@ -59,7 +53,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-80">
|
||||
<div class="h-80">
|
||||
<%= live_component @socket, LivebookWeb.PathSelectComponent,
|
||||
id: "path_select",
|
||||
path: @path,
|
||||
|
@ -92,10 +86,29 @@ defmodule LivebookWeb.HomeLive do
|
|||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="w-full py-12">
|
||||
<h3 class="text-xl font-semibold text-gray-800 mb-5">
|
||||
<div class="py-12">
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-gray-800">
|
||||
Explore
|
||||
</h2>
|
||||
<%= live_redirect to: Routes.explore_path(@socket, :page),
|
||||
class: "flex items-center text-blue-600" do %>
|
||||
<span class="font-semibold">See all</span>
|
||||
<%= remix_icon("arrow-right-line", class: "align-middle ml-1") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<%= for {info, idx} <- Enum.with_index(@notebook_infos) do %>
|
||||
<%= live_component @socket, LivebookWeb.NotebookCardComponent,
|
||||
id: "notebook-card-#{idx}",
|
||||
notebook_info: info %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-12">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-800">
|
||||
Running sessions
|
||||
</h3>
|
||||
</h2>
|
||||
<%= if @session_summaries == [] do %>
|
||||
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
|
||||
<div>
|
||||
|
@ -160,12 +173,8 @@ defmodule LivebookWeb.HomeLive do
|
|||
{:noreply, assign(socket, path: path)}
|
||||
end
|
||||
|
||||
def handle_event("open_welcome", %{}, socket) do
|
||||
create_session(socket, notebook: Livebook.Notebook.Welcome.new())
|
||||
end
|
||||
|
||||
def handle_event("new", %{}, socket) do
|
||||
create_session(socket)
|
||||
{:noreply, create_session(socket)}
|
||||
end
|
||||
|
||||
def handle_event("fork", %{}, socket) do
|
||||
|
@ -173,20 +182,20 @@ defmodule LivebookWeb.HomeLive do
|
|||
socket = put_import_flash_messages(socket, messages)
|
||||
notebook = Notebook.forked(notebook)
|
||||
images_dir = Session.images_dir_for_notebook(socket.assigns.path)
|
||||
create_session(socket, notebook: notebook, copy_images_from: images_dir)
|
||||
{:noreply, create_session(socket, notebook: notebook, copy_images_from: images_dir)}
|
||||
end
|
||||
|
||||
def handle_event("open", %{}, socket) do
|
||||
{notebook, messages} = import_notebook(socket.assigns.path)
|
||||
socket = put_import_flash_messages(socket, messages)
|
||||
create_session(socket, notebook: notebook, path: socket.assigns.path)
|
||||
{:noreply, create_session(socket, notebook: notebook, path: socket.assigns.path)}
|
||||
end
|
||||
|
||||
def handle_event("fork_session", %{"id" => session_id}, socket) do
|
||||
data = Session.get_data(session_id)
|
||||
notebook = Notebook.forked(data.notebook)
|
||||
%{images_dir: images_dir} = Session.get_summary(session_id)
|
||||
create_session(socket, notebook: notebook, copy_images_from: images_dir)
|
||||
{:noreply, create_session(socket, notebook: notebook, copy_images_from: images_dir)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -204,7 +213,7 @@ defmodule LivebookWeb.HomeLive do
|
|||
def handle_info({:import_content, content}, socket) do
|
||||
{notebook, messages} = Livebook.LiveMarkdown.Import.notebook_from_markdown(content)
|
||||
socket = put_import_flash_messages(socket, messages)
|
||||
create_session(socket, notebook: notebook)
|
||||
{:noreply, create_session(socket, notebook: notebook)}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
|
@ -246,16 +255,6 @@ defmodule LivebookWeb.HomeLive do
|
|||
end
|
||||
end
|
||||
|
||||
defp create_session(socket, opts \\ []) do
|
||||
case SessionSupervisor.create_session(opts) do
|
||||
{:ok, id} ->
|
||||
{:noreply, push_redirect(socket, to: Routes.session_path(socket, :page, id))}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to create a notebook: #{reason}")}
|
||||
end
|
||||
end
|
||||
|
||||
defp import_notebook(path) do
|
||||
content = File.read!(path)
|
||||
LiveMarkdown.Import.notebook_from_markdown(content)
|
||||
|
|
22
lib/livebook_web/live/notebook_card_component.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
defmodule LivebookWeb.NotebookCardComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-center p-6 border-2 border-gray-100 rounded-t-2xl h-[150px]">
|
||||
<img src="<%= @notebook_info.image_url %>" class="max-h-full max-w-[75%]" />
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-gray-100 rounded-b-2xl flex-grow">
|
||||
<%= live_redirect @notebook_info.title,
|
||||
to: Routes.explore_path(@socket, :notebook, @notebook_info.slug),
|
||||
class: "text-gray-800 font-semibold cursor-pointer" %>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
<%= @notebook_info.description %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
21
lib/livebook_web/live/session_helpers.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule LivebookWeb.SessionHelpers do
|
||||
import Phoenix.LiveView
|
||||
alias LivebookWeb.Router.Helpers, as: Routes
|
||||
|
||||
@doc """
|
||||
Creates a new session, redirects on success,
|
||||
puts an error flash message on failure.
|
||||
|
||||
Accepts the same options as `Livebook.SessionSupervisor.create_session/1`.
|
||||
"""
|
||||
@spec create_session(Phoenix.LiveView.Socket.t(), keyword()) :: Phoenix.LiveView.Socket.t()
|
||||
def create_session(socket, opts \\ []) do
|
||||
case Livebook.SessionSupervisor.create_session(opts) do
|
||||
{:ok, id} ->
|
||||
push_redirect(socket, to: Routes.session_path(socket, :page, id))
|
||||
|
||||
{:error, reason} ->
|
||||
put_flash(socket, :error, "Failed to create session: #{reason}")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -71,40 +71,41 @@ defmodule LivebookWeb.SessionLive do
|
|||
id="session"
|
||||
data-element="session"
|
||||
phx-hook="Session">
|
||||
<div class="w-16 flex flex-col items-center space-y-5 px-3 py-7 bg-gray-900">
|
||||
<%= live_patch to: Routes.home_path(@socket, :page) do %>
|
||||
<img src="/logo.png" height="40" width="40" alt="livebook" />
|
||||
<% end %>
|
||||
<span class="tooltip right distant" aria-label="Sections (ss)">
|
||||
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center" data-element="sections-list-toggle">
|
||||
<%= remix_icon("booklet-fill") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip right distant" aria-label="Connected users (su)">
|
||||
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center" data-element="clients-list-toggle">
|
||||
<%= remix_icon("group-fill") %>
|
||||
</button>
|
||||
</span>
|
||||
<span class="tooltip right distant" aria-label="Runtime settings (sr)">
|
||||
<%= live_patch to: Routes.session_path(@socket, :runtime_settings, @session_id),
|
||||
class: "text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center #{if(@live_action == :runtime_settings, do: "text-gray-50 bg-gray-700")}" do %>
|
||||
<%= remix_icon("cpu-line", class: "text-2xl") %>
|
||||
<% end %>
|
||||
</span>
|
||||
<div class="flex-grow"></div>
|
||||
<span class="tooltip right distant" aria-label="Keyboard shortcuts (?)">
|
||||
<%= live_patch to: Routes.session_path(@socket, :shortcuts, @session_id),
|
||||
class: "text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center #{if(@live_action == :shortcuts, do: "text-gray-50 bg-gray-700")}" do %>
|
||||
<%= remix_icon("keyboard-box-fill", class: "text-2xl") %>
|
||||
<% end %>
|
||||
</span>
|
||||
<span class="tooltip right distant" aria-label="User profile">
|
||||
<%= live_patch to: Routes.session_path(@socket, :user, @session_id),
|
||||
class: "text-gray-400 rounded-xl h-8 w-8 flex items-center justify-center" do %>
|
||||
<%= render_user_avatar(@current_user, class: "h-full w-full", text_class: "text-xs") %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
<%= live_component @socket, LivebookWeb.SidebarComponent,
|
||||
id: :sidebar,
|
||||
items: [
|
||||
%{type: :logo},
|
||||
%{
|
||||
type: :button,
|
||||
data_element: "sections-list-toggle",
|
||||
icon: "booklet-fill",
|
||||
label: "Sections (ss)",
|
||||
active: false
|
||||
},
|
||||
%{
|
||||
type: :button,
|
||||
data_element: "clients-list-toggle",
|
||||
icon: "group-fill",
|
||||
label: "Connected users (su)",
|
||||
active: false
|
||||
},
|
||||
%{
|
||||
type: :link,
|
||||
icon: "cpu-line",
|
||||
path: Routes.session_path(@socket, :runtime_settings, @session_id),
|
||||
label: "Runtime settings (sr)",
|
||||
active: @live_action == :runtime_settings
|
||||
},
|
||||
%{type: :break},
|
||||
%{
|
||||
type: :link,
|
||||
icon: "keyboard-box-fill",
|
||||
path: Routes.session_path(@socket, :shortcuts, @session_id),
|
||||
label: "Keyboard shortcuts (?)",
|
||||
active: @live_action == :shortcuts
|
||||
},
|
||||
%{type: :user, current_user: @current_user, path: Routes.session_path(@socket, :user, @session_id)}
|
||||
] %>
|
||||
<div class="flex flex-col h-full w-full max-w-xs absolute z-30 top-0 left-[64px] shadow-xl md:static md:shadow-none overflow-y-auto bg-gray-50 border-r border-gray-100 px-6 py-10"
|
||||
data-element="side-panel">
|
||||
<div data-element="sections-list">
|
||||
|
|
77
lib/livebook_web/live/sidebar_component.ex
Normal file
|
@ -0,0 +1,77 @@
|
|||
defmodule LivebookWeb.SidebarComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import LivebookWeb.UserHelpers
|
||||
|
||||
# ## Attributes
|
||||
#
|
||||
# * `:items` - a list of sidebar items
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div class="w-16 flex flex-col items-center space-y-5 px-3 py-7 bg-gray-900">
|
||||
<%= for item <- @items do %>
|
||||
<%= render_item(@socket, item) %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(socket, %{type: :logo} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<%= live_patch to: Routes.home_path(socket, :page) do %>
|
||||
<img src="/images/logo.png" height="40" width="40" alt="livebook" />
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :button} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<span class="tooltip right distant" aria-label="<%= item.label %>">
|
||||
<button class="text-2xl text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center"
|
||||
data-element="<%= item.data_element %>">
|
||||
<%= remix_icon(item.icon) %>
|
||||
</button>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :link} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<span class="tooltip right distant" aria-label="<%= item.label %>">
|
||||
<%= live_patch to: item.path,
|
||||
class: "text-gray-400 hover:text-gray-50 focus:text-gray-50 rounded-xl h-10 w-10 flex items-center justify-center #{if(item.active, do: "text-gray-50 bg-gray-700")}" do %>
|
||||
<%= remix_icon(item.icon, class: "text-2xl") %>
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :break}) do
|
||||
assigns = %{}
|
||||
|
||||
~L"""
|
||||
<div class="flex-grow"></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_item(_socket, %{type: :user} = item) do
|
||||
assigns = %{item: item}
|
||||
|
||||
~L"""
|
||||
<span class="tooltip right distant" aria-label="User profile">
|
||||
<%= live_patch to: item.path,
|
||||
class: "text-gray-400 rounded-xl h-8 w-8 flex items-center justify-center" do %>
|
||||
<%= render_user_avatar(item.current_user, class: "h-full w-full", text_class: "text-xs") %>
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -23,6 +23,9 @@ defmodule LivebookWeb.Router do
|
|||
live "/home/user-profile", HomeLive, :user
|
||||
live "/home/import/:tab", HomeLive, :import
|
||||
live "/home/sessions/:session_id/close", HomeLive, :close_session
|
||||
live "/explore", ExploreLive, :page
|
||||
live "/explore/user-profile", ExploreLive, :user
|
||||
live "/explore/notebooks/:slug", ExploreLive, :notebook
|
||||
live "/sessions/:id", SessionLive, :page
|
||||
live "/sessions/:id/user-profile", SessionLive, :user
|
||||
live "/sessions/:id/shortcuts", SessionLive, :shortcuts
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="h-screen flex items-center justify-center bg-gray-900">
|
||||
<div class="flex flex-col space-y-4 items-center">
|
||||
<a href="/">
|
||||
<img src="/logo.png" height="128" width="128" alt="livebook" />
|
||||
<img src="/images/logo.png" height="128" width="128" alt="livebook" />
|
||||
</a>
|
||||
<div class="text-2xl text-gray-50">
|
||||
Authentication required
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="h-screen flex items-center justify-center bg-gray-900">
|
||||
<div class="flex flex-col space-y-4 items-center">
|
||||
<a href="/">
|
||||
<img src="/logo.png" height="128" width="128" alt="livebook" />
|
||||
<img src="/images/logo.png" height="128" width="128" alt="livebook" />
|
||||
</a>
|
||||
<div class="text-2xl text-gray-50">
|
||||
Authentication required
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="h-screen flex items-center justify-center bg-gray-900">
|
||||
<div class="flex flex-col space-y-4 items-center">
|
||||
<a href="/">
|
||||
<img src="/logo.png" height="128" width="128" alt="livebook" />
|
||||
<img src="/images/logo.png" height="128" width="128" alt="livebook" />
|
||||
</a>
|
||||
<div class="text-2xl text-gray-50">
|
||||
No Numbats here!
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="h-screen flex items-center justify-center bg-gray-900">
|
||||
<div class="flex flex-col space-y-4 items-center">
|
||||
<a href="/">
|
||||
<img src="/logo.png" height="128" width="128" alt="livebook" />
|
||||
<img src="/images/logo.png" height="128" width="128" alt="livebook" />
|
||||
</a>
|
||||
<div class="text-2xl text-gray-50">
|
||||
Something went wrong.
|
||||
|
|
BIN
priv/static/images/axon.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
priv/static/images/elixir.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
priv/static/images/nx.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
priv/static/images/vega_lite.png
Normal file
After Width: | Height: | Size: 12 KiB |
18
test/livebook/explore_test.exs
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Livebook.ExploreTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Livebook.Notebook
|
||||
alias Livebook.Notebook.Explore
|
||||
|
||||
describe "notebook_by_slug!/1" do
|
||||
test "returns notebook structure if found" do
|
||||
assert %Notebook{} = Explore.notebook_by_slug!("intro-to-livebook")
|
||||
end
|
||||
|
||||
test "raises an error if no matching notebook if found" do
|
||||
assert_raise Explore.NotFoundError, fn ->
|
||||
Explore.notebook_by_slug!("invalid-slug")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
defmodule Livebook.Notebook.WelcomeTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Livebook.Notebook
|
||||
alias Livebook.Notebook.Welcome
|
||||
|
||||
test "new/0 correctly builds a new notebook" do
|
||||
assert %Notebook{} = Welcome.new()
|
||||
end
|
||||
end
|
19
test/livebook_web/live/explore_live_test.exs
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule LivebookWeb.ExploreLiveTest do
|
||||
use LivebookWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
test "link to introductory notebook correctly creates a new session", %{conn: conn} do
|
||||
{:ok, view, _} = live(conn, "/explore")
|
||||
|
||||
assert {:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> element(~s{a}, "Let's go")
|
||||
|> render_click()
|
||||
|
||||
assert to =~ "/sessions/"
|
||||
|
||||
{:ok, view, _} = live(conn, to)
|
||||
assert render(view) =~ "Welcome to Livebook"
|
||||
end
|
||||
end
|
|
@ -163,8 +163,9 @@ defmodule LivebookWeb.HomeLiveTest do
|
|||
|
||||
assert {:error, {:live_redirect, %{to: to}}} =
|
||||
view
|
||||
|> element(~s{[aria-label="Introduction"] button})
|
||||
|> element(~s{a}, "Welcome to Livebook")
|
||||
|> render_click()
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert to =~ "/sessions/"
|
||||
|
||||
|
|