mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-10 06:01:44 +08:00
Deploy app notebook
This commit is contained in:
parent
87902ca34e
commit
4320fb1f6b
3 changed files with 179 additions and 72 deletions
|
|
@ -8,6 +8,8 @@
|
||||||
.ri-livebook-sections,
|
.ri-livebook-sections,
|
||||||
.ri-livebook-runtime,
|
.ri-livebook-runtime,
|
||||||
.ri-livebook-shortcuts,
|
.ri-livebook-shortcuts,
|
||||||
|
.ri-livebook-secrets,
|
||||||
|
.ri-livebook-deploy,
|
||||||
.ri-livebook-save {
|
.ri-livebook-save {
|
||||||
@apply text-xl align-middle;
|
@apply text-xl align-middle;
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +18,14 @@
|
||||||
content: "\eade";
|
content: "\eade";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ri-livebook-secrets:before {
|
||||||
|
content: "\eed0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.ri-livebook-deploy:before {
|
||||||
|
content: "\f096";
|
||||||
|
}
|
||||||
|
|
||||||
.ri-livebook-runtime:before {
|
.ri-livebook-runtime:before {
|
||||||
content: "\ebf0";
|
content: "\ebf0";
|
||||||
}
|
}
|
||||||
|
|
@ -27,3 +37,7 @@
|
||||||
.ri-livebook-save:before {
|
.ri-livebook-save:before {
|
||||||
content: "\f0b3";
|
content: "\f0b3";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ri-livebook-deploy:before {
|
||||||
|
content: "\f0b3";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,48 +2,33 @@
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
Mix.install([
|
Mix.install([
|
||||||
{:kino, "~> 0.8.0"}
|
{:kino, "~> 0.8.1", github: "livebook-dev/kino"}
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
In this notebook, we will build a chat application using
|
In this notebook, we will build and deplay a chat application.
|
||||||
[`kino`](https://github.com/livebook-dev/kino). First, let's
|
To do so, we will use Livebook's companion library called
|
||||||
have a look at the building blocks that we will need!
|
[`kino`](https://github.com/livebook-dev/kino).
|
||||||
|
|
||||||
|
In a nutshell, Kino is a library that you install as part
|
||||||
|
of your notebooks to make your notebooks interactive.
|
||||||
|
Kino comes from the Greek prefix of the same name and it
|
||||||
|
stands for "motion". And, as you learn the library, it
|
||||||
|
will become clear that this is precisely what it brings to
|
||||||
|
our notebooks.
|
||||||
|
|
||||||
|
There is many functionality in the Kino library: it can render
|
||||||
|
Markdown, animate frames, display tables, manage inputs, and
|
||||||
|
more. For building notebook applications, we rely on two main
|
||||||
|
building blocks: `Kino.Control` and `Kino.Frame`.
|
||||||
|
|
||||||
|
You can see `kino` listed as a dependency above, so let's run
|
||||||
|
the setup cell and get started.
|
||||||
|
|
||||||
## Kino.Control
|
## Kino.Control
|
||||||
|
|
||||||
In our [introduction to Kino](/learn/notebooks/intro-to-kino),
|
|
||||||
we learned about inputs and several different outputs, such as
|
|
||||||
tables, frames, and more. In particular, we learned how to use
|
|
||||||
inputs to capture values directly into our notebooks:
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
name = Kino.Input.text("Your name")
|
|
||||||
```
|
|
||||||
|
|
||||||
and use them to print something back:
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
IO.puts("Hello, #{Kino.Input.read(name)}!")
|
|
||||||
```
|
|
||||||
|
|
||||||
Inputs have one special power: they are shared across all users
|
|
||||||
accessing the notebook. For example, if you copy and paste the
|
|
||||||
URL of this notebook into another tab, the input will share the
|
|
||||||
same value. This plays into Livebook's strengths of being an
|
|
||||||
interactive and collaborative tool.
|
|
||||||
|
|
||||||
Sometimes, however, you don't want the input to be shared.
|
|
||||||
You want each different user to get their own inputs and perform
|
|
||||||
individual actions. That's exactly how
|
|
||||||
[`Kino.Control`](https://hexdocs.pm/kino/Kino.Control.html) works.
|
|
||||||
Each control is specific to each user on the page. You then receive
|
|
||||||
each user interaction as a message.
|
|
||||||
|
|
||||||
## The button control
|
|
||||||
|
|
||||||
The simplest control is `Kino.Control.button/1`. Let's give it a try:
|
The simplest control is `Kino.Control.button/1`. Let's give it a try:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
|
|
@ -73,33 +58,25 @@ Let's see a different one in the next example.
|
||||||
|
|
||||||
<!-- livebook:{"branch_parent_index":0} -->
|
<!-- livebook:{"branch_parent_index":0} -->
|
||||||
|
|
||||||
## The form control
|
## Enumerating controls
|
||||||
|
|
||||||
Whenever we want to submit multiple inputs at once, we can use
|
All Kino controls are enumerable. This means we can treat them
|
||||||
`Kino.Control.form/2`.
|
as a collection, an infinite collection in this case, and consume
|
||||||
|
their events. Let's define another button:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
inputs = [
|
click_me_again = Kino.Control.button("Click me again!")
|
||||||
first_name: Kino.Input.text("First name"),
|
|
||||||
last_name: Kino.Input.text("Last name")
|
|
||||||
]
|
|
||||||
|
|
||||||
form = Kino.Control.form(inputs, submit: "Greet")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Execute the cell above and you will see a form rendered.
|
And now let's consume it:
|
||||||
You can now fill in the form and press the submit button.
|
|
||||||
Each submission will trigger a new event. Let's consume
|
|
||||||
them as a stream. Elixir streams are lazy collections that
|
|
||||||
are consumed as they happen:
|
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
for event <- Kino.Control.stream(form) do
|
for event <- click_me_again do
|
||||||
IO.inspect(event)
|
IO.inspect(event)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, as you submit the form, you should see a new event
|
Now, as you submit the button, you should see a new event
|
||||||
printed. However, there is a downside: we are now stuck
|
printed. However, there is a downside: we are now stuck
|
||||||
inside this infinite loop of events. Luckily, we started
|
inside this infinite loop of events. Luckily, we started
|
||||||
this particular section as a branched section, which means
|
this particular section as a branched section, which means
|
||||||
|
|
@ -108,20 +85,86 @@ is something you should keep in mind in the future. You
|
||||||
can also stop it by pressing the "Stop" button above the
|
can also stop it by pressing the "Stop" button above the
|
||||||
Code cell.
|
Code cell.
|
||||||
|
|
||||||
<!-- livebook:{"branch_parent_index":0} -->
|
## Kino.Frame and animations
|
||||||
|
|
||||||
## The chat application
|
`Kino.Frame` allows us to render an empty frame and update it
|
||||||
|
as we progress. Let's render an empty frame:
|
||||||
We are now equipped with all knowledge necessary to build
|
|
||||||
our chat application. First, we will need a frame. Every
|
|
||||||
time a new message is received, we will append it to the
|
|
||||||
frame:
|
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
frame = Kino.Frame.new()
|
frame = Kino.Frame.new()
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we need a form with the user name and their message:
|
Now, let's render a random number between 1 and 100 directly
|
||||||
|
in the frame:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Kino.Frame.render(frame, "Got: #{Enum.random(1..100)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice how every time you reevaluate the cell above it updates
|
||||||
|
the frame. You can also use `Kino.Frame.append/2` to append to
|
||||||
|
the frame:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Kino.Frame.append(frame, "Got: #{Enum.random(1..100)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Appending multiple times will always add new contents. The content
|
||||||
|
can be reset by calling `Kino.Frame.render/2` or `Kino.Frame.clear/1`.
|
||||||
|
|
||||||
|
One important thing about frames is that they are shared across
|
||||||
|
all users. If you open up this same notebook in another tab and
|
||||||
|
execute the cell above, it will append the new result on all tabs.
|
||||||
|
This means we can use frames for building collaborative applications
|
||||||
|
within Livebook itself!
|
||||||
|
|
||||||
|
You can combine this with loops to dynamically add contents
|
||||||
|
or animate your notebooks. In fact, there is a convenience function
|
||||||
|
called `Kino.animate/2` to be used exactly for this purpose:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
Kino.animate(100, fn i ->
|
||||||
|
Kino.Markdown.new("**Iteration: `#{i}`**")
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above example creates a new frame behind the scenes and renders
|
||||||
|
new Markdown output every 100ms. You can use the same approach to
|
||||||
|
render regular output or images too!
|
||||||
|
|
||||||
|
There's also `Kino.animate/3`, in case you need to accumulate state or
|
||||||
|
halt the animation at certain point. Both `animate` functions allow
|
||||||
|
an enumerable to be given, which means we can animate a frame based
|
||||||
|
on the events of a control:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
button = Kino.Control.button("Click") |> Kino.render()
|
||||||
|
|
||||||
|
Kino.animate(button, 0, fn _event, counter ->
|
||||||
|
new_counter = counter + 1
|
||||||
|
md = Kino.Markdown.new("**Clicks: `#{new_counter}`**")
|
||||||
|
{:cont, md, new_counter}
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
One of the benefits of using `animate` to consume events is
|
||||||
|
that it does not block the notebook execution and we can
|
||||||
|
proceed as usual.
|
||||||
|
|
||||||
|
## Putting it all together
|
||||||
|
|
||||||
|
We have learned about controls and frames, which means now we
|
||||||
|
are ready to build our chat application.
|
||||||
|
|
||||||
|
The first step is to define the frame we want to render our
|
||||||
|
chat messages:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
frame = Kino.Frame.new()
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we will use a new control, called forms, to render and submit
|
||||||
|
multiple inputs at once:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
inputs = [
|
inputs = [
|
||||||
|
|
@ -132,22 +175,72 @@ inputs = [
|
||||||
form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:message])
|
form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:message])
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice we used a new option, called `:reset_on_submit`,
|
Now, every time the form is submitted, we want to append
|
||||||
that automatically clears the input once submitted.
|
the message to a frame. We have learned about `Kino.animate/3`,
|
||||||
Finally, let's stream the form events and post each
|
that receives control events, but unfortunately it only updates
|
||||||
message to the frame:
|
frames in place, while we want to always append content.
|
||||||
|
We could accumulate the content ourselves and always re-render
|
||||||
|
it all on the frame, but that sounds a bit wasteful.
|
||||||
|
|
||||||
|
Luckily, Kino also provides a function called `listen`. `listen`
|
||||||
|
also consumes events from controls and enumerables, but it does
|
||||||
|
not assume we want to render a frame, ultimately giving us more
|
||||||
|
control. Let's give it a try:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
for %{data: %{name: name, message: message}} <- Kino.Control.stream(form) do
|
Kino.listen(form, fn %{data: %{name: name, message: message}, origin: origin} ->
|
||||||
content = Kino.Markdown.new("**#{name}**: #{message}")
|
if name != "" and message != "" do
|
||||||
Kino.Frame.append(frame, content)
|
content = Kino.Markdown.new("**#{name}**: #{message}")
|
||||||
end
|
Kino.Frame.append(frame, content)
|
||||||
|
else
|
||||||
|
content = Kino.Markdown.new("_ERROR! You need a name and message to submit..._")
|
||||||
|
Kino.Frame.append(frame, content, to: origin)
|
||||||
|
end
|
||||||
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
Execute the cell above and your chat app should be
|
Execute the cell above and your chat app should be
|
||||||
fully operational. Open up this same notebook across
|
fully operational. `listen` receives the form events,
|
||||||
on different tabs and each different user can post
|
which includes the value of each input. If a name and
|
||||||
their messages.
|
message have been given, we append it to the frame.
|
||||||
|
If one of them is missing, we append an error message
|
||||||
|
to the frame with the `to: origin` option. This means
|
||||||
|
that particular message will be seen only by the user
|
||||||
|
who submitted the form, instead of everyone.
|
||||||
|
|
||||||
In the next notebook we will go one step further and
|
Open up this same notebook across on different tabs and
|
||||||
[develop a multiplayer pong game](/learn/notebooks/pong)!
|
each different user can post their messages.
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
Our chat application is ready, therefore it means we are
|
||||||
|
ready to deploy! Click on the <i class="ri-livebook-deploy"></i>
|
||||||
|
icon on the sidebar.
|
||||||
|
|
||||||
|
Now, define a slug for your deployment, such as "chat-app",
|
||||||
|
set a password (or disable password protection), and click
|
||||||
|
"Deploy".
|
||||||
|
|
||||||
|
Once you do so, you will see that... the deployed application
|
||||||
|
will be in a "Booting" state forever? Well, clearly something
|
||||||
|
has gone wrong.
|
||||||
|
|
||||||
|
To understand what went wrong, we need to talk about how
|
||||||
|
the Deploy feature works. When you deploy a notebook,
|
||||||
|
Livebook will execute all of the code cells in the notebook.
|
||||||
|
Your application will effectively be all frames, controls,
|
||||||
|
etc. that you rendered on the page. However, this implies
|
||||||
|
that Livebook can fully execute all cells in your notebook,
|
||||||
|
without errors and without getting stuck.
|
||||||
|
|
||||||
|
However, if you remember the "Enumerating controls" section,
|
||||||
|
we did create an infinite loop there! And because that cell
|
||||||
|
never finishes, the deployed application never finishes booting.
|
||||||
|
The solution is easy, delete the whole "Enumerating controls"
|
||||||
|
section (or just that cell), and try again. Now booting should
|
||||||
|
finish rather quickly and you will be able to interact with
|
||||||
|
your newly deployed app. Feel free to further improve it,
|
||||||
|
by removing frames from previous sections that do not belong
|
||||||
|
to the chat app or by adding new features.
|
||||||
|
|
||||||
|
Congratulations on shipping!
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ successfully.
|
||||||
|
|
||||||
If you save secrets in your Hub, you will only need to input the secret
|
If you save secrets in your Hub, you will only need to input the secret
|
||||||
once in notebooks authored by you. You can manage all of your secrets
|
once in notebooks authored by you. You can manage all of your secrets
|
||||||
by clicking the lock icon (<i class="ri-lock-password-line"></i>) on the
|
by clicking the lock icon (<i class="ri-livebook-secrets"></i>) on the
|
||||||
sidebar.
|
sidebar.
|
||||||
|
|
||||||
## Markdown extensions
|
## Markdown extensions
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue