Merge branch 'elixir-nx:main' into main

This commit is contained in:
Jean Carlos 2021-07-07 19:23:38 -03:00 committed by GitHub
commit 52e248efa1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1126 additions and 1040 deletions

View file

@ -281,11 +281,10 @@ function handleDocumentKeyDown(hook, event) {
saveNotebook(hook);
} else if (keyBuffer.tryMatch(["d", "d"])) {
deleteFocusedCell(hook);
} else if (
hook.state.focusedCellType === "elixir" &&
(keyBuffer.tryMatch(["e", "e"]) || (cmd && key === "Enter"))
) {
queueFocusedCellEvaluation(hook);
} else if (keyBuffer.tryMatch(["e", "e"]) || (cmd && key === "Enter")) {
if (hook.state.focusedCellType === "elixir") {
queueFocusedCellEvaluation(hook);
}
} else if (keyBuffer.tryMatch(["e", "a"])) {
queueAllCellsEvaluation(hook);
} else if (keyBuffer.tryMatch(["e", "s"])) {

View file

@ -10,8 +10,8 @@ import { storeUserData } from "../lib/user";
const UserForm = {
mounted() {
this.el.addEventListener("submit", (event) => {
const name = this.el.data_name.value;
const hex_color = this.el.data_hex_color.value;
const name = this.el.user_form_name.value;
const hex_color = this.el.user_form_hex_color.value;
storeUserData({ name, hex_color });
});
},

View file

@ -46,14 +46,14 @@
}
},
"../deps/phoenix": {
"version": "1.5.9",
"version": "1.5.0",
"license": "MIT"
},
"../deps/phoenix_html": {
"version": "2.14.3"
"version": "2.14.2"
},
"../deps/phoenix_live_view": {
"version": "0.15.7",
"version": "0.15.5",
"license": "MIT"
},
"node_modules/@babel/code-frame": {

View file

@ -3,6 +3,7 @@ module.exports = {
purge: [
'../lib/**/*.ex',
'../lib/**/*.leex',
'../lib/**/*.heex',
'../lib/**/*.eex',
'./js/**/*.js'
],

View file

@ -39,11 +39,12 @@ defmodule Livebook.LiveMarkdown.Export do
defp render_cell(%Cell.Elixir{} = cell) do
code = get_elixir_cell_code(cell)
delimiter = code_block_delimiter(code)
"""
```elixir
#{delimiter}elixir
#{code}
```\
#{delimiter}\
"""
|> prepend_metadata(cell.metadata)
end
@ -117,12 +118,23 @@ defmodule Livebook.LiveMarkdown.Export do
defp format_code(code) do
try do
Code.format_string!(code)
code
|> Code.format_string!()
|> IO.iodata_to_binary()
rescue
_ -> code
end
end
defp code_block_delimiter(code) do
max_streak =
Regex.scan(~r/`{3,}/, code)
|> Enum.map(fn [string] -> byte_size(string) end)
|> Enum.max(&>=/2, fn -> 2 end)
String.duplicate("`", max_streak + 1)
end
defp put_truthy(map, entries) do
Enum.reduce(entries, map, fn {key, value}, map ->
if value do

View file

@ -154,7 +154,7 @@ most busy processes!
Sometimes you may want to render arbitrary content as rich-text,
that's when `Kino.Markdown.new/1` comes into play:
```elixir
````elixir
"""
# Example
@ -174,7 +174,7 @@ A regular Markdown file.
| 2 | Erlang | https://www.erlang.org |
"""
|> Kino.Markdown.new()
```
````
## Kino.render/1

View file

@ -1,6 +1,6 @@
defmodule LivebookWeb.Helpers do
import Phoenix.LiveView.Helpers
import Phoenix.HTML.Tag
use Phoenix.Component
alias LivebookWeb.Router.Helpers, as: Routes
@doc """
@ -14,7 +14,7 @@ defmodule LivebookWeb.Helpers do
modal_class = Keyword.get(opts, :modal_class)
modal_opts = [
id: :modal,
id: "modal",
return_to: path,
modal_class: modal_class,
component: component,
@ -41,15 +41,6 @@ defmodule LivebookWeb.Helpers do
defp mac?(user_agent), do: String.match?(user_agent, ~r/Mac OS X/)
defp windows?(user_agent), do: String.match?(user_agent, ~r/Windows/)
@doc """
Returns [Remix](https://remixicon.com) icon tag.
"""
def remix_icon(name, attrs \\ []) do
icon_class = "ri-#{name}"
attrs = Keyword.update(attrs, :class, icon_class, fn class -> "#{icon_class} #{class}" end)
content_tag(:i, "", attrs)
end
defdelegate ansi_string_to_html(string, opts \\ []), to: LivebookWeb.ANSI
@doc """
@ -83,7 +74,7 @@ defmodule LivebookWeb.Helpers do
Returns path to specific process dialog within LiveDashboard.
"""
def live_dashboard_process_path(socket, pid) do
pid_str = Phoenix.LiveDashboard.Helpers.encode_pid(pid)
pid_str = Phoenix.LiveDashboard.PageBuilder.encode_pid(pid)
Routes.live_dashboard_path(socket, :page, node(), "processes", info: pid_str)
end
@ -116,15 +107,40 @@ defmodule LivebookWeb.Helpers do
end
@doc """
Renders a list of select input options with the given one selected.
"""
def render_select(name, options, selected) do
assigns = %{name: name, options: options, selected: selected}
Renders [Remix](https://remixicon.com) icon.
~L"""
<select class="input" name=<%= @name %>>
<%= for {value, label} <- options do %>
<%= tag :option, value: value, selected: value == selected %>
## Examples
<.remix_icon icon="cpu-line" />
<.remix_icon icon="cpu-line" class="align-middle mr-1" />
"""
def remix_icon(assigns) do
assigns =
assigns
|> assign_new(:class, fn -> "" end)
|> assign(:attrs, assigns_to_attributes(assigns, [:icon, :class]))
~H"""
<i class={"ri-#{@icon} #{@class}"} {@attrs}></i>
"""
end
@doc """
Renders a list of select input options with the given one selected.
## Examples
<.select
name="language"
selected={@language}
options={[en: "English", pl: "Polski", fr: "Français"]} />
"""
def select(assigns) do
~H"""
<select class="input" name={@name}>
<%= for {value, label} <- @options do %>
<option value={value} selected={value == @selected}>
<%= label %>
</option>
<% end %>
@ -134,23 +150,47 @@ defmodule LivebookWeb.Helpers do
@doc """
Renders a checkbox input styled as a switch.
"""
def render_switch(name, checked, label, opts \\ []) do
assigns = %{
name: name,
checked: checked,
label: label,
disabled: Keyword.get(opts, :disabled, false)
}
~L"""
## Examples
<.switch_checkbox
name="likes_cats"
label="I very much like cats"
checked={@likes_cats} />
"""
def switch_checkbox(assigns) do
assigns = assign_new(assigns, :disabled, fn -> false end)
~H"""
<div class="flex space-x-3 items-center justify-between">
<span class="text-gray-700"><%= @label %></span>
<label class="switch-button <%= if(@disabled, do: "switch-button--disabled") %>">
<%= tag :input, class: "switch-button__checkbox", type: "checkbox", name: @name, checked: @checked %>
<label class={"switch-button #{if(@disabled, do: "switch-button--disabled")}"}>
<input class="switch-button__checkbox" type="checkbox" name={@name} checked={@checked} />
<div class="switch-button__bg"></div>
</label>
</div>
"""
end
@doc """
Renders a choice button that is either active or not.
## Examples
<.choice_button active={@tab == "my_tab"} phx-click="set_my_tab">
My tab
</.choice_button>
"""
def choice_button(assigns) do
assigns =
assigns
|> assign_new(:class, fn -> "" end)
|> assign(:attrs, assigns_to_attributes(assigns, [:active, :class]))
~H"""
<button class={"choice-button #{if(@active, do: "active")} #{@class}"} {@attrs}>
<%= render_block(@inner_block) %>
</button>
"""
end
end

View file

@ -1,13 +1,17 @@
defmodule LivebookWeb.NotebookCardComponent do
use LivebookWeb, :live_component
defmodule LivebookWeb.ExploreHelpers do
use Phoenix.Component
@impl true
def render(assigns) do
~L"""
alias LivebookWeb.Router.Helpers, as: Routes
@doc """
Renders an explore notebook card.
"""
def notebook_card(assigns) do
~H"""
<div class="flex flex-col">
<%= live_redirect to: Routes.explore_path(@socket, :notebook, @notebook_info.slug),
class: "flex items-center justify-center p-6 border-2 border-gray-100 rounded-t-2xl h-[150px]" do %>
<img src="<%= @notebook_info.image_url %>" class="max-h-full max-w-[75%]" />
<img src={@notebook_info.image_url} class="max-h-full max-w-[75%]" />
<% end %>
<div class="px-6 py-4 bg-gray-100 rounded-b-2xl flex-grow">
<%= live_redirect @notebook_info.title,

View file

@ -4,6 +4,7 @@ defmodule LivebookWeb.ExploreLive do
import LivebookWeb.UserHelpers
import LivebookWeb.SessionHelpers
alias LivebookWeb.{SidebarHelpers, ExploreHelpers}
alias Livebook.Notebook.Explore
@impl true
@ -26,22 +27,20 @@ defmodule LivebookWeb.ExploreLive do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex flex-grow h-full">
<%= live_component LivebookWeb.SidebarComponent,
id: :sidebar,
items: [
%{type: :logo},
%{type: :break},
%{type: :user, current_user: @current_user, path: Routes.explore_path(@socket, :user)}
] %>
<SidebarHelpers.sidebar>
<SidebarHelpers.logo_item socket={@socket} />
<SidebarHelpers.break_item />
<SidebarHelpers.user_item current_user={@current_user} path={Routes.explore_path(@socket, :user)} />
</SidebarHelpers.sidebar>
<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") %>
<.remix_icon icon="arrow-left-line" class="text-2xl align-middle" />
<% end %>
<h1 class="text-3xl text-gray-800 font-semibold">
Explore
@ -61,19 +60,20 @@ defmodule LivebookWeb.ExploreLive do
<%= @lead_notebook_info.description %>
</p>
<div class="mt-4">
<%= live_patch "Let's go", to: Routes.explore_path(@socket, :notebook, @lead_notebook_info.slug),
<%= 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" />
<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 LivebookWeb.NotebookCardComponent,
id: "notebook-card-#{idx}",
notebook_info: info %>
<%# Note: it's fine to use stateless components in this comprehension,
because @notebook_infos never change %>
<%= for info <- @notebook_infos do %>
<ExploreHelpers.notebook_card notebook_info={info} socket={@socket} />
<% end %>
</div>
</div>
@ -82,7 +82,7 @@ defmodule LivebookWeb.ExploreLive do
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: :user_modal,
id: "user",
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.explore_path(@socket, :page) %>

View file

@ -4,6 +4,7 @@ defmodule LivebookWeb.HomeLive do
import LivebookWeb.UserHelpers
import LivebookWeb.SessionHelpers
alias LivebookWeb.{SidebarHelpers, ExploreHelpers}
alias Livebook.{SessionSupervisor, Session, LiveMarkdown, Notebook}
@impl true
@ -28,64 +29,61 @@ defmodule LivebookWeb.HomeLive do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex flex-grow h-full">
<%= live_component LivebookWeb.SidebarComponent,
id: :sidebar,
items: [
%{type: :break},
%{type: :user, current_user: @current_user, path: Routes.home_path(@socket, :user)}
] %>
<SidebarHelpers.sidebar>
<SidebarHelpers.break_item />
<SidebarHelpers.user_item current_user={@current_user} path={Routes.home_path(@socket, :user)} />
</SidebarHelpers.sidebar>
<div class="flex-grow px-6 py-8 overflow-y-auto">
<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="flex flex-col space-y-2 items-center pb-4 border-b border-gray-200
sm:flex-row sm:space-y-0 sm:justify-between">
<div class="text-2xl text-gray-800 font-semibold">
<img src="/images/logo-with-text.png" class="h-[50px]" alt="Livebook" />
</div>
<div class="flex space-x-2 pt-2">
<%= live_patch to: Routes.home_path(@socket, :import, "url"),
class: "button button-outlined-gray whitespace-nowrap" do %>
Import
<% end %>
<button class="button button-blue"
phx-click="new">
<%= live_patch "Import",
to: Routes.home_path(@socket, :import, "url"),
class: "button button-outlined-gray whitespace-nowrap" %>
<button class="button button-blue" phx-click="new">
New notebook
</button>
</div>
</div>
<div class="h-80">
<%= live_component LivebookWeb.PathSelectComponent,
id: "path_select",
path: @path,
extnames: [LiveMarkdown.extension()],
running_paths: paths(@session_summaries),
phx_target: nil,
phx_submit: nil do %>
id: "path_select",
path: @path,
extnames: [LiveMarkdown.extension()],
running_paths: paths(@session_summaries),
phx_target: nil,
phx_submit: nil do %>
<div class="flex justify-end space-x-2">
<%= content_tag :button,
class: "button button-outlined-gray whitespace-nowrap",
phx_click: "fork",
disabled: not path_forkable?(@path) do %>
<%= remix_icon("git-branch-line", class: "align-middle mr-1") %>
<button class="button button-outlined-gray whitespace-nowrap"
phx-click="fork"
disabled={not path_forkable?(@path)}>
<.remix_icon icon="git-branch-line" class="align-middle mr-1" />
<span>Fork</span>
<% end %>
</button>
<%= if path_running?(@path, @session_summaries) do %>
<%= live_redirect "Join session", to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)),
class: "button button-blue" %>
<%= live_redirect "Join session",
to: Routes.session_path(@socket, :page, session_id_by_path(@path, @session_summaries)),
class: "button button-blue" %>
<% else %>
<%= tag :span, if(File.regular?(@path) and not file_writable?(@path),
do: [class: "tooltip top", aria_label: "This file is write-protected, please fork instead"],
else: []
) %>
<%= content_tag :button, "Open",
class: "button button-blue",
phx_click: "open",
disabled: not path_openable?(@path, @session_summaries) %>
<span {open_button_tooltip_attrs(@path)}>
<button class="button button-blue"
phx-click="open"
disabled={not path_openable?(@path, @session_summaries)}>
Open
</button>
</span>
<% end %>
</div>
<% end %>
</div>
<div class="py-12">
<div class="mb-4 flex justify-between items-center">
<h2 class="text-xl font-semibold text-gray-800">
@ -94,37 +92,23 @@ defmodule LivebookWeb.HomeLive do
<%= 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") %>
<.remix_icon 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 LivebookWeb.NotebookCardComponent,
id: "notebook-card-#{idx}",
notebook_info: info %>
<%# Note: it's fine to use stateless components in this comprehension,
because @notebook_infos never change %>
<%= for info <- @notebook_infos do %>
<ExploreHelpers.notebook_card notebook_info={info} socket={@socket} />
<% end %>
</div>
</div>
<div class="py-12">
<h2 class="mb-4 text-xl font-semibold text-gray-800">
Running sessions
</h2>
<%= if @session_summaries == [] do %>
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
<div>
<%= remix_icon("windy-line", class: "text-gray-400 text-xl") %>
</div>
<div class="text-gray-600">
You do not have any running sessions.
<br>
Please create a new one by clicking <span class="font-semibold">New notebook</span>
</div>
</div>
<% else %>
<%= live_component LivebookWeb.HomeLive.SessionsComponent,
id: "sessions_list",
session_summaries: @session_summaries %>
<% end %>
<.sessions_list session_summaries={@session_summaries} socket={@socket} />
</div>
</div>
</div>
@ -132,7 +116,7 @@ defmodule LivebookWeb.HomeLive do
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: :user_modal,
id: "user",
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.home_path(@socket, :page) %>
@ -140,7 +124,7 @@ defmodule LivebookWeb.HomeLive do
<%= if @live_action == :close_session do %>
<%= live_modal LivebookWeb.HomeLive.CloseSessionComponent,
id: :close_session_modal,
id: "close-session",
modal_class: "w-full max-w-xl",
return_to: Routes.home_path(@socket, :page),
session_summary: @session_summary %>
@ -148,7 +132,7 @@ defmodule LivebookWeb.HomeLive do
<%= if @live_action == :import do %>
<%= live_modal LivebookWeb.HomeLive.ImportComponent,
id: :import_modal,
id: "import",
modal_class: "w-full max-w-xl",
return_to: Routes.home_path(@socket, :page),
tab: @tab %>
@ -156,6 +140,73 @@ defmodule LivebookWeb.HomeLive do
"""
end
defp open_button_tooltip_attrs(path) do
if File.regular?(path) and not file_writable?(path) do
[class: "tooltip top", aria_label: "This file is write-protected, please fork instead"]
else
[]
end
end
defp sessions_list(%{session_summaries: []} = assigns) do
~H"""
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
<div>
<.remix_icon icon="windy-line" class="text-gray-400 text-xl" />
</div>
<div class="text-gray-600">
You do not have any running sessions.
<br>
Please create a new one by clicking <span class="font-semibold">New notebook</span>
</div>
</div>
"""
end
defp sessions_list(assigns) do
~H"""
<div class="flex flex-col space-y-4">
<%= for summary <- @session_summaries do %>
<div class="p-5 flex items-center border border-gray-200 rounded-lg"
data-test-session-id={summary.session_id}>
<div class="flex-grow flex flex-col space-y-1">
<%= live_redirect summary.notebook_name,
to: Routes.session_path(@socket, :page, summary.session_id),
class: "font-semibold text-gray-800 hover:text-gray-900" %>
<div class="text-gray-600 text-sm">
<%= summary.path || "No file" %>
</div>
</div>
<div class="relative" id={"session-#{summary.session_id}-menu"} phx-hook="Menu" data-element="menu">
<button class="icon-button" data-toggle>
<.remix_icon icon="more-2-fill" class="text-xl" />
</button>
<div class="menu" data-content>
<button class="menu__item text-gray-500"
phx-click="fork_session"
phx-value-id={summary.session_id}>
<.remix_icon icon="git-branch-line" />
<span class="font-medium">Fork</span>
</button>
<a class="menu__item text-gray-500"
href={live_dashboard_process_path(@socket, summary.pid)}
target="_blank">
<.remix_icon icon="dashboard-2-line" />
<span class="font-medium">See on Dashboard</span>
</a>
<%= live_patch to: Routes.home_path(@socket, :close_session, summary.session_id),
class: "menu__item text-red-600" do %>
<.remix_icon icon="close-circle-line" />
<span class="font-medium">Close</span>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
"""
end
@impl true
def handle_params(%{"session_id" => session_id}, _url, socket) do
session_summary = Enum.find(socket.assigns.session_summaries, &(&1.session_id == session_id))

View file

@ -5,7 +5,7 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 flex flex-col space-y-8">
<h3 class="text-2xl font-semibold text-gray-800">
Close session
@ -16,8 +16,8 @@ defmodule LivebookWeb.HomeLive.CloseSessionComponent do
This won't delete any persisted files.
</p>
<div class="mt-8 flex justify-end space-x-2">
<button class="button button-red" phx-click="close" phx-target="<%= @myself %>">
<%= remix_icon("close-circle-line", class: "align-middle mr-1") %>
<button class="button button-red" phx-click="close" phx-target={@myself}>
<.remix_icon icon="close-circle-line" class="align-middle mr-1" />
Close session
</button>
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>

View file

@ -3,22 +3,22 @@ defmodule LivebookWeb.HomeLive.ImportComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 flex flex-col space-y-8">
<h3 class="text-2xl font-semibold text-gray-800">
Import notebook
</h3>
<div class="tabs">
<%= live_patch to: Routes.home_path(@socket, :import, "url"),
class: "tab #{if(@tab == "url", do: "active")}" do %>
<%= remix_icon("download-cloud-2-line", class: "align-middle") %>
class: "tab #{if(@tab == "url", do: "active")}" do %>
<.remix_icon icon="download-cloud-2-line" class="align-middle" />
<span class="font-medium">
From URL
</span>
<% end %>
<%= live_patch to: Routes.home_path(@socket, :import, "content"),
class: "tab #{if(@tab == "content", do: "active")}" do %>
<%= remix_icon("clipboard-line", class: "align-middle") %>
class: "tab #{if(@tab == "content", do: "active")}" do %>
<.remix_icon icon="clipboard-line" class="align-middle" />
<span class="font-medium">
From clipboard
</span>
@ -27,17 +27,12 @@ defmodule LivebookWeb.HomeLive.ImportComponent do
</div>
</div>
<div>
<%= case @tab do %>
<% "url" -> %>
<%= live_component LivebookWeb.HomeLive.ImportUrlComponent,
id: "import_url" %>
<% "content" -> %>
<%= live_component LivebookWeb.HomeLive.ImportContentComponent,
id: "import_content" %>
<% end %>
<%= live_component component_for_tab(@tab), id: "import-#{@tab}" %>
</div>
</div>
"""
end
defp component_for_tab("url"), do: LivebookWeb.HomeLive.ImportUrlComponent
defp component_for_tab("content"), do: LivebookWeb.HomeLive.ImportContentComponent
end

View file

@ -8,26 +8,25 @@ defmodule LivebookWeb.HomeLive.ImportContentComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex-col space-y-5">
<p class="text-gray-700">
Import notebook by directly pasting the <span class="font-semibold">live markdown</span> content.
</p>
<%= f = form_for :data, "#",
phx_submit: "import",
phx_change: "validate",
phx_target: @myself,
autocomplete: "off" %>
<.form let={f} for={:data}
phx-submit="import"
phx-change="validate"
phx-target={@myself}
autocomplete="off">
<%= textarea f, :content, value: @content, class: "input resize-none",
placeholder: "Notebook content",
autofocus: true,
spellcheck: "false",
rows: 5 %>
<%= submit "Import",
class: "mt-5 button button-blue",
disabled: @content == "" %>
</form>
<button class="mt-5 button button-blue" type="submit" disabled={@content == ""}>
Import
</button>
</.form>
</div>
"""
end

View file

@ -10,7 +10,7 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex-col space-y-5">
<%= if @error_message do %>
<div class="error-box">
@ -20,19 +20,21 @@ defmodule LivebookWeb.HomeLive.ImportUrlComponent do
<p class="text-gray-700">
Paste the URL to a .livemd file, to a GitHub file, or to a Gist to import it.
</p>
<%= f = form_for :data, "#",
phx_submit: "import",
phx_change: "validate",
phx_target: @myself,
autocomplete: "off" %>
<.form let={f} for={:data}
phx-submit="import"
phx-change="validate"
phx-target={@myself}
autocomplete="off">
<%= text_input f, :url, value: @url, class: "input",
placeholder: "Notebook URL",
autofocus: true,
spellcheck: "false" %>
<%= submit "Import",
class: "mt-5 button button-blue",
disabled: not Utils.valid_url?(@url) %>
</form>
<button class="mt-5 button button-blue"
type="submit"
disabled={not Utils.valid_url?(@url)}>
Import
</button>
</.form>
</div>
"""
end

View file

@ -1,47 +0,0 @@
defmodule LivebookWeb.HomeLive.SessionsComponent do
use LivebookWeb, :live_component
@impl true
def render(assigns) do
~L"""
<div class="flex flex-col space-y-4">
<%= for summary <- @session_summaries do %>
<div class="p-5 flex items-center border border-gray-200 rounded-lg"
data-test-session-id="<%= summary.session_id %>">
<div class="flex-grow flex flex-col space-y-1">
<%= live_redirect summary.notebook_name, to: Routes.session_path(@socket, :page, summary.session_id),
class: "font-semibold text-gray-800 hover:text-gray-900" %>
<div class="text-gray-600 text-sm">
<%= summary.path || "No file" %>
</div>
</div>
<div class="relative" id="session-<%= summary.session_id %>-menu" phx-hook="Menu" data-element="menu">
<button class="icon-button" data-toggle>
<%= remix_icon("more-2-fill", class: "text-xl") %>
</button>
<div class="menu" data-content>
<button class="menu__item text-gray-500"
phx-click="fork_session"
phx-value-id="<%= summary.session_id %>">
<%= remix_icon("git-branch-line") %>
<span class="font-medium">Fork</span>
</button>
<%= link to: live_dashboard_process_path(@socket, summary.pid),
class: "menu__item text-gray-500",
target: "_blank" do %>
<%= remix_icon("dashboard-2-line") %>
<span class="font-medium">See on Dashboard</span>
<% end %>
<%= live_patch to: Routes.home_path(@socket, :close_session, summary.session_id),
class: "menu__item text-red-600" do %>
<%= remix_icon("close-circle-line") %>
<span class="font-medium">Close</span>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
"""
end
end

View file

@ -3,9 +3,8 @@ defmodule LivebookWeb.ModalComponent do
@impl true
def render(assigns) do
~L"""
<div class="fixed z-[10000] inset-0"
id="<%= @id %>">
~H"""
<div class="fixed z-[10000] inset-0">
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
@ -15,17 +14,17 @@ defmodule LivebookWeb.ModalComponent do
phx-capture-click="close"
phx-window-keydown="close"
phx-key="escape"
phx-target="#<%= @id %>"
phx-target={@myself}
phx-page-loading></div>
<!-- Modal box -->
<div class="relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl <%= @modal_class %>"
<div class={"relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl #{@modal_class}"}
role="dialog"
aria-modal="true">
<%= live_patch to: @return_to, class: "absolute top-6 right-6 text-gray-400 flex space-x-1 items-center" do %>
<span class="text-sm">(esc)</span>
<%= remix_icon("close-line", class: "text-2xl") %>
<.remix_icon icon="close-line" class="text-2xl" />
<% end %>
<%= live_component @component, @opts %>

View file

@ -3,8 +3,8 @@ defmodule LivebookWeb.Output.ImageComponent do
@impl true
def render(assigns) do
~L"""
<%= tag :img, src: data_url(@content, @mime_type), alt: "output image" %>
~H"""
<img src={data_url(@content, @mime_type)} alt="output image" />
"""
end

View file

@ -13,11 +13,11 @@ defmodule LivebookWeb.Output.MarkdownComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="markdown"
id="markdown-renderer-<%= @id %>"
id={"markdown-renderer-#{@id}"}
phx-hook="MarkdownRenderer"
data-id="<%= @id %>">
data-id={@id}>
</div>
"""
end

View file

@ -30,7 +30,7 @@ defmodule LivebookWeb.Output.TableDynamicLive do
@impl true
def render(%{loading: true} = assigns) do
~L"""
~H"""
<div class="max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4">
<div class="h-4 bg-gray-200 rounded-lg w-3/4"></div>
@ -42,7 +42,7 @@ defmodule LivebookWeb.Output.TableDynamicLive do
end
def render(assigns) do
~L"""
~H"""
<div class="mb-4 flex items-center space-x-3">
<h3 class="font-semibold text-gray-800">
<%= @name %>
@ -52,9 +52,8 @@ defmodule LivebookWeb.Output.TableDynamicLive do
<div class="flex space-x-2">
<%= if :refetch in @features do %>
<span class="tooltip left" aria-label="Refetch">
<%= tag :button, class: "icon-button",
phx_click: "refetch" %>
<%= remix_icon("refresh-line", class: "text-xl") %>
<button class="icon-button" phx-click="refetch">
<.remix_icon icon="refresh-line" class="text-xl" />
</button>
</span>
<% end %>
@ -62,22 +61,20 @@ defmodule LivebookWeb.Output.TableDynamicLive do
<!-- Pagination -->
<%= if :pagination in @features and @total_rows > 0 do %>
<div class="flex space-x-2">
<%= tag :button,
class: "flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300",
disabled: @page == 1,
phx_click: "prev" %>
<%= remix_icon("arrow-left-s-line", class: "text-xl") %>
<button class="flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300"
phx-click="prev"
disabled={@page == 1}>
<.remix_icon icon="arrow-left-s-line" class="text-xl" />
<span>Prev</span>
</button>
<div class="flex items-center px-3 py-1 rounded-lg border border-gray-300 font-medium text-sm text-gray-400">
<span><%= @page %> of <%= max_page(@total_rows, @limit) %></span>
</div>
<%= tag :button,
class: "flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300",
disabled: @page == max_page(@total_rows, @limit),
phx_click: "next" %>
<button class="flex items-center font-medium text-sm text-gray-400 hover:text-gray-800 disabled:pointer-events-none disabled:text-gray-300"
phx-click="next"
disabled={@page == max_page(@total_rows, @limit)}>
<span>Next</span>
<%= remix_icon("arrow-right-s-line", class: "text-xl") %>
<.remix_icon icon="arrow-right-s-line" class="text-xl" />
</button>
</div>
<% end %>
@ -94,13 +91,13 @@ defmodule LivebookWeb.Output.TableDynamicLive do
<thead class="text-left">
<tr class="border-b border-gray-200 whitespace-nowrap">
<%= for {column, idx} <- Enum.with_index(@columns) do %>
<th class="py-3 px-6 text-gray-700 font-semibold <%= if(:sorting in @features, do: "cursor-pointer", else: "pointer-events-none") %>"
<th class={"py-3 px-6 text-gray-700 font-semibold #{if(:sorting in @features, do: "cursor-pointer", else: "pointer-events-none")}"}
phx-click="column_click"
phx-value-column_idx="<%= idx %>">
phx-value-column_idx={idx}>
<div class="flex items-center space-x-1">
<span><%= column.label %></span>
<%= tag :span, class: unless(@order_by == column.key, do: "invisible") %>
<%= remix_icon(order_icon(@order), class: "text-xl align-middle leading-none") %>
<span class={unless(@order_by == column.key, do: "invisible")}>
<.remix_icon icon={order_icon(@order)} class="text-xl align-middle leading-none" />
</span>
</div>
</th>

View file

@ -3,25 +3,25 @@ defmodule LivebookWeb.Output.TextComponent do
@impl true
def render(assigns) do
~L"""
<div id="virtualized-text-<%= @id %>"
~H"""
<div id={"virtualized-text-#{@id}"}
class="relative"
phx-hook="VirtualizedLines"
data-max-height="300"
data-follow="<%= @follow %>">
data-follow={to_string(@follow)}>
<%# Add a newline to each element, so that multiple lines can be copied properly %>
<div data-template class="hidden"
id="virtualized-text-<%= @id %>-template"
id={"virtualized-text-#{@id}-template"}
><%= for line <- ansi_to_html_lines(@content) do %><div><%= [line, "\n"] %></div><% end %></div>
<div data-content class="overflow-auto whitespace-pre font-editor text-gray-500 tiny-scrollbar"
id="virtualized-text-<%= @id %>-content"
id={"virtualized-text-#{@id}-content"}
phx-update="ignore"></div>
<div class="absolute right-0 top-0 z-10">
<button class="icon-button bg-gray-100"
id="virtualized-text-<%= @id %>-clipcopy"
id={"virtualized-text-#{@id}-clipcopy"}
phx-hook="ClipCopy"
data-target-id="virtualized-text-<%= @id %>-template">
<%= remix_icon("clipboard-line", class: "text-lg") %>
data-target-id={"virtualized-text-#{@id}-template"}>
<.remix_icon icon="clipboard-line" class="text-lg" />
</button>
</div>
</div>

View file

@ -10,8 +10,8 @@ defmodule LivebookWeb.Output.VegaLiteDynamicLive do
@impl true
def render(assigns) do
~L"""
<div id="vega-lite-<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>">
~H"""
<div id={"vega-lite-#{@id}"} phx-hook="VegaLite" phx-update="ignore" data-id={@id}>
</div>
"""
end

View file

@ -9,8 +9,8 @@ defmodule LivebookWeb.Output.VegaLiteStaticComponent do
@impl true
def render(assigns) do
~L"""
<div id="vega-lite-<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>">
~H"""
<div id={"vega-lite-#{@id}"} phx-hook="VegaLite" phx-update="ignore" data-id={@id}>
</div>
"""
end

View file

@ -19,11 +19,10 @@ defmodule LivebookWeb.PathSelectComponent do
@impl true
def mount(socket) do
inner_block = Map.get(socket.assigns, :inner_block, nil)
{:ok,
assign(socket,
inner_block: inner_block,
socket
|> assign_new(:inner_block, fn -> nil end)
|> assign(
current_dir: nil,
new_directory_name: nil,
deleting_path: nil,
@ -60,34 +59,31 @@ defmodule LivebookWeb.PathSelectComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="h-full flex flex-col">
<div class="flex space-x-3 items-center mb-4">
<form class="flex-grow"
phx-change="set_path"
<%= if @phx_submit do %>
phx-submit="<%= @phx_submit %>"
<% else %>
onsubmit="return false"
<% end %>
<%= if @phx_target, do: "phx-target=#{@phx_target}" %>>
phx-submit={@phx_submit}
onsubmit={unless(@phx_submit, do: "return false")}
phx-target={@phx_target}>
<input class="input"
id="input-path"
phx-hook="FocusOnUpdate"
type="text"
name="path"
placeholder="File"
value="<%= @path %>"
value={@path}
spellcheck="false"
autocomplete="off" />
</form>
<div class="relative" id="path-selector-menu" phx-hook="Menu" data-element="menu">
<button class="icon-button" data-toggle tabindex="-1">
<%= remix_icon("more-2-fill", class: "text-xl") %>
<.remix_icon icon="more-2-fill" class="text-xl" />
</button>
<div class="menu" data-content>
<button class="menu__item text-gray-500" phx-click="new_directory" phx-target="<%= @myself %>">
<%= remix_icon("folder-add-fill", class: "text-gray-400") %>
<button class="menu__item text-gray-500" phx-click="new_directory" phx-target={@myself}>
<.remix_icon icon="folder-add-fill" class="text-gray-400" />
<span class="font-medium">New directory</span>
</button>
</div>
@ -107,13 +103,13 @@ defmodule LivebookWeb.PathSelectComponent do
<div class="flex space-x-4">
<button class="text-red-600 font-medium text-sm whitespace-nowrap"
phx-click="do_delete_file"
phx-target="<%= @myself %>">
<%= remix_icon("delete-bin-6-line", class: "align-middle mr-1") %>
phx-target={@myself}>
<.remix_icon icon="delete-bin-6-line" class="align-middle mr-1" />
Delete
</button>
<button class="text-gray-600 font-medium text-sm"
phx-click="cancel_delete_file"
phx-target="<%= @myself %>">
phx-target={@myself}>
Cancel
</button>
</div>
@ -124,23 +120,23 @@ defmodule LivebookWeb.PathSelectComponent do
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2 border-b border-dashed border-grey-200 mb-2 pb-2">
<div class="flex space-x-2 items-center p-2 rounded-lg">
<span class="block">
<%= remix_icon("folder-add-fill", class: "text-xl align-middle text-gray-400") %>
<.remix_icon icon="folder-add-fill" class="text-xl align-middle text-gray-400" />
</span>
<span class="flex font-medium text-gray-500">
<div
phx-window-keydown="cancel_new_directory"
phx-key="escape"
phx-target="<%= @myself %>">
phx-target={@myself}>
<input
type="text"
value="<%= @new_directory_name %>"
value={@new_directory_name}
autofocus
spellcheck="false"
autocomplete="off"
phx-blur="cancel_new_directory"
phx-window-keydown="create_directory"
phx-key="enter"
phx-target="<%= @myself %>" />
phx-target={@myself} />
</div>
</span>
</div>
@ -150,14 +146,24 @@ defmodule LivebookWeb.PathSelectComponent do
<%= if highlighting?(@files) do %>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2 border-b border-dashed border-grey-200 mb-2 pb-2">
<%= for file <- @files, file.highlighted != "" do %>
<%= render_file(file, @phx_target, @myself, @renaming_path, @renamed_name) %>
<.file
file={file}
phx_target={@phx_target}
myself={@myself}
renaming_path={@renaming_path}
renamed_name={@renamed_name} />
<% end %>
</div>
<% end %>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<%= for file <- @files, file.highlighted == "" do %>
<%= render_file(file, @phx_target, @myself, @renaming_path, @renamed_name) %>
<.file
file={file}
phx_target={@phx_target}
myself={@myself}
renaming_path={@renaming_path}
renamed_name={@renamed_name} />
<% end %>
</div>
</div>
@ -169,68 +175,60 @@ defmodule LivebookWeb.PathSelectComponent do
Enum.any?(files, &(&1.highlighted != ""))
end
defp render_file(
%{path: renaming_path} = file,
_phx_target,
myself,
renaming_path,
renamed_name
) do
assigns = %{file: file, myself: myself, renamed_name: renamed_name}
~L"""
defp file(%{file: %{path: path}, renaming_path: path} = assigns) do
~H"""
<div class="flex space-x-2 items-center p-2 rounded-lg">
<span class="block">
<%= remix_icon("edit-line", class: "text-xl align-middle text-gray-400") %>
<.remix_icon icon="edit-line" class="text-xl align-middle text-gray-400" />
</span>
<span class="flex font-medium text-gray-500">
<div
phx-window-keydown="cancel_rename_file"
phx-key="escape"
phx-target="<%= @myself %>">
phx-target={@myself}>
<input class="w-full"
type="text"
value="<%= @renamed_name %>"
value={@renamed_name}
autofocus
spellcheck="false"
autocomplete="off"
phx-blur="cancel_rename_file"
phx-window-keydown="do_rename_file"
phx-key="enter"
phx-target="<%= @myself %>" />
phx-target={@myself} />
</div>
</span>
</div>
"""
end
defp render_file(file, phx_target, myself, _renaming_path, _renamed_name) do
defp file(assigns) do
icon =
case file do
case assigns.file do
%{is_running: true} -> "play-circle-line"
%{is_dir: true} -> "folder-fill"
_ -> "file-code-line"
end
assigns = %{file: file, icon: icon, myself: myself}
assigns = assign(assigns, :icon, icon)
~L"""
~H"""
<div class="relative"
id="file-menu-<%= @file.path %>"
id={"file-menu-#{@file.path}"}
phx-hook="Menu"
data-primary="false"
data-element="menu">
<button class="w-full flex space-x-2 items-center p-2 rounded-lg hover:bg-gray-100 focus:ring-1 focus:ring-gray-400"
data-toggle
phx-click="set_path"
phx-value-path="<%= @file.path %>"
<%= if phx_target, do: "phx-target=#{phx_target}" %>>
phx-value-path={@file.path}
phx-target={@phx_target}>
<span class="block">
<%= remix_icon(@icon, class: "text-xl align-middle #{if(@file.is_running, do: "text-green-300", else: "text-gray-400")}") %>
<.remix_icon icon={@icon} class={"text-xl align-middle #{if(@file.is_running, do: "text-green-300", else: "text-gray-400")}"} />
</span>
<span class="flex font-medium overflow-hidden whitespace-nowrap <%= if(@file.is_running, do: "text-green-300", else: "text-gray-500") %>">
<span class={"flex font-medium overflow-hidden whitespace-nowrap #{if(@file.is_running, do: "text-green-300", else: "text-gray-500")}"}>
<%= if @file.highlighted != "" do %>
<span class="font-medium <%= if(@file.is_running, do: "text-green-400", else: "text-gray-900") %>">
<span class={"font-medium #{if(@file.is_running, do: "text-green-400", else: "text-gray-900")}"}>
<%= @file.highlighted %>
</span>
<% end %>
@ -242,16 +240,16 @@ defmodule LivebookWeb.PathSelectComponent do
<div class="menu" data-content>
<button class="menu__item text-gray-500"
phx-click="rename_file"
phx-target="<%= @myself %>"
phx-value-path="<%= @file.path %>">
<%= remix_icon("edit-line") %>
phx-target={@myself}
phx-value-path={@file.path}>
<.remix_icon icon="edit-line" />
<span class="font-medium">Rename</span>
</button>
<button class="menu__item text-red-600"
phx-click="delete_file"
phx-target="<%= @myself %>"
phx-value-path="<%= @file.path %>">
<%= remix_icon("delete-bin-6-line") %>
phx-target={@myself}
phx-value-path={@file.path}>
<.remix_icon icon="delete-bin-6-line" />
<span class="font-medium">Delete</span>
</button>
</div>

View file

@ -2,10 +2,11 @@ defmodule LivebookWeb.SessionLive do
use LivebookWeb, :live_view
import LivebookWeb.UserHelpers
import Livebook.Utils, only: [access_by_id: 1]
alias LivebookWeb.SidebarHelpers
alias Livebook.{SessionSupervisor, Session, Delta, Notebook, Runtime}
alias Livebook.Notebook.Cell
import Livebook.Utils, only: [access_by_id: 1]
@impl true
def mount(%{"id" => session_id}, %{"current_user_id" => current_user_id} = session, socket) do
@ -67,53 +68,41 @@ defmodule LivebookWeb.SessionLive do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex flex-grow h-full"
id="session"
data-element="session"
phx-hook="Session">
<%= live_component 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: :link,
icon: "delete-bin-6-fill",
path: Routes.session_path(@socket, :bin, @session_id),
label: "Bin (sb)",
active: @live_action == :bin
},
%{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)}
] %>
<SidebarHelpers.sidebar>
<SidebarHelpers.logo_item socket={@socket} />
<SidebarHelpers.button_item
icon="booklet-fill"
label="Sections (ss)"
data_element="sections-list-toggle" />
<SidebarHelpers.button_item
icon="group-fill"
label="Connected users (su)"
data_element="clients-list-toggle" />
<SidebarHelpers.link_item
icon="cpu-line"
label="Runtime settings (sr)"
path={Routes.session_path(@socket, :runtime_settings, @session_id)}
active={@live_action == :runtime_settings} />
<SidebarHelpers.link_item
icon="delete-bin-6-fill"
label="Bin (sb)"
path={Routes.session_path(@socket, :bin, @session_id)}
active={@live_action == :bin} />
<SidebarHelpers.break_item />
<SidebarHelpers.link_item
icon="keyboard-box-fill"
label="Keyboard shortcuts (?)"
path={Routes.session_path(@socket, :shortcuts, @session_id)}
active={@live_action == :shortcuts} />
<SidebarHelpers.user_item
current_user={@current_user}
path={Routes.session_path(@socket, :user, @session_id)} />
</SidebarHelpers.sidebar>
<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">
@ -125,14 +114,14 @@ defmodule LivebookWeb.SessionLive do
<%= for section_item <- @data_view.sections_items do %>
<button class="text-left hover:text-gray-900 text-gray-500"
data-element="sections-list-item"
data-section-id="<%= section_item.id %>">
data-section-id={section_item.id}>
<%= section_item.name %>
</button>
<% end %>
</div>
<button class="mt-8 p-8 py-1 text-gray-500 text-sm font-medium rounded-xl border border-gray-400 border-dashed hover:bg-gray-100 inline-flex items-center justify-center space-x-2"
phx-click="append_section">
<%= remix_icon("add-line", class: "text-lg align-center") %>
<.remix_icon icon="add-line" class="text-lg align-center" />
<span>New section</span>
</button>
</div>
@ -148,13 +137,13 @@ defmodule LivebookWeb.SessionLive do
<div class="mt-4 flex flex-col space-y-4">
<%= for {client_pid, user} <- @data_view.clients do %>
<div class="flex items-center justify-between space-x-2"
id="clients-list-item-<%= inspect(client_pid) %>"
id={"clients-list-item-#{inspect(client_pid)}"}
data-element="clients-list-item"
data-client-pid="<%= inspect(client_pid) %>">
data-client-pid={inspect(client_pid)}>
<button class="flex space-x-2 items-center text-gray-500 hover:text-gray-900 disabled:pointer-events-none"
<%= if client_pid == @self, do: "disabled" %>
disabled={client_pid == @self}
data-element="client-link">
<%= render_user_avatar(user, class: "h-7 w-7 flex-shrink-0", text_class: "text-xs") %>
<.user_avatar user={user} class="h-7 w-7 flex-shrink-0" text_class="text-xs" />
<span><%= user.name || "Anonymous" %></span>
</button>
<%= if client_pid != @self do %>
@ -162,14 +151,14 @@ defmodule LivebookWeb.SessionLive do
data-element="client-follow-toggle"
data-meta="follow">
<button class="icon-button">
<%= remix_icon("pushpin-line", class: "text-lg") %>
<.remix_icon icon="pushpin-line" class="text-lg" />
</button>
</span>
<span class="tooltip left" aria-label="Unfollow this user"
data-element="client-follow-toggle"
data-meta="unfollow">
<button class="icon-button">
<%= remix_icon("pushpin-fill", class: "text-lg") %>
<.remix_icon icon="pushpin-fill" class="text-lg" />
</button>
</span>
<% end %>
@ -192,23 +181,23 @@ defmodule LivebookWeb.SessionLive do
data-update-attribute="phx-value-name"><%= @data_view.notebook_name %></h1>
<div class="relative" id="session-menu" phx-hook="Menu" data-element="menu">
<button class="icon-button" data-toggle>
<%= remix_icon("more-2-fill", class: "text-xl") %>
<.remix_icon icon="more-2-fill" class="text-xl" />
</button>
<div class="menu" data-content>
<button class="menu__item text-gray-500"
phx-click="fork_session">
<%= remix_icon("git-branch-line") %>
<.remix_icon icon="git-branch-line" />
<span class="font-medium">Fork</span>
</button>
<%= link to: live_dashboard_process_path(@socket, @session_pid),
class: "menu__item text-gray-500",
target: "_blank" do %>
<%= remix_icon("dashboard-2-line") %>
<a class="menu__item text-gray-500"
href={live_dashboard_process_path(@socket, @session_pid)}
target="_blank">
<.remix_icon icon="dashboard-2-line" />
<span class="font-medium">See on Dashboard</span>
<% end %>
</a>
<%= live_patch to: Routes.home_path(@socket, :close_session, @session_id),
class: "menu__item text-red-600" do %>
<%= remix_icon("close-circle-line") %>
<.remix_icon icon="close-circle-line" />
<span class="font-medium">Close</span>
<% end %>
</div>
@ -237,13 +226,16 @@ defmodule LivebookWeb.SessionLive do
<div class="fixed bottom-[0.4rem] right-[1.5rem]">
<%= live_component LivebookWeb.SessionLive.IndicatorsComponent,
session_id: @session_id,
data_view: @data_view %>
path: @data_view.path,
dirty: @data_view.dirty,
runtime: @data_view.runtime,
global_evaluation_status: @data_view.global_evaluation_status %>
</div>
</div>
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: :user_modal,
id: "user",
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.session_path(@socket, :page, @session_id) %>
@ -251,7 +243,7 @@ defmodule LivebookWeb.SessionLive do
<%= if @live_action == :runtime_settings do %>
<%= live_modal LivebookWeb.SessionLive.RuntimeComponent,
id: :runtime_settings_modal,
id: "runtime-settings",
modal_class: "w-full max-w-4xl",
return_to: Routes.session_path(@socket, :page, @session_id),
session_id: @session_id,
@ -260,7 +252,7 @@ defmodule LivebookWeb.SessionLive do
<%= if @live_action == :file_settings do %>
<%= live_modal LivebookWeb.SessionLive.PersistenceComponent,
id: :runtime_settings_modal,
id: "persistence",
modal_class: "w-full max-w-4xl",
return_to: Routes.session_path(@socket, :page, @session_id),
session_id: @session_id,
@ -270,7 +262,7 @@ defmodule LivebookWeb.SessionLive do
<%= if @live_action == :shortcuts do %>
<%= live_modal LivebookWeb.SessionLive.ShortcutsComponent,
id: :shortcuts_modal,
id: "shortcuts",
modal_class: "w-full max-w-6xl",
platform: @platform,
return_to: Routes.session_path(@socket, :page, @session_id) %>
@ -278,7 +270,7 @@ defmodule LivebookWeb.SessionLive do
<%= if @live_action == :cell_settings do %>
<%= live_modal settings_component_for(@cell),
id: :cell_settings_modal,
id: "cell-settings",
modal_class: "w-full max-w-xl",
session_id: @session_id,
cell: @cell,
@ -287,7 +279,7 @@ defmodule LivebookWeb.SessionLive do
<%= if @live_action == :cell_upload do %>
<%= live_modal LivebookWeb.SessionLive.CellUploadComponent,
id: :cell_upload_modal,
id: "cell-upload",
modal_class: "w-full max-w-xl",
session_id: @session_id,
cell: @cell,
@ -297,7 +289,7 @@ defmodule LivebookWeb.SessionLive do
<%= if @live_action == :delete_section do %>
<%= live_modal LivebookWeb.SessionLive.DeleteSectionComponent,
id: :delete_section_modal,
id: "delete-section",
modal_class: "w-full max-w-xl",
session_id: @session_id,
section: @section,
@ -307,7 +299,7 @@ defmodule LivebookWeb.SessionLive do
<%= if @live_action == :bin do %>
<%= live_modal LivebookWeb.SessionLive.BinComponent,
id: :bin_modal,
id: "bin",
modal_class: "w-full max-w-4xl",
session_id: @session_id,
bin_entries: @data_view.bin_entries,

View file

@ -15,7 +15,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex-col space-y-5">
<%= if @error_message do %>
<div class="error-box">
@ -39,7 +39,11 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
<p class="text-gray-700">
Then enter the connection information below:
</p>
<%= f = form_for :data, "#", phx_submit: "init", phx_change: "validate", autocomplete: "off", spellcheck: "false" %>
<.form let={f} for={:data}
phx-submit="init"
phx-change="validate"
autocomplete="off"
spellcheck="false">
<div class="flex flex-col space-y-4">
<div>
<div class="input-label">Name</div>
@ -50,8 +54,12 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
<%= text_input f, :cookie, value: @data["cookie"], class: "input", placeholder: "mycookie" %>
</div>
</div>
<%= submit "Connect", class: "mt-5 button button-blue", disabled: not data_valid?(@data) %>
</form>
<button class="mt-5 button button-blue"
type="submit"
disabled={not data_valid?(@data)}>
Connect
</button>
</.form>
</div>
"""
end

View file

@ -30,7 +30,7 @@ defmodule LivebookWeb.SessionLive.BinComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 max-w-4xl flex flex-col space-y-3">
<h3 class="text-2xl font-semibold text-gray-800">
Bin
@ -42,18 +42,18 @@ defmodule LivebookWeb.SessionLive.BinComponent do
<%= if @bin_entries == [] do %>
<div class="p-5 flex space-x-4 items-center border border-gray-200 rounded-lg">
<div>
<%= remix_icon("windy-line", class: "text-gray-400 text-xl") %>
<.remix_icon icon="windy-line" class="text-gray-400 text-xl" />
</div>
<div class="text-gray-600">
There are currently no cells in the bin.
</div>
</div>
<% else %>
<form phx-change="search" onsubmit="return false" phx-target="<%= @myself %>">
<form phx-change="search" onsubmit="return false" phx-target={@myself}>
<input class="input"
type="text"
name="search"
value="<%= @search %>"
value={@search}
placeholder="Search"
autocomplete="off"
spellcheck="false"
@ -67,40 +67,38 @@ defmodule LivebookWeb.SessionLive.BinComponent do
<%= index %>.
<span class="font-semibold"><%= Cell.type(cell) |> Atom.to_string() |> String.capitalize() %></span> cell
deleted from <span class="font-semibold"><%= entry.section_name %></span> section
<span class="font-semibold">
<%= entry.deleted_at |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words() %> ago
</span>
<span class="font-semibold"><%= format_date_relatively(entry.deleted_at) %></span>
</p>
<div class="flex justify-end space-x-2">
<span class="tooltip left" aria-label="Copy source">
<button class="icon-button"
id="bin-cell-<%= cell.id %>-clipcopy"
id={"bin-cell-#{cell.id}-clipcopy"}
phx-hook="ClipCopy"
data-target-id="bin-cell-<%= cell.id %>-source">
<%= remix_icon("clipboard-line", class: "text-lg") %>
data-target-id={"bin-cell-#{cell.id}-source"}>
<.remix_icon icon="clipboard-line" class="text-lg" />
</button>
</span>
<span class="tooltip left" aria-label="Restore">
<button class="icon-button"
phx-click="restore"
phx-value-cell_id="<%= entry.cell.id %>"
phx-target="<%= @myself %>">
<%= remix_icon("arrow-go-back-line", class: "text-lg") %>
phx-value-cell_id={entry.cell.id}
phx-target={@myself}>
<.remix_icon icon="arrow-go-back-line" class="text-lg" />
</button>
</span>
</div>
</div>
<div class="markdown">
<pre><code
id="bin-cell-<%= cell.id %>-source"
id={"bin-cell-#{cell.id}-source"}
phx-hook="Highlight"
data-language="<%= Cell.type(cell) %>"><%= cell.source %></code></pre>
data-language={Cell.type(cell)}><%= cell.source %></code></pre>
</div>
</div>
<% end %>
<%= if length(@matching_entries) > @limit do %>
<div class="flex justify-center">
<button class="button button-outlined-gray" phx-click="more" phx-target="<%= @myself %>">
<button class="button button-outlined-gray" phx-click="more" phx-target={@myself}>
Older
</button>
</div>
@ -112,6 +110,11 @@ defmodule LivebookWeb.SessionLive.BinComponent do
"""
end
defp format_date_relatively(date) do
time_words = date |> DateTime.to_naive() |> Livebook.Utils.Time.time_ago_in_words()
time_words <> " ago"
end
@impl true
def handle_event("search", %{"search" => search}, socket) do
{:noreply, assign(socket, search: search, limit: @initial_limit) |> assign_matching_entries()}

View file

@ -2,86 +2,64 @@ defmodule LivebookWeb.SessionLive.CellComponent do
use LivebookWeb, :live_component
def render(assigns) do
~L"""
~H"""
<div class="flex flex-col relative"
data-element="cell"
id="cell-<%= @cell_view.id %>"
id={"cell-#{@cell_view.id}"}
phx-hook="Cell"
data-cell-id="<%= @cell_view.id %>"
data-type="<%= @cell_view.type %>"
data-session-path="<%= Routes.session_path(@socket, :page, @session_id) %>">
<%= render_cell_content(assigns) %>
data-cell-id={@cell_view.id}
data-type={@cell_view.type}
data-session-path={Routes.session_path(@socket, :page, @session_id)}>
<%= render_cell(assigns) %>
</div>
"""
end
def render_cell_content(%{cell_view: %{type: :markdown}} = assigns) do
~L"""
def render_cell(%{cell_view: %{type: :markdown}} = assigns) do
~H"""
<div class="mb-1 flex items-center justify-end">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
<%= render_cell_anchor_link(assigns) %>
<.cell_link_button cell_id={@cell_view.id} />
<span class="tooltip top" aria-label="Edit content" data-element="enable-insert-mode-button">
<button class="icon-button">
<%= remix_icon("pencil-line", class: "text-xl") %>
<.remix_icon icon="pencil-line" class="text-xl" />
</button>
</span>
<span class="tooltip top" aria-label="Insert image" data-element="insert-image-button">
<%= live_patch to: Routes.session_path(@socket, :cell_upload, @session_id, @cell_view.id),
class: "icon-button" do %>
<%= remix_icon("image-add-line", class: "text-xl") %>
<.remix_icon icon="image-add-line" class="text-xl" />
<% end %>
</span>
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="-1">
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
</button>
</span>
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="1">
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
</button>
</span>
<span class="tooltip top" aria-label="Delete">
<button class="icon-button"
phx-click="delete_cell"
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
</button>
</span>
<.move_cell_up_button cell_id={@cell_view.id} />
<.move_cell_down_button cell_id={@cell_view.id} />
<.delete_cell_button cell_id={@cell_view.id} />
</div>
</div>
<div class="flex relative">
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
<.cell_body>
<div class="pb-4" data-element="editor-box">
<.editor cell_view={@cell_view} />
</div>
<div class="w-full">
<div class="pb-4" data-element="editor-box">
<%= render_editor(assigns) %>
</div>
<div class="markdown" data-element="markdown-container" id="markdown-container-<%= @cell_view.id %>" phx-update="ignore">
<%= render_content_placeholder("bg-gray-200", @cell_view.empty?) %>
</div>
<div class="markdown"
data-element="markdown-container"
id={"markdown-container-#{@cell_view.id}"}
phx-update="ignore">
<.content_placeholder bg_class="bg-gray-200" empty={@cell_view.empty?} />
</div>
</div>
</.cell_body>
"""
end
def render_cell_content(%{cell_view: %{type: :elixir}} = assigns) do
~L"""
def render_cell(%{cell_view: %{type: :elixir}} = assigns) do
~H"""
<div class="mb-1 flex items-center justify-between">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions" data-primary>
<%= if @cell_view.evaluation_status == :ready do %>
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="queue_cell_evaluation"
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("play-circle-fill", class: "text-xl") %>
phx-value-cell_id={@cell_view.id}>
<.remix_icon icon="play-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
<%= if(@cell_view.validity_status == :evaluated, do: "Reevaluate", else: "Evaluate") %>
</span>
@ -89,8 +67,8 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<% else %>
<button class="text-gray-600 hover:text-gray-800 focus:text-gray-800 flex space-x-1 items-center"
phx-click="cancel_cell_evaluation"
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("stop-circle-fill", class: "text-xl") %>
phx-value-cell_id={@cell_view.id}>
<.remix_icon icon="stop-circle-fill" class="text-xl" />
<span class="text-sm font-medium">
Stop
</span>
@ -98,344 +76,255 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<% end %>
</div>
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
<%= render_cell_anchor_link(assigns) %>
<span class="tooltip top" aria-label="Cell settings">
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "icon-button" do %>
<%= remix_icon("list-settings-line", class: "text-xl") %>
<% end %>
</span>
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="-1">
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
</button>
</span>
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="1">
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
</button>
</span>
<span class="tooltip top" aria-label="Delete">
<button class="icon-button"
phx-click="delete_cell"
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
</button>
</span>
<.cell_link_button cell_id={@cell_view.id} />
<.cell_settings_button cell_id={@cell_view.id} socket={@socket} session_id={@session_id} />
<.move_cell_up_button cell_id={@cell_view.id} />
<.move_cell_down_button cell_id={@cell_view.id} />
<.delete_cell_button cell_id={@cell_view.id} />
</div>
</div>
<.cell_body>
<.editor cell_view={@cell_view} />
<%= if @cell_view.outputs != [] do %>
<div class="mt-2">
<.outputs cell_view={@cell_view} socket={@socket} />
</div>
<% end %>
</.cell_body>
"""
end
def render_cell(%{cell_view: %{type: :input}} = assigns) do
~H"""
<div class="mb-1 flex items-center justify-end">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
<.cell_link_button cell_id={@cell_view.id} />
<.cell_settings_button cell_id={@cell_view.id} socket={@socket} session_id={@session_id} />
<.move_cell_up_button cell_id={@cell_view.id} />
<.move_cell_down_button cell_id={@cell_view.id} />
<.delete_cell_button cell_id={@cell_view.id} />
</div>
</div>
<.cell_body>
<form phx-change="set_cell_value" phx-submit="queue_bound_cells_evaluation">
<input type="hidden" name="cell_id" value={@cell_view.id} />
<div class="input-label">
<%= @cell_view.name %>
</div>
<%= if @cell_view.input_type == :textarea do %>
<textarea
data-element="input"
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
name="value"
spellcheck="false"
tabindex="-1"><%= [?\n, @cell_view.value] %></textarea>
<% else %>
<input type={html_input_type(@cell_view.input_type)}
data-element="input"
class={"input w-auto #{if(@cell_view.error, do: "input--error")}"}
name="value"
value={@cell_view.value}
phx-debounce="300"
spellcheck="false"
autocomplete="off"
tabindex="-1" />
<% end %>
<%= if @cell_view.error do %>
<div class="input-error">
<%= String.capitalize(@cell_view.error) %>
</div>
<% end %>
</form>
</.cell_body>
"""
end
defp cell_body(assigns) do
~H"""
<div class="flex relative">
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
</div>
<div class="w-full">
<%= render_editor(assigns) %>
<%= if @cell_view.outputs != [] do %>
<div class="mt-2">
<%= render_outputs(assigns, @socket) %>
</div>
<% end %>
<%= render_block(@inner_block) %>
</div>
</div>
"""
end
def render_cell_content(%{cell_view: %{type: :input}} = assigns) do
~L"""
<div class="mb-1 flex items-center justify-end">
<div class="relative z-20 flex items-center justify-end space-x-2" data-element="actions">
<%= render_cell_anchor_link(assigns) %>
<span class="tooltip top" aria-label="Cell settings">
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "icon-button" do %>
<%= remix_icon("list-settings-line", class: "text-xl") %>
<% end %>
</span>
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="-1">
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
</button>
</span>
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id="<%= @cell_view.id %>"
phx-value-offset="1">
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
</button>
</span>
<span class="tooltip top" aria-label="Delete">
<button class="icon-button"
phx-click="delete_cell"
phx-value-cell_id="<%= @cell_view.id %>">
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
</button>
</span>
</div>
</div>
<div class="flex relative">
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-element="cell-focus-indicator">
</div>
<div>
<form phx-change="set_cell_value" phx-submit="queue_bound_cells_evaluation">
<input type="hidden" name="cell_id" value="<%= @cell_view.id %>" />
<div class="input-label">
<%= @cell_view.name %>
</div>
<%= if (@cell_view.input_type == :textarea) do %>
<textarea
data-element="input"
class="input <%= if(@cell_view.error, do: "input--error") %>"
name="value"
spellcheck="false"
tabindex="-1"><%= [?\n, @cell_view.value] %></textarea>
<% else %>
<input type="<%= html_input_type(@cell_view.input_type) %>"
data-element="input"
class="input <%= if(@cell_view.error, do: "input--error") %>"
name="value"
value="<%= @cell_view.value %>"
phx-debounce="300"
spellcheck="false"
autocomplete="off"
tabindex="-1" />
<% end %>
<%= if @cell_view.error do %>
<div class="input-error">
<%= String.capitalize(@cell_view.error) %>
</div>
<% end %>
</form>
</div>
</div>
defp cell_link_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Link">
<a href={"#cell-#{@cell_id}"} class="icon-button">
<.remix_icon icon="link" class="text-xl" />
</a>
</span>
"""
end
defp render_editor(assigns) do
~L"""
defp cell_settings_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Cell settings">
<%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_id), class: "icon-button" do %>
<.remix_icon icon="list-settings-line" class="text-xl" />
<% end %>
</span>
"""
end
defp move_cell_up_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id={@cell_id}
phx-value-offset="-1">
<.remix_icon icon="arrow-up-s-line" class="text-xl" />
</button>
</span>
"""
end
defp move_cell_down_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_cell"
phx-value-cell_id={@cell_id}
phx-value-offset="1">
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
</button>
</span>
"""
end
defp delete_cell_button(assigns) do
~H"""
<span class="tooltip top" aria-label="Delete">
<button class="icon-button"
phx-click="delete_cell"
phx-value-cell_id={@cell_id}>
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
</button>
</span>
"""
end
defp editor(assigns) do
~H"""
<div class="py-3 rounded-lg bg-editor relative">
<div
id="editor-container-<%= @cell_view.id %>"
id={"editor-container-#{@cell_view.id}"}
data-element="editor-container"
phx-update="ignore">
<div class="px-8">
<%= render_content_placeholder("bg-gray-500", @cell_view.empty?) %>
<.content_placeholder bg_class="bg-gray-500" empty={@cell_view.empty?} />
</div>
</div>
<%= if @cell_view.type == :elixir do %>
<div class="absolute bottom-2 right-2">
<%= render_cell_status(
@cell_view.validity_status,
@cell_view.evaluation_status,
@cell_view.evaluation_time_ms,
"cell-#{@cell_view.id}-evaluation#{@cell_view.number_of_evaluations}"
) %>
<.cell_status cell_view={@cell_view} />
</div>
<% end %>
</div>
"""
end
defp render_cell_anchor_link(assigns) do
~L"""
<span class="tooltip top" aria-label="Link">
<a href="#cell-<%= @cell_view.id %>" class="icon-button">
<%= remix_icon("link", class: "text-xl") %>
</a>
</span>
"""
end
# The whole page has to load and then hooks are mounded.
# There may be a tiny delay before the markdown is rendered
# or editors are mounted, so show neat placeholders immediately.
defp render_content_placeholder(_bg_class, true = _empty) do
assigns = %{}
~L"""
<div class="h-4"></div>
"""
end
defp render_content_placeholder(bg_class, false = _empty) do
assigns = %{bg_class: bg_class}
~L"""
<div class="max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4">
<div class="h-4 <%= @bg_class %> rounded-lg w-3/4"></div>
<div class="h-4 <%= @bg_class %> rounded-lg"></div>
<div class="h-4 <%= @bg_class %> rounded-lg w-5/6"></div>
</div>
</div>
"""
end
defp render_outputs(assigns, socket) do
~L"""
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200">
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
<%= render_output(socket, output, "cell-#{@cell_view.id}-output#{index}") %>
defp content_placeholder(assigns) do
~H"""
<%= if @empty do %>
<div class="h-4"></div>
<% else %>
<div class="max-w-2xl w-full animate-pulse">
<div class="flex-1 space-y-4">
<div class={"#{@bg_class} h-4 rounded-lg w-3/4"}></div>
<div class={"#{@bg_class} h-4 rounded-lg"}></div>
<div class={"#{@bg_class} h-4 rounded-lg w-5/6"}></div>
</div>
<% end %>
</div>
"""
end
defp render_output(_socket, text, id) when is_binary(text) do
# Captured output usually has a trailing newline that we can ignore,
# because each line is itself an HTML block anyway.
text = String.replace_suffix(text, "\n", "")
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: true)
end
defp render_output(_socket, {:text, text}, id) do
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false)
end
defp render_output(_socket, {:markdown, markdown}, id) do
live_component(LivebookWeb.Output.MarkdownComponent, id: id, content: markdown)
end
defp render_output(_socket, {:image, content, mime_type}, id) do
live_component(LivebookWeb.Output.ImageComponent,
id: id,
content: content,
mime_type: mime_type
)
end
defp render_output(_socket, {:vega_lite_static, spec}, id) do
live_component(LivebookWeb.Output.VegaLiteStaticComponent, id: id, spec: spec)
end
defp render_output(socket, {:vega_lite_dynamic, pid}, id) do
live_render(socket, LivebookWeb.Output.VegaLiteDynamicLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output(socket, {:table_dynamic, pid}, id) do
live_render(socket, LivebookWeb.Output.TableDynamicLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output(_socket, {:error, formatted, :runtime_restart_required}, _id) do
assigns = %{formatted: formatted}
~L"""
<div class="flex flex-col space-y-4">
<%= render_error_message_output(@formatted) %>
<div>
<button class="button button-gray" phx-click="restart_runtime">
Restart runtime
</button>
</div>
</div>
<% end %>
"""
end
defp render_output(_socket, {:error, formatted, _type}, _id) do
render_error_message_output(formatted)
end
defp render_output(_socket, output, _id) do
render_error_message_output("""
Unknown output format: #{inspect(output)}. If you're using Kino,
you may want to update Kino and Livebook to the latest version.
""")
end
defp render_error_message_output(message) do
assigns = %{message: message}
~L"""
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
defp cell_status(%{cell_view: %{evaluation_status: :evaluating}} = assigns) do
~H"""
<.status_indicator circle_class="bg-blue-500" animated_circle_class="bg-blue-400" change_indicator={true}>
<span class="font-mono"
id={"cell-timer-#{@cell_view.id}-evaluation-#{@cell_view.number_of_evaluations}"}
phx-hook="Timer"
phx-update="ignore">
</span>
</.status_indicator>
"""
end
defp render_cell_status(cell_view, evaluation_status, evaluation_time_ms, evaluation_id)
defp render_cell_status(_, :evaluating, _, evaluation_id) do
timer =
content_tag(:span, nil,
phx_hook: "Timer",
# Make sure each evaluation gets its own timer
id: "#{evaluation_id}-timer",
phx_update: "ignore",
class: "font-mono"
)
render_status_indicator(timer, "bg-blue-500",
animated_circle_class: "bg-blue-400",
change_indicator: true
)
defp cell_status(%{cell_view: %{evaluation_status: :queued}} = assigns) do
~H"""
<.status_indicator circle_class="bg-gray-500" animated_circle_class="bg-gray-400">
Queued
</.status_indicator>
"""
end
defp render_cell_status(_, :queued, _, _) do
render_status_indicator("Queued", "bg-gray-500", animated_circle_class: "bg-gray-400")
defp cell_status(%{cell_view: %{validity_status: :evaluated}} = assigns) do
~H"""
<.status_indicator
circle_class="bg-green-400"
change_indicator={true}
tooltip={evaluated_label(@cell_view.evaluation_time_ms)}>
Evaluated
</.status_indicator>
"""
end
defp render_cell_status(:evaluated, _, evaluation_time_ms, _) do
render_status_indicator("Evaluated", "bg-green-400",
change_indicator: true,
tooltip: evaluated_label(evaluation_time_ms)
)
defp cell_status(%{cell_view: %{validity_status: :stale}} = assigns) do
~H"""
<.status_indicator circle_class="bg-yellow-200" change_indicator={true}>
Stale
</.status_indicator>
"""
end
defp render_cell_status(:stale, _, evaluation_time_ms, _) do
render_status_indicator("Stale", "bg-yellow-200",
change_indicator: true,
tooltip: evaluated_label(evaluation_time_ms)
)
defp cell_status(%{cell_view: %{validity_status: :aborted}} = assigns) do
~H"""
<.status_indicator circle_class="bg-red-400">
Aborted
</.status_indicator>
"""
end
defp render_cell_status(:aborted, _, _, _) do
render_status_indicator("Aborted", "bg-red-400")
end
defp cell_status(assigns), do: ~H""
defp render_cell_status(_, _, _, _), do: nil
defp status_indicator(assigns) do
assigns =
assigns
|> assign_new(:animated_circle_class, fn -> nil end)
|> assign_new(:change_indicator, fn -> false end)
|> assign_new(:tooltip, fn -> nil end)
defp render_status_indicator(element, circle_class, opts \\ []) do
assigns = %{
element: element,
circle_class: circle_class,
animated_circle_class: Keyword.get(opts, :animated_circle_class),
change_indicator: Keyword.get(opts, :change_indicator, false),
tooltip: Keyword.get(opts, :tooltip)
}
~L"""
<div class="<%= if(@tooltip, do: "tooltip") %> bottom distant-medium" aria-label="<%= @tooltip %>">
~H"""
<div class={"#{if(@tooltip, do: "tooltip")} bottom distant-medium"} aria-label={@tooltip}>
<div class="flex items-center space-x-1">
<div class="flex text-xs text-gray-400">
<%= @element %>
<%= render_block(@inner_block) %>
<%= if @change_indicator do %>
<span data-element="change-indicator">*</span>
<% end %>
</div>
<span class="flex relative h-3 w-3">
<%= if @animated_circle_class do %>
<span class="animate-ping absolute inline-flex h-3 w-3 rounded-full <%= @animated_circle_class %> opacity-75"></span>
<span class={"#{@animated_circle_class} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"}></span>
<% end %>
<span class="relative inline-flex rounded-full h-3 w-3 <%= @circle_class %>"></span>
<span class={"#{@circle_class} relative inline-flex rounded-full h-3 w-3"}></span>
</span>
</div>
</div>
@ -456,6 +345,95 @@ defmodule LivebookWeb.SessionLive.CellComponent do
defp evaluated_label(_time_ms), do: nil
# Outputs
defp outputs(assigns) do
~H"""
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200">
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
<%= render_output(output, %{id: "cell-#{@cell_view.id}-output#{index}", socket: @socket}) %>
</div>
<% end %>
</div>
"""
end
defp render_output(text, %{id: id}) when is_binary(text) do
# Captured output usually has a trailing newline that we can ignore,
# because each line is itself an HTML block anyway.
text = String.replace_suffix(text, "\n", "")
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: true)
end
defp render_output({:text, text}, %{id: id}) do
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false)
end
defp render_output({:markdown, markdown}, %{id: id}) do
live_component(LivebookWeb.Output.MarkdownComponent, id: id, content: markdown)
end
defp render_output({:image, content, mime_type}, %{id: id}) do
live_component(LivebookWeb.Output.ImageComponent,
id: id,
content: content,
mime_type: mime_type
)
end
defp render_output({:vega_lite_static, spec}, %{id: id}) do
live_component(LivebookWeb.Output.VegaLiteStaticComponent, id: id, spec: spec)
end
defp render_output({:vega_lite_dynamic, pid}, %{id: id, socket: socket}) do
live_render(socket, LivebookWeb.Output.VegaLiteDynamicLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output({:table_dynamic, pid}, %{id: id, socket: socket}) do
live_render(socket, LivebookWeb.Output.TableDynamicLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output({:error, formatted, :runtime_restart_required}, %{}) do
assigns = %{formatted: formatted}
~H"""
<div class="flex flex-col space-y-4">
<%= render_error_message_output(@formatted) %>
<div>
<button class="button button-gray" phx-click="restart_runtime">
Restart runtime
</button>
</div>
</div>
"""
end
defp render_output({:error, formatted, _type}, %{}) do
render_error_message_output(formatted)
end
defp render_output(output, %{}) do
render_error_message_output("""
Unknown output format: #{inspect(output)}. If you're using Kino,
you may want to update Kino and Livebook to the latest version.
""")
end
defp render_error_message_output(message) do
assigns = %{message: message}
~H"""
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
"""
end
defp html_input_type(:password), do: "password"
defp html_input_type(:number), do: "number"
defp html_input_type(:color), do: "color"

View file

@ -10,7 +10,7 @@ defmodule LivebookWeb.SessionLive.CellUploadComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 flex flex-col space-y-8">
<h3 class="text-2xl font-semibold text-gray-800">
Insert image
@ -28,31 +28,32 @@ defmodule LivebookWeb.SessionLive.CellUploadComponent do
</div>
<div class="w-full h-2 rounded-lg bg-blue-200">
<div class="h-full rounded-lg bg-blue-600 transition-all ease-out duration-1000"
style="width: <%= entry.progress %>%">
style={"width: #{entry.progress}%"}>
</div>
</div>
</div>
<% end %>
<form phx-submit="save" phx-change="validate" phx-target="<%= @myself %>">
<form phx-submit="save" phx-change="validate" phx-target={@myself}>
<div class="w-full flex space-x-2">
<div>
<label>
<%= live_file_input @uploads.cell_image, class: "hidden" %>
<div class="cursor-pointer button button-gray button-square-icon">
<%= remix_icon("folder-upload-line") %>
<.remix_icon icon="folder-upload-line" />
</div>
</label>
</div>
<div class="flex-grow">
<input class="input" name="name" placeholder="Name" autocomplete="off" value="<%= @name %>" />
<input class="input" name="name" value={@name} placeholder="Name" autocomplete="off" />
</div>
</div>
<div class="mt-8 flex justify-end space-x-2">
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
<%= content_tag :button, "Upload",
type: :submit,
class: "button button-blue",
disabled: @uploads.cell_image.entries == [] or @name == "" %>
<button class="button button-blue"
type="submit"
disabled={@uploads.cell_image.entries == [] or @name == ""}>
Upload
</button>
</div>
</form>
</div>

View file

@ -3,7 +3,7 @@ defmodule LivebookWeb.SessionLive.DeleteSectionComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 flex flex-col space-y-8">
<h3 class="text-2xl font-semibold text-gray-800">
Delete section
@ -12,15 +12,19 @@ defmodule LivebookWeb.SessionLive.DeleteSectionComponent do
Are you sure you want to delete this section -
<span class="font-semibold"><%= @section.name %></span>?
</p>
<form phx-submit="delete" phx-target="<%= @myself %>">
<form phx-submit="delete" phx-target={@myself}>
<h3 class="mb-1 text-lg font-semibold text-gray-800">
Options
</h3>
<%# If there is no previous section, all cells need to be deleted %>
<%= render_switch("delete_cells", @is_first, "Delete all cells in this section", disabled: @is_first) %>
<.switch_checkbox
name="delete_cells"
label="Delete all cells in this section"
checked={@is_first}
disabled={@is_first} />
<div class="mt-8 flex justify-end space-x-2">
<button type="submit" class="button button-red">
<%= remix_icon("delete-bin-6-line", class: "align-middle mr-1") %>
<.remix_icon icon="delete-bin-6-line" class="align-middle mr-1" />
Delete
</button>
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>

View file

@ -15,14 +15,17 @@ defmodule LivebookWeb.SessionLive.ElixirCellSettingsComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 flex flex-col space-y-8">
<h3 class="text-2xl font-semibold text-gray-800">
Cell settings
</h3>
<form phx-submit="save" phx-target="<%= @myself %>">
<form phx-submit="save" phx-target={@myself}>
<div class="w-full flex-col space-y-6">
<%= render_switch("disable_formatting", @disable_formatting, "Disable code formatting (when saving to file)") %>
<.switch_checkbox
name="disable_formatting"
label="Disable code formatting (when saving to file)"
checked={@disable_formatting} />
</div>
<div class="mt-8 flex justify-end space-x-2">
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>

View file

@ -15,7 +15,7 @@ defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex-col space-y-5">
<%= if @error_message do %>
<div class="error-box">

View file

@ -10,7 +10,7 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex-col space-y-5">
<p class="text-gray-700">
Run the notebook code within the Livebook node itself.

View file

@ -3,21 +3,21 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex flex-col space-y-2 items-center" data-element="notebook-indicators">
<%= if @data_view.path do %>
<%= if @data_view.dirty do %>
<%= if @path do %>
<%= if @dirty do %>
<span class="tooltip left" aria-label="Autosave pending">
<%= live_patch to: Routes.session_path(@socket, :file_settings, @session_id),
class: "icon-button icon-outlined-button border-blue-400 hover:bg-blue-50 focus:bg-blue-50" do %>
<%= remix_icon("save-line", class: "text-xl text-blue-500") %>
<.remix_icon icon="save-line" class="text-xl text-blue-500" />
<% end %>
</span>
<% else %>
<span class="tooltip left" aria-label="Notebook saved">
<%= live_patch to: Routes.session_path(@socket, :file_settings, @session_id),
class: "icon-button icon-outlined-button border-green-300 hover:bg-green-50 focus:bg-green-50" do %>
<%= remix_icon("save-line", class: "text-xl text-green-400") %>
<.remix_icon icon="save-line" class="text-xl text-green-400" />
<% end %>
</span>
<% end %>
@ -25,18 +25,20 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
<span class="tooltip left" aria-label="Choose a file to save the notebook">
<%= live_patch to: Routes.session_path(@socket, :file_settings, @session_id),
class: "icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100" do %>
<%= remix_icon("save-line", class: "text-xl text-gray-400") %>
<.remix_icon icon="save-line" class="text-xl text-gray-400" />
<% end %>
</span>
<% end %>
<%= if @data_view.runtime do %>
<%= render_global_evaluation_status(@data_view.global_evaluation_status) %>
<%= if @runtime do %>
<.global_evaluation_status
status={elem(@global_evaluation_status, 0)}
cell_id={elem(@global_evaluation_status, 1)} />
<% else %>
<span class="tooltip left" aria-label="Choose a runtime to run the notebook in">
<%= live_patch to: Routes.session_path(@socket, :runtime_settings, @session_id),
class: "icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100" do %>
<%= remix_icon("loader-3-line", class: "text-xl text-gray-400") %>
<.remix_icon icon="loader-3-line" class="text-xl text-gray-400" />
<% end %>
</span>
<% end %>
@ -51,55 +53,47 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
"""
end
defp render_global_evaluation_status({:evaluating, cell_id}) do
assigns = %{cell_id: cell_id}
~L"""
defp global_evaluation_status(%{status: :evaluating} = assigns) do
~H"""
<span class="tooltip left" aria-label="Go to evaluating cell">
<button class="icon-button icon-outlined-button border-blue-400 hover:bg-blue-50 focus:bg-blue-50"
data-element="focus-cell-button"
data-target="<%= @cell_id %>">
<%= remix_icon("loader-3-line", class: "text-xl text-blue-500 animate-spin") %>
data-target={@cell_id}>
<.remix_icon icon="loader-3-line" class="text-xl text-blue-500 animate-spin" />
</button>
</span>
"""
end
defp render_global_evaluation_status({:evaluated, cell_id}) do
assigns = %{cell_id: cell_id}
~L"""
defp global_evaluation_status(%{status: :evaluated} = assigns) do
~H"""
<span class="tooltip left" aria-label="Go to last evaluated cell">
<button class="icon-button icon-outlined-button border-green-300 hover:bg-green-50 focus:bg-green-50"
data-element="focus-cell-button"
data-target="<%= @cell_id %>">
<%= remix_icon("loader-3-line", class: "text-xl text-green-400") %>
data-target={@cell_id}>
<.remix_icon icon="loader-3-line" class="text-xl text-green-400" />
</button>
</span>
"""
end
defp render_global_evaluation_status({:stale, cell_id}) do
assigns = %{cell_id: cell_id}
~L"""
defp global_evaluation_status(%{status: :stale} = assigns) do
~H"""
<span class="tooltip left" aria-label="Go to first stale cell">
<button class="icon-button icon-outlined-button border-yellow-200 hover:bg-yellow-50 focus:bg-yellow-50"
data-element="focus-cell-button"
data-target="<%= @cell_id %>">
<%= remix_icon("loader-3-line", class: "text-xl text-yellow-300") %>
data-target={@cell_id}>
<.remix_icon icon="loader-3-line" class="text-xl text-yellow-300" />
</button>
</span>
"""
end
defp render_global_evaluation_status({:fresh, nil}) do
assigns = %{}
~L"""
defp global_evaluation_status(%{status: :fresh} = assigns) do
~H"""
<span class="tooltip left" aria-label="Ready to evaluate">
<button class="icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100 cursor-default">
<%= remix_icon("loader-3-line", class: "text-xl text-gray-400") %>
<.remix_icon icon="loader-3-line" class="text-xl text-gray-400" />
</button>
</span>
"""

View file

@ -19,31 +19,33 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 flex flex-col space-y-8">
<h3 class="text-2xl font-semibold text-gray-800">
Cell settings
</h3>
<form phx-submit="save" phx-change="validate" phx-target="<%= @myself %>">
<form phx-submit="save" phx-change="validate" phx-target={@myself}>
<div class="flex flex-col space-y-6">
<div>
<div class="input-label">Type</div>
<%= render_select("type", input_types(), @type) %>
<.select name="type" selected={@type} options={input_types()} />
</div>
<div>
<div class="input-label">Name</div>
<input type="text" class="input" name="name" value="<%= @name %>" spellcheck="false" autocomplete="off" autofocus />
<input type="text" class="input" name="name" value={@name} spellcheck="false" autocomplete="off" autofocus />
</div>
<div>
<%= render_switch("reactive", @reactive, "Reactive (reevaluates dependent cells on change)") %>
<.switch_checkbox
name="reactive"
label="Reactive (reevaluates dependent cells on change)"
checked={@reactive} />
</div>
</div>
<div class="mt-8 flex justify-end space-x-2">
<%= live_patch "Cancel", to: @return_to, class: "button button-outlined-gray" %>
<%= content_tag :button, "Save",
type: :submit,
class: "button button-blue",
disabled: @name == "" %>
<button class="button button-blue" type="submit" disabled={@name == ""}>
Save
</button>
</div>
</form>
</div>

View file

@ -2,31 +2,31 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
use LivebookWeb, :live_component
def render(assigns) do
~L"""
~H"""
<div class="relative top-0.5 m-0 flex justify-center" data-element="insert-buttons">
<div class="w-full absolute z-10 <%= if(@persistent, do: "opacity-100", else: "opacity-0") %> hover:opacity-100 focus-within:opacity-100 flex space-x-2 justify-center items-center">
<div class={"w-full absolute z-10 #{if(@persistent, do: "opacity-100", else: "opacity-0")} hover:opacity-100 focus-within:opacity-100 flex space-x-2 justify-center items-center"}>
<button class="button button-small"
phx-click="insert_cell"
phx-value-type="markdown"
phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @insert_cell_index %>"
phx-value-section_id={@section_id}
phx-value-index={@insert_cell_index}
>+ Markdown</button>
<button class="button button-small"
phx-click="insert_cell"
phx-value-type="elixir"
phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @insert_cell_index %>"
phx-value-section_id={@section_id}
phx-value-index={@insert_cell_index}
>+ Elixir</button>
<button class="button button-small"
phx-click="insert_cell"
phx-value-type="input"
phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @insert_cell_index %>"
phx-value-section_id={@section_id}
phx-value-index={@insert_cell_index}
>+ Input</button>
<button class="button button-small"
phx-click="insert_section_into"
phx-value-section_id="<%= @section_id %>"
phx-value-index="<%= @insert_cell_index %>"
phx-value-section_id={@section_id}
phx-value-index={@insert_cell_index}
>+ Section</button>
</div>
</div>

View file

@ -24,7 +24,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="flex-col space-y-5">
<p class="text-gray-700">
Start a new local node in the context of a Mix project.
@ -40,17 +40,16 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
<%= if @status == :initial do %>
<div class="h-full h-52">
<%= live_component LivebookWeb.PathSelectComponent,
id: "path_select",
path: @path,
extnames: [],
running_paths: [],
phx_target: nil,
phx_submit: if(disabled?(@path), do: nil, else: "init") %>
id: "path_select",
path: @path,
extnames: [],
running_paths: [],
phx_target: nil,
phx_submit: if(disabled?(@path), do: nil, else: "init") %>
</div>
<%= content_tag :button, if(matching_runtime?(@current_runtime, @path), do: "Reconnect", else: "Connect"),
class: "button button-blue",
phx_click: "init",
disabled: disabled?(@path) %>
<button class="button button-blue" phx-click="init" disabled={disabled?(@path)}>
<%= if(matching_runtime?(@current_runtime, @path), do: "Reconnect", else: "Connect") %>
</button>
<% end %>
<%= if @status != :initial do %>
<div class="markdown">
@ -58,7 +57,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do
id="mix-standalone-init-output"
phx-update="append"
phx-hook="ScrollOnUpdate"
><%= for {output, i} <- @outputs do %><span id="output-<%= i %>"><%= ansi_string_to_html(output) %></span><% end %></code></pre>
><%= for {output, i} <- @outputs do %><span id={"output-#{i}"}><%= ansi_string_to_html(output) %></span><% end %></code></pre>
</div>
<% end %>
</div>

View file

@ -12,7 +12,7 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 flex flex-col space-y-3">
<h3 class="text-2xl font-semibold text-gray-800">
File
@ -22,26 +22,30 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
Specify where the notebook should be automatically persisted.
</p>
<div class="flex space-x-4">
<%= content_tag :button, "Save to file",
class: "choice-button #{if(@path != nil, do: "active")}",
phx_click: "set_persistence_type",
phx_value_type: "file",
phx_target: @myself %>
<%= content_tag :button, "Memory only",
class: "choice-button #{if(@path == nil, do: "active")}",
phx_click: "set_persistence_type",
phx_value_type: "memory",
phx_target: @myself %>
<.choice_button
active={@path != nil}
phx-click="set_persistence_type"
phx-value-type="file"
phx-target={@myself}>
Save to file
</.choice_button>
<.choice_button
active={@path == nil}
phx-click="set_persistence_type"
phx-value-type="memory"
phx-target={@myself}>
Memory only
</.choice_button>
</div>
<%= if @path != nil do %>
<div class="h-full h-52">
<%= live_component LivebookWeb.PathSelectComponent,
id: "path_select",
path: @path,
extnames: [LiveMarkdown.extension()],
running_paths: @running_paths,
phx_target: @myself,
phx_submit: if(disabled?(@path, @current_path, @running_paths), do: nil, else: "save") %>
id: "path_select",
path: @path,
extnames: [LiveMarkdown.extension()],
running_paths: @running_paths,
phx_target: @myself,
phx_submit: if(disabled?(@path, @current_path, @running_paths), do: nil, else: "save") %>
</div>
<% end %>
<div class="flex flex-col space-y-2">
@ -51,11 +55,12 @@ defmodule LivebookWeb.SessionLive.PersistenceComponent do
</div>
<% end %>
<div>
<%= content_tag :button, "Save",
class: "button button-blue mt-2",
phx_click: "save",
phx_target: @myself,
disabled: disabled?(@path, @current_path, @running_paths) %>
<button class="button button-blue mt-2"
phx-click="save"
phx-target={@myself}
disabled={disabled?(@path, @current_path, @running_paths)}>
Save
</button>
</div>
</div>
</div>

View file

@ -29,7 +29,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 pb-4 max-w-4xl flex flex-col space-y-3">
<h3 class="text-2xl font-semibold text-gray-800">
Runtime
@ -60,7 +60,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
<button class="button button-outlined-red"
type="button"
phx-click="disconnect"
phx-target="<%= @myself %>">
phx-target={@myself}>
Disconnect
</button>
<% else %>
@ -70,48 +70,39 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
<% end %>
</div>
<div class="flex space-x-4">
<%= content_tag :button, "Elixir standalone",
class: "choice-button #{if(@type == "elixir_standalone", do: "active")}",
phx_click: "set_runtime_type",
phx_value_type: "elixir_standalone",
phx_target: @myself %>
<%= content_tag :button, "Mix standalone",
class: "choice-button #{if(@type == "mix_standalone", do: "active")}",
phx_click: "set_runtime_type",
phx_value_type: "mix_standalone",
phx_target: @myself %>
<%= content_tag :button, "Attached node",
class: "choice-button #{if(@type == "attached", do: "active")}",
phx_click: "set_runtime_type",
phx_value_type: "attached",
phx_target: @myself %>
<%= content_tag :button, "Embedded",
class: "choice-button #{if(@type == "embedded", do: "active")}",
phx_click: "set_runtime_type",
phx_value_type: "embedded",
phx_target: @myself %>
<.choice_button
active={@type == "elixir_standalone"}
phx-click="set_runtime_type"
phx-value-type="elixir_standalone"
phx-target={@myself}>
Elixir standalone
</.choice_button>
<.choice_button
active={@type == "mix_standalone"}
phx-click="set_runtime_type"
phx-value-type="mix_standalone"
phx-target={@myself}>
Mix standalone
</.choice_button>
<.choice_button
active={@type == "attached"}
phx-click="set_runtime_type"
phx-value-type="attached"
phx-target={@myself}>
Attached node
</.choice_button>
<.choice_button
active={@type == "embedded"}
phx-click="set_runtime_type"
phx-value-type="embedded"
phx-target={@myself}>
Embedded
</.choice_button>
</div>
<div>
<%= if @type == "elixir_standalone" do %>
<%= live_render @socket, LivebookWeb.SessionLive.ElixirStandaloneLive,
id: :elixir_standalone_runtime,
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
<% end %>
<%= if @type == "mix_standalone" do %>
<%= live_render @socket, LivebookWeb.SessionLive.MixStandaloneLive,
id: :mix_standalone_runtime,
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
<% end %>
<%= if @type == "attached" do %>
<%= live_render @socket, LivebookWeb.SessionLive.AttachedLive,
id: :attached_runtime,
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
<% end %>
<%= if @type == "embedded" do %>
<%= live_render @socket, LivebookWeb.SessionLive.EmbeddedLive,
id: :embedded_runtime,
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
<% end %>
<%= live_render @socket, live_view_for_type(@type),
id: "runtime-config-#{@type}",
session: %{"session_id" => @session_id, "current_runtime" => @runtime} %>
</div>
</div>
</div>
@ -128,6 +119,11 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
defp runtime_type(%Runtime.Attached{}), do: "attached"
defp runtime_type(%Runtime.Embedded{}), do: "embedded"
defp live_view_for_type("elixir_standalone"), do: LivebookWeb.SessionLive.ElixirStandaloneLive
defp live_view_for_type("mix_standalone"), do: LivebookWeb.SessionLive.MixStandaloneLive
defp live_view_for_type("attached"), do: LivebookWeb.SessionLive.AttachedLive
defp live_view_for_type("embedded"), do: LivebookWeb.SessionLive.EmbeddedLive
@impl true
def handle_event("set_runtime_type", %{"type" => type}, socket) do
{:noreply, assign(socket, type: type)}

View file

@ -2,48 +2,47 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
use LivebookWeb, :live_component
def render(assigns) do
~L"""
<div
data-element="section"
data-section-id="<%= @section_view.id %>">
~H"""
<div data-element="section" data-section-id={@section_view.id}>
<div class="flex space-x-4 items-center" data-element="section-headline">
<h2 class="flex-grow text-gray-800 font-semibold text-2xl px-1 -ml-1 rounded-lg border border-transparent hover:border-blue-200 focus:border-blue-300"
<h2 class="flex-grow text-gray-800 font-semibold text-2xl px-1 -ml-1 rounded-lg border
border-transparent hover:border-blue-200 focus:border-blue-300"
data-element="section-name"
id="<%= @section_view.html_id %>"
id={@section_view.html_id}
contenteditable
spellcheck="false"
phx-blur="set_section_name"
phx-value-section_id="<%= @section_view.id %>"
phx-value-section_id={@section_view.id}
phx-hook="ContentEditable"
data-update-attribute="phx-value-name"><%= @section_view.name %></h2>
<%# ^ Note it's important there's no space between <h2> and </h2>
because we want the content to exactly match section name. %>
<div class="flex space-x-2 items-center" data-element="section-actions">
<span class="tooltip top" aria-label="Link">
<a href="#<%= @section_view.html_id %>" class="icon-button">
<%= remix_icon("link", class: "text-xl") %>
<a href={"##{@section_view.html_id}"} class="icon-button">
<.remix_icon icon="link" class="text-xl" />
</a>
</span>
<span class="tooltip top" aria-label="Move up">
<button class="icon-button"
phx-click="move_section"
phx-value-section_id="<%= @section_view.id %>"
phx-value-section_id={@section_view.id}
phx-value-offset="-1">
<%= remix_icon("arrow-up-s-line", class: "text-xl") %>
<.remix_icon icon="arrow-up-s-line" class="text-xl" />
</button>
</span>
<span class="tooltip top" aria-label="Move down">
<button class="icon-button"
phx-click="move_section"
phx-value-section_id="<%= @section_view.id %>"
phx-value-section_id={@section_view.id}
phx-value-offset="1">
<%= remix_icon("arrow-down-s-line", class: "text-xl") %>
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
</button>
</span>
<span class="tooltip top" aria-label="Delete">
<%= live_patch to: Routes.session_path(@socket, :delete_section, @session_id, @section_view.id),
class: "icon-button" do %>
<%= remix_icon("delete-bin-6-line", class: "text-xl") %>
<.remix_icon icon="delete-bin-6-line" class="text-xl" />
<% end %>
</span>
</div>
@ -56,11 +55,13 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
persistent: false,
section_id: @section_view.id,
insert_cell_index: index %>
<%= live_component LivebookWeb.SessionLive.CellComponent,
id: cell_view.id,
session_id: @session_id,
cell_view: cell_view %>
<% end %>
<%= live_component LivebookWeb.SessionLive.InsertButtonsComponent,
id: "#{@section_view.id}:last",
persistent: @section_view.cell_views == [],

View file

@ -122,7 +122,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 flex flex-col space-y-3">
<h3 class="text-2xl font-semibold text-gray-800">
Keyboard shortcuts
@ -134,54 +134,55 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
the notebook and execute commands, whereas in the <span class="font-semibold">insert mode</span>
you have editor focus and directly modify the given cell content.
</p>
<%= render_shortcuts_section("Navigation mode", @shortcuts.navigation_mode, @basic, @platform) %>
<%= render_shortcuts_section("Insert mode", @shortcuts.insert_mode, @basic, @platform) %>
<%= render_shortcuts_section("Universal", @shortcuts.universal, @basic, @platform) %>
<.shortcuts_section title="Navigation mode" shortcuts={@shortcuts.navigation_mode} basic={@basic} platform={@platform} />
<.shortcuts_section title="Insert mode" shortcuts={@shortcuts.insert_mode} basic={@basic} platform={@platform} />
<.shortcuts_section title="Universal" shortcuts={@shortcuts.universal} basic={@basic} platform={@platform} />
<div class="mt-8 flex justify-end">
<form phx-change="settings" onsubmit="return false;" phx-target="<%= @myself %>">
<%= render_switch("basic", @basic, "Basic view (essential shortcuts only)") %>
<form phx-change="settings" onsubmit="return false;" phx-target={@myself}>
<.switch_checkbox
name="basic"
label="Basic view (essential shortcuts only)"
checked={@basic} />
</form>
</div>
</div>
"""
end
defp render_shortcuts_section(title, shortcuts, basic, platform) do
defp shortcuts_section(assigns) do
shortcuts =
if basic do
Enum.filter(shortcuts, & &1[:basic])
if assigns.basic do
Enum.filter(assigns.shortcuts, & &1[:basic])
else
shortcuts
assigns.shortcuts
end
{left, right} = split_in_half(shortcuts)
assigns = %{title: title, left: left, right: right, platform: platform}
assigns = assign(assigns, left: left, right: right)
~L"""
~H"""
<h3 class="text-lg font-medium text-gray-900 pt-4">
<%= @title %>
</h3>
<div class="mt-2 flex flex-col lg:flex-row lg:space-x-4">
<div class="lg:flex-grow">
<%= render_shortcuts_section_table(@left, @platform) %>
<.shortcuts_section_table shortcuts={@left} platform={@platform} />
</div>
<div class="lg:w-1/2">
<%= render_shortcuts_section_table(@right, @platform) %>
<.shortcuts_section_table shortcuts={@right} platform={@platform} />
</div>
</div>
"""
end
defp render_shortcuts_section_table(shortcuts, platform) do
assigns = %{shortcuts: shortcuts, platform: platform}
~L"""
defp shortcuts_section_table(assigns) do
~H"""
<table>
<tbody>
<%= for shortcut <- @shortcuts do %>
<tr>
<td class="py-2 pr-3">
<%= render_shortcut_seq(shortcut, @platform) %>
<.shortcut shortcut={shortcut} platform={@platform} />
</td>
<td class="lg:whitespace-nowrap">
<%= shortcut.desc %>
@ -193,19 +194,24 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
"""
end
defp render_shortcut_seq(shortcut, platform) do
defp shortcut(%{shortcut: shortcut, platform: platform}) do
seq = shortcut[:"seq_#{platform}"] || shortcut.seq
press_all = Map.get(shortcut, :press_all, false)
joiner =
if press_all do
remix_icon("add-line", class: "text-lg text-gray-600")
assigns = %{}
~H"""
<.remix_icon icon="add-line" class="text-lg text-gray-600" />
"""
end
elements = Enum.map_intersperse(seq, joiner, &content_tag("kbd", &1))
assigns = %{elements: elements}
~L"""
~H"""
<div class="flex space-x-1 items-center markdown">
<%= for element <- @elements do %>
<%= element %>

View file

@ -1,77 +0,0 @@
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

View file

@ -0,0 +1,70 @@
defmodule LivebookWeb.SidebarHelpers do
use Phoenix.Component
import LivebookWeb.Helpers
import LivebookWeb.UserHelpers
alias LivebookWeb.Router.Helpers, as: Routes
@doc """
Renders sidebar container.
Other functions in this module render sidebar
items of various type.
"""
def sidebar(assigns) do
~H"""
<div class="w-16 flex flex-col items-center space-y-5 px-3 py-7 bg-gray-900">
<%= render_block(@inner_block) %>
</div>
"""
end
def logo_item(assigns) do
~H"""
<span>
<%= live_patch to: Routes.home_path(@socket, :page) do %>
<img src="/images/logo.png" height="40" width="40" alt="livebook" />
<% end %>
</span>
"""
end
def button_item(assigns) do
~H"""
<span class="tooltip right distant" aria-label={@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={@data_element}>
<.remix_icon icon={@icon} />
</button>
</span>
"""
end
def link_item(assigns) do
~H"""
<span class="tooltip right distant" aria-label={@label}>
<%= live_patch to: @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(@active, do: "text-gray-50 bg-gray-700")}" do %>
<.remix_icon icon={@icon} class="text-2xl" />
<% end %>
</span>
"""
end
def break_item(assigns) do
~H"""
<div class="flex-grow"></div>
"""
end
def user_item(assigns) do
~H"""
<span class="tooltip right distant" aria-label="User profile">
<%= live_patch to: @path, class: "text-gray-400 rounded-xl h-8 w-8 flex items-center justify-center" do %>
<.user_avatar user={@current_user} text_class="text-xs" />
<% end %>
</span>
"""
end
end

View file

@ -19,20 +19,20 @@ defmodule LivebookWeb.UserComponent do
@impl true
def render(assigns) do
~L"""
~H"""
<div class="p-6 flex flex-col space-y-5">
<h3 class="text-2xl font-semibold text-gray-800">
User profile
</h3>
<div class="flex justify-center">
<%= render_user_avatar(@preview_user, class: "h-20 w-20", text_class: "text-3xl") %>
<.user_avatar user={@preview_user} class="h-20 w-20" text_class="text-3xl" />
</div>
<%= f = form_for :data, "#",
id: "user_form",
phx_target: @myself,
phx_submit: "save",
phx_change: "validate",
phx_hook: "UserForm" %>
<.form let={f} for={:data}
phx-submit="save"
phx-change="validate"
phx-target={@myself}
id="user_form"
phx-hook="UserForm">
<div class="flex flex-col space-y-5">
<div>
<div class="input-label">Display name</div>
@ -42,30 +42,32 @@ defmodule LivebookWeb.UserComponent do
<div class="input-label">Cursor color</div>
<div class="flex space-x-4 items-center">
<div class="border-[3px] rounded-lg p-1 flex justify-center items-center"
style="border-color: <%= @preview_user.hex_color %>">
style={"border-color: #{@preview_user.hex_color}"}>
<div class="rounded h-5 w-5"
style="background-color: <%= @preview_user.hex_color %>">
style={"background-color: #{@preview_user.hex_color}"}>
</div>
</div>
<div class="relative flex-grow">
<%= text_input f, :hex_color, value: @data["hex_color"], class: "input", spellcheck: "false", maxlength: 7 %>
<%= tag :button, class: "icon-button absolute right-2 top-1",
type: "button",
phx_click: "randomize_color",
phx_target: @myself %>
<%= remix_icon("refresh-line", class: "text-xl") %>
<button
class="icon-button absolute right-2 top-1"
type="button"
phx-click="randomize_color"
phx-target={@myself}>
<.remix_icon icon="refresh-line" class="text-xl" />
</button>
</div>
</div>
</div>
<%= tag :button, class: "button button-blue flex space-x-1 justify-center items-center",
type: "submit",
disabled: not @valid %>
<%= remix_icon("save-line") %>
<button
class="button button-blue flex space-x-1 justify-center items-center"
type="submit"
disabled={not @valid}>
<.remix_icon icon="save-line" />
<span>Save</span>
</button>
</div>
</form>
</.form>
</div>
"""
end

View file

@ -1,30 +1,25 @@
defmodule LivebookWeb.UserHelpers do
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
use Phoenix.Component
alias Livebook.Users.User
@doc """
Renders user avatar,
Renders user avatar.
## Options
## Examples
* `:class` - class added to the avatar box
* `:text_class` - class added to the avatar text
<.user_avatar user={@user} class="h-20 w-20" text_class="text-3xl" />
"""
def render_user_avatar(user, opts \\ []) do
assigns = %{
name: user.name,
hex_color: user.hex_color,
class: Keyword.get(opts, :class, "w-full h-full"),
text_class: Keyword.get(opts, :text_class)
}
def user_avatar(assigns) do
assigns =
assigns
|> assign_new(:class, fn -> "w-full h-full" end)
|> assign_new(:text_class, fn -> "" end)
~L"""
<div class="rounded-full <%= @class %> flex items-center justify-center" style="background-color: <%= @hex_color %>">
<div class="<%= @text_class %> text-gray-100 font-semibold">
<%= avatar_text(@name) %>
~H"""
<div class={"#{@class} rounded-full flex items-center justify-center"} style={"background-color: #{@user.hex_color}"}>
<div class={"#{@text_class} text-gray-100 font-semibold"}>
<%= avatar_text(@user.name) %>
</div>
</div>
"""

View file

@ -4,7 +4,7 @@
<div class="flex items-center space-x-2 rounded-lg px-4 py-2 bg-blue-100 text-blue-600 hover:opacity-75 cursor-pointer" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info">
<%= remix_icon("information-line", class: "text-2xl") %>
<.remix_icon icon="information-line" class="text-2xl" />
<span class="whitespace-pre-wrap"><%= live_flash(@flash, :info) %></span>
</div>
<% end %>
@ -13,7 +13,7 @@
<div class="flex items-center space-x-2 rounded-lg px-4 py-2 bg-red-100 text-red-400 hover:opacity-75 cursor-pointer" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error">
<%= remix_icon("error-warning-line", class: "text-2xl") %>
<.remix_icon icon="error-warning-line" class="text-2xl" />
<span class="whitespace-pre-wrap"><%= live_flash(@flash, :error) %></span>
</div>
<% end %>

View file

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Livebook" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")}/>
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}></script>
</head>
<body>
<%= @inner_content %>

View file

@ -34,14 +34,14 @@ defmodule Livebook.MixProject do
defp deps do
[
{:phoenix, "~> 1.5.7"},
{:phoenix, "1.6.0-dev", github: "phoenixframework/phoenix", override: true},
# We point LV to an exact version, because we install
# the npm package from there to bundle all the assets,
# so the Elixir-side version must match
{:phoenix_live_view, "0.15.7"},
{:phoenix_live_dashboard, "~> 0.4"},
{:phoenix_live_view, "0.16.0-dev", github: "phoenixframework/phoenix_live_view"},
{:phoenix_live_dashboard, "0.5.0-dev", github: "phoenixframework/phoenix_live_dashboard"},
{:floki, ">= 0.27.0", only: :test},
{:phoenix_html, "~> 2.11"},
{:phoenix_html, "3.0.0-dev", github: "phoenixframework/phoenix_html", override: true},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:telemetry_metrics, "~> 0.4"},
{:telemetry_poller, "~> 0.4"},

View file

@ -10,17 +10,18 @@
"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.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"phoenix": {:hex, :phoenix, "1.5.9", "a6368d36cfd59d917b37c44386e01315bc89f7609a10a45a22f47c007edf2597", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [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", "7e4bce20a67c012f1fbb0af90e5da49fa7bf0d34e3a067795703b74aef75427d"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
"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.7", "09720b8e5151b3ca8ef739cd7626d4feb987c69ba0b509c9bbdb861d5a365881", [: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", "3a756cf662420272d0f1b3b908cce5222163b5a95aa9bab404f9d29aff53276e"},
"phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "95b9383c6a08be4ab1a0c8a7bbf6b6911d888ca1", []},
"phoenix_html": {:git, "https://github.com/phoenixframework/phoenix_html.git", "d35bebbea395569573ef0e1757cbec735da0573b", []},
"phoenix_live_dashboard": {:git, "https://github.com/phoenixframework/phoenix_live_dashboard.git", "1cc67e3c7275b8e68d8201e5dc3660893ae9e4ec", []},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [: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", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "92100b658b257a9dffc11a4ca13e4e9054048f61", []},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"phoenix_view": {:git, "https://github.com/phoenixframework/phoenix_view.git", "90ce9c9ef5f832f80e956b77d079f79171ed45d0", []},
"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.5.0", "51c998f788c4e68fc9f947a5eba8c215fbb1d63a520f7604134cab0270ea6513", [: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", "5b2c8925a5e2587446f33810a58c01e66b3c345652eeec809b76ba007acde71a"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -418,4 +418,57 @@ defmodule Livebook.LiveMarkdown.ExportTest do
assert expected_document == document
end
test "handles backticks in code cell" do
notebook = %{
Notebook.new()
| name: "My Notebook",
metadata: %{},
sections: [
%{
Notebook.Section.new()
| name: "Section 1",
metadata: %{},
cells: [
%{
Notebook.Cell.new(:elixir)
| source: """
\"\"\"
```elixir
x = 1
```
````markdown
# Heading
````
\"\"\"\
"""
}
]
}
]
}
expected_document = """
# My Notebook
## Section 1
`````elixir
\"\"\"
```elixir
x = 1
```
````markdown
# Heading
````
\"\"\"
`````
"""
document = Export.notebook_to_markdown(notebook)
assert expected_document == document
end
end

View file

@ -5,7 +5,7 @@ defmodule Livebook.Runtime.ErlDist.NodeManagerTest do
test "terminates when the last runtime server terminates" do
{:ok, manager_pid} =
NodeManager.start_link(unload_modules_on_termination: false, anonymous: true)
start_supervised({NodeManager, [unload_modules_on_termination: false, anonymous: true]})
server1 = NodeManager.start_runtime_server(manager_pid)
server2 = NodeManager.start_runtime_server(manager_pid)

View file

@ -5,7 +5,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
setup do
{:ok, manager_pid} =
NodeManager.start_link(unload_modules_on_termination: false, anonymous: true)
start_supervised({NodeManager, [unload_modules_on_termination: false, anonymous: true]})
runtime_server_pid = NodeManager.start_runtime_server(manager_pid)
RuntimeServer.set_owner(runtime_server_pid, self())